VitrualBox迁移TrueNas

类似方式可以用来将VirtualBox虚机迁移到各种虚拟化平台。

磁盘导出

使用Virtualbox命令行工具导入Raw格式的磁盘镜像

# VBoxManage.exe clonehd [虚拟磁盘] --format raw [镜像文件]
C:\'Program Files'\Oracle\VirtualBox\VBoxManage.exe clonehd .\whoops-dev.vhd --format raw whoops-dev.img

DD写入

拷贝镜像文件导nas,创建Zvol,登录终端,使用dd命令把镜像写入对应zvol。

sudo dd if=whoops-dev.img of=/dev/whoops-nas/vm/whoops-dev status=progress

创建虚机

在Truenas控制台创建虚机,镜像选择对应zvol,注意引导方式要和原来Virtualbox一样UEFI或者BIOS。

备份虚机

使用dd命令把对应zvol,拷贝到指定文件。

sudo dd if=/dev/whoops-nas/vm/whoops-dev of=whoops-dev.img status=progress
阅读全文

fio测试

常用参数

-filename=/dev/sdb		#要测试盘的名称,支持文件系统或者裸设备,/dev/sda2或/dev/sdb
-direct=1		 #测试过程绕过机器自带的buffer,使测试结果更真实(Linux在读写时,数据会先写到缓存,再在后台写到硬盘,读的时候也是优先从缓存中读,这样访问速度会加快,但是一旦掉电,缓存中数据就会清空,所有一种模式为DirectIO,可以跳过缓存,直接读写硬盘)
-ioengine=libaio		#定义使用什么io引擎去下发io请求,常用的一些 libaio:Linux本地异步I/O;rbd:通过librbd直接访问CEPH Rados 
-iodepth=16		 #队列的深度为16,在异步模式下,CPU不能一直无限的发命令到硬盘设备。比如SSD执行读写如果发生了卡顿,那有可能系统会一直不停的发命令,几千个,甚至几万个,这样一方面SSD扛不住,另一方面这么多命令会很占内存,系统也要挂掉了。这样,就带来一个参数叫做队列深度。
-bs=4k #单次io的块文件大小为4k
-numjobs=10 #本次测试的线程数是10
-size=5G #每个线程读写的数据量是5GB
-runtime=60 #测试时间为60秒,可以设置2m为两分钟。如果不配置此项,会将设置的size大小全部写入或者读取完为止
-rw=randread #测试随机读的I/O
-rw=randwrite #测试随机写的I/O
-rw=randrw #测试随机混合写和读的I/O
-rw=read #测试顺序读的I/O
-rw=write #测试顺序写的I/O
-rw=rw #测试顺序混合写和读的I/O
-thread #使用pthread_create创建线程,另一种是fork创建进程。进程的开销比线程要大,一般都采用thread测试
-rwmixwrite=30 #在混合读写的模式下,写占30%(即rwmixread读为70%,单独配置这样的一个参数即可)
-group_reporting #关于显示结果的,汇总每个进程的信息
-name="TDSQL_4KB_read_test" #定义测试任务名称
扩展
-lockmem=1g #只使用1g内存进行测试
-zero_buffers #用全0初始化缓冲区,默认是用随机数据填充缓冲区
-random_distribution=random #默认情况下,fio 会在询问时使用完全均匀的随机分布,有需要的话可以自定义访问区域,zipf、pareto、normal、zoned
-nrfiles=8 #每个进程生成文件的数量

报告分析

下面是每个执行的数据方向的I/O统计数据信息的代表值含义

read/write: 读/写的IO操作(还有一个trim没用过)
salt: 提交延迟,这是提交I/O所花费的时间(min:最小值,max:最大值,avg:平均值,stdev:标准偏差)
chat: 完成延迟,表示从提交到完成I/O部分的时间
lat: 相应时间,表示从fio创建I/O单元到完成I/O操作的时间
bw: 带宽统计
iops: IOPS统计
lat(nsec/usec/msec): I/O完成延迟的分布。这是从I/O离开fio到它完成的时间。与上面单独的读/写/修剪部分不同,这里和其余部分的数据适用于报告组的所有I/ o。10=0.01%意味着0.01%的I/O在250us以下完成。250=0.02%意味着0.02%的I/O需要10到250us才能完成。
cpu: cpu使用率
IO depths: I/O深度在作业生命周期中的分布
IO submit: 在一个提交调用中提交了多少个I/O。每一个分录表示该数额及其以下,直到上一分录为止——例如,4=100%意味着我们每次提交0到4个I/O调用
IO complete: 和上边的submit一样,不过这个是完成了多少个
IO issued rwt: 发出的read/write/trim请求的数量,以及其中有多少请求被缩短或删除
IO latency: 满足指定延迟目标所需的I/O深度

下面是Run status group 0 (all jobs) 全部任务汇总信息的代表值含义:

bw: 总带宽以及最小和最大带宽
io: 该组中所有线程执行的累计I/O
run: 这组线程中最小和最长的运行时

最后是Linux中特有的磁盘状态统计信息的代表值含义:

ios: 所有组的I/ o个数
merge: I/O调度器执行的总合并数
ticks: 使磁盘繁忙的滴答数(仅供参考,原文是Number of ticks we kept the disk busy)
in_queue: 在磁盘队列中花费的总时间
util: 磁盘利用率。值为100%意味着我们保留了磁盘,如果一直很忙,那么50%的时间磁盘就会闲置一半的时间

测试用例

# 顺序读
fio --name=big-file-multi-read --directory=/tmp -direct=1 --rw=read --bs=4M --size=2G --numjobs=2
# 顺序写
fio --name=big-file-multi-write --directory=/tmp -direct=1 --rw=write --bs=4M --size=2G --numjobs=2
# 随机读
fio --name=big-file-multi-randread --directory=/tmp -direct=1 --rw=randread --bs=4M --size=2G --numjobs=2
# 随机写
fio --name=big-file-multi-randwrite --directory=/tmp -direct=1 --rw=randwrite --bs=4M --size=2G --numjobs=2
阅读全文

Ansible

介绍

Ansible是一种开源的自动化运维工具,可以实现配置管理、应用部署、任务执行等操作,通过SSH协议进行通信,无需在远程主机上安装客户端,具有简单易用、扩展性强、可靠稳定等特点。

pip install ansible

Inventory

Inventory是一个用于描述被管理节点的列表文件。它定义了Ansible任务将要在哪些主机上执行。清单可以包含IP地址、主机名、组名等信息,以及变量和组相关的配置。

清单文件通常采用INI格式或YAML格式编写,其中每个主机都有对应的主机名或IP地址,并且可以根据需求进行分组。通过在清单中定义主机组,可以更方便地对不同组的主机进行管理和操作。

除了静态清单外,Ansible还支持动态清单,这意味着清单可以从外部数据源(如云平台、自定义脚本、数据库等)动态生成。这样可以实现按需扩展或动态调整被管理节点,提高了灵活性和可扩展性。

使用清单,您可以轻松地指定要在哪些主机上执行特定的Ansible任务或剧本,使得管理和组织被管理节点变得简单而高效。

[self]
127.0.0.1 var1=value1

[all:vars]
all_var=all

Ad-HOC

Ansible的Ad-Hoc命令是一种快速、临时性的方式来执行单个任务或命令,而无需编写和执行完整的Playbook。通过Ad-Hoc命令,可以直接在终端上与远程主机进行交互和操作。

ansible [pattern] -m [module] -a "[module options]"

常用参数

位置参数:
  pattern       匹配主机模式

选项:
  --list-hosts          输出匹配的主机列表,不执行其他任何操作
  --playbook-dir BASEDIR
                        由于此工具不使用playbooks,请将其用作替代playbook目录。这设置了许多功能的相对路径,包括roles/ group_vars/等。
  --task-timeout TASK_TIMEOUT
                        设置任务的超时限制(以秒为单位),必须是正整数。
  -B SECONDS, --background SECONDS
                        异步运行,在X秒后失败(默认=N/A)
  -M MODULE_PATH, --module-path MODULE_PATH
                        在模块库之前添加以冒号分隔的路径(默认={{ ANSIBLE_HOME ~ "/plugins/modules:/usr/share/ansible/plugins/modules" }})
  -P POLL_INTERVAL, --poll POLL_INTERVAL
                        如果使用-B,设置轮询间隔(默认=15)
  -a MODULE_ARGS, --args MODULE_ARGS
                        使用空格分隔的k=v格式指定操作的选项:-a 'opt1=val1 opt2=val2' 或使用JSON字符串:-a '{"opt1": "val1", "opt2": "val2"}'
  -e EXTRA_VARS, --extra-vars EXTRA_VARS
                        设置额外的变量,格式为key=value或YAML/JSON,如果是文件名,请在前面加上@
  -f FORKS, --forks FORKS
                        指定要使用的并行进程数(默认=5)
  -h, --help            显示此帮助消息并退出
  -i INVENTORY, --inventory INVENTORY, --inventory-file INVENTORY
                        指定清单主机路径或逗号分隔的主机列表。--inventory-file已废弃
  -l SUBSET, --limit SUBSET
                        进一步限制所选主机到附加的模式
  -m MODULE_NAME, --module-name MODULE_NAME
                        要执行的操作的名称(默认=command)
  -o, --one-line        精简输出
  -v, --verbose         导致Ansible打印更多的调试消息。添加多个-v将增加详细程度,内置插件当前可评估至-vvvvvv。开始时一个合理的级别是-vvv,连接调试可能需要-vvvv。

特权升级选项:
  --become-method BECOME_METHOD
                        要使用的特权升级方法(默认=sudo),使用ansible-doc -t become -l列出有效选项。
  --become-user BECOME_USER
                        以此用户身份运行操作(默认=root)
  -b, --become          使用特权升级运行操作(不会提示密码)

连接选项:
  --private-key PRIVATE_KEY_FILE, --key-file PRIVATE_KEY_FILE
                        使用此文件进行连接身份验证
  --scp-extra-args SCP_EXTRA_ARGS
                        指定传递给scp的额外参数(例如-l)
  --sftp-extra-args SFTP_EXTRA_ARGS
                        指定传递给sftp的额外参数(例如-f、-l)
  --ssh-common-args SSH_COMMON_ARGS
                        指定传递给sftp/scp/ssh的常见参数(例如ProxyCommand)
  --ssh-extra-args SSH_EXTRA_ARGS
                        指定传递给ssh的额外参数(例如-R)
  -T TIMEOUT, --timeout TIMEOUT
                        覆盖连接超时时间(以秒为单位)(默认=10)
  -c CONNECTION, --connection CONNECTION
                        要使用的连接类型(默认=smart)
  -u REMOTE_USER, --user REMOTE_USER
                        以此用户身份连接(默认=None)

Playbook

Ansible的Playbook是一种用于定义、配置和部署多个任务的文件。它是一种结构化的、可扩展的自动化工具,可以实现复杂的运维流程和长期管理。

Playbook使用YAML格式编写,由一个或多个任务(tasks)组成。每个任务都定义了要在目标主机上执行的具体操作,如执行命令、复制文件、安装软件包等。任务可以根据需要指定不同的模块、参数和条件。

ansible-playbook playbook [playbook ...]

以下是一个简单的Playbook示例:

- name: Install and start Nginx
  hosts: web_servers
  become: true

  tasks:
    - name: Install Nginx package
      apt:
        name: nginx
        state: present

    - name: Copy Nginx configuration file
      copy:
        src: /path/to/nginx.conf
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: '0644'

    - name: Start Nginx service
      service:
        name: nginx
        state: started

这个Playbook中,name字段是Playbook的名称,hosts字段指定了目标主机或主机组,become: true表示以特权用户身份运行任务。

通过执行ansible-playbook命令,可以使用Playbook来自动化执行任务和操作。例如,可以使用以下命令运行上述示例Playbook:

ansible-playbook -i inventory.ini  playbook.yml

常用参数

位置参数:
  playbook              Playbook文件名

选项:
  --flush-cache         清除清单中每个主机的事实缓存
  --force-handlers      即使任务失败也运行handlers
  --list-hosts          输出匹配的主机列表,不执行其他任何操作
  --list-tags           列出所有可用的标签
  --list-tasks          列出将要执行的所有任务
  --skip-tags SKIP_TAGS
                        只运行标签与这些值不匹配的plays和tasks
  --start-at-task START_AT_TASK
                        从与此名称匹配的任务开始运行playbook
  --step                逐步确认每个任务是否运行
  --syntax-check        对playbook执行语法检查,但不执行它
  -M MODULE_PATH, --module-path MODULE_PATH
                        在模块库之前添加以冒号分隔的路径(默认值为{{ ANSIBLE_HOME ~ "/plugins/modules:/usr/share/ansible/plugins/modules" })
  -e EXTRA_VARS, --extra-vars EXTRA_VARS
                        设置附加的变量,格式为key=value或YAML/JSON,如果是文件名则在前面加上@
  -f FORKS, --forks FORKS
                        指定要使用的并行进程数(默认值为5)
  -h, --help            显示此帮助消息并退出
  -i INVENTORY, --inventory INVENTORY, --inventory-file INVENTORY
                        指定清单主机路径或逗号分隔的主机列表。--inventory-file已弃用
  -k, --ask-pass        提示输入连接密码
  -l SUBSET, --limit SUBSET
                        进一步将选定的主机限制为附加的模式
  -t TAGS, --tags TAGS  只运行带有这些标签的plays和tasks
  -v, --verbose         导致Ansible打印更多的调试信息。多次添加-v会增加详细程度,内置插件目前支持最多-vvvvvv。开始时一个合理的级别是-vvv,连接调试可能需要-vvvv。

连接选项:
  --private-key PRIVATE_KEY_FILE, --key-file PRIVATE_KEY_FILE
                        使用此文件进行连接身份验证
  --scp-extra-args SCP_EXTRA_ARGS
                        指定传递给scp的额外参数(例如-l)
  --sftp-extra-args SFTP_EXTRA_ARGS
                        指定传递给sftp的额外参数(例如-f,-l)
  --ssh-common-args SSH_COMMON_ARGS
                        指定传递给sftp/scp/ssh的常见参数(例如ProxyCommand)
  --ssh-extra-args SSH_EXTRA_ARGS
                        指定传递给ssh的额外参数(例如-R)
  -T TIMEOUT, --timeout TIMEOUT
                        覆盖连接超时时间(以秒为单位)(默认值为10)
  -c CONNECTION, --connection CONNECTION
                        使用的连接类型(默认为smart)
  -u REMOTE_USER, --user REMOTE_USER
                        以此用户身份连接(默认为None)

特权升级选项:
  --become-method BECOME_METHOD
                        使用的特权升级方法(默认为sudo),使用`ansible-doc -t become -l`列出有效选择。
  --become-user BECOME_USER
                        以此用户身份运行操作(默认为root)
  -b, --become          以特权身份运行操作(不会提示输入密码)
阅读全文

Git使用GPG签名

生成秘钥

gpg --full-generate-key

列入所有keys

gpg --list-keys

输出如下,获取KeyID

pub   rsa4096 2023-06-12 [SC]
      <KeyID>
uid           [ultimate] name (comment) <email>
sub   rsa4096 2023-06-12 [E]

导出公钥,可以放到Github或者Gitlab。

gpg --armor --export <KeyID>

输入如下

-----BEGIN PGP PUBLIC KEY BLOCK-----
...
-----END PGP PUBLIC KEY BLOCK-----

设置Git

设置git commit时自动签名。

git config --global user.signingkey <KeyID>
git config --global gpg.program gpg
git config --global commit.gpgsign true

备份、恢复公私钥

以下是备份 GPG 公私钥的步骤:

打开命令行终端并输入以下命令来导出您的私钥:

gpg --export-secret-keys -a <KeyID> > private.key

输入以下命令以导出您的公钥:

gpg --export -a <KeyID> > public.key

将生成的两个文件(private.key 和 public.key)复制到您想要备份它们的位置,如 USB 磁盘或云存储。

您可以在需要恢复密钥时使用这些备份文件来重新导入密钥。使用以下命令将私钥导入到 GPG 中:

gpg --import private.key

最后,使用以下命令将公钥导入到 GPG 中:

gpg --import public.key

这样就完成了 GPG 公私钥的备份和恢复。

阅读全文

扩展Kubernetes(一)

Controller and CustomResourceDefinitions

Kubernetes的Custom Resource Definition(CRD)和Controller机制是Kubernetes提供的一种扩展Kubernetes API的方法,可以用于自定义Kubernetes资源类型以及对这些资源的控制。

CRD是一种自定义的Kubernetes资源类型,它允许用户在Kubernetes中创建新的资源类型。CRD通过Kubernetes API Server暴露出来,使得用户可以使用kubectl或其他工具对其进行管理。

Controller是CRD的另一个重要组成部分,它实现了对CRD资源的控制逻辑。Controller会根据CRD资源的状态变化来触发相应的操作,比如创建、更新、删除等。Controller通常会通过watch机制来监听CRD资源的变化,并根据实际情况对其进行处理。CRD和Controller机制为Kubernetes提供了更强大的扩展能力,使得用户可以更好地适应不同场景的需求。

Extend Kubernetes with CustomResourceDefinitions.

  • 定义、创建自定义资源
# crd.yaml
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: foos.samplecontroller.k8s.io
spec:
  group: samplecontroller.k8s.io
  version: v1alpha1
  names:
    kind: Foo
    plural: foos
  scope: Namespaced

kubectl apply -f crd.yaml
  • Kubectl操作自定义资源
kubectl get foo
kubectl watch foo
  • Client-go操作自定义资源
import (
    "context"

    samplecontrollerv1alpha1 "k8s.io/sample-controller/pkg/apis/samplecontroller/v1alpha1"
    clientset "k8s.io/sample-controller/pkg/generated/clientset/versioned"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func main() {
    // 获取 kubernetes 集群的 rest.Config 对象
    config, err := rest.InClusterConfig()
    if err != nil {
        panic(err)
    }
    
    // 创建 CRD 客户端
    cs, err := clientset.NewForConfig(config)
    if err != nil {
        panic(err)
    }

    // 创建自定义对象 foo
    foo := &samplecontrollerv1alpha1.Foo{
        ObjectMeta: metav1.ObjectMeta{
            Name:      "example-foo",
            Namespace: "default",
        },
        Spec: samplecontrollerv1alpha1.FooSpec{
            Size: 3,
        },
    }

    // 创建或更新自定义对象 foo
    result, err := cs.SamplecontrollerV1alpha1().Foos("default").Create(context.Background(), foo, metav1.CreateOptions{})
    if err != nil {
        result, err = cs.SamplecontrollerV1alpha1().Foos("default").Update(context.Background(), foo, metav1.UpdateOptions{})
        if err != nil {
            panic(err)
        }
    }

    // 删除自定义对象 foo
    err = cs.SamplecontrollerV1alpha1().Foos("default").Delete(context.Background(), foo.Name, metav1.DeleteOptions{})
    if err != nil {
        panic(err)
    }
}

Write Controller with client-go

Controller Architecture

CRD Client code generate

k8s.io/code-generator 是Kubernetes官方提供的一种代码生成工具。这个工具可以根据定义好的CRD来自动生成客户端代码,包括类型定义、列表和单个对象的获取、创建、更新和删除等操作的接口,以及相关实现代码。

通过使用k8s.io/code-generator, 我们可以避免手动编写大量重复的代码,从而提高开发效率和代码质量。

SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/..
CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)}

bash "${CODEGEN_PKG}"/generate-groups.sh "deepcopy,client,informer,lister" \
k8s.io/sample-controller/pkg/generated k8s.io/sample-controller/pkg/apis \
samplecontroller:v1alpha1 \
--output-base "$(dirname "${BASH_SOURCE[0]}")/../../.." \
--go-header-file "${SCRIPT_ROOT}"/hack/boilerplate.go.txt
# pkg/generated
├── clientset
│   └── versioned
│       ├── clientset.go
│       ├── doc.go
│       ├── fake
│       │   ├── clientset_generated.go
│       │   ├── doc.go
│       │   └── register.go
│       ├── scheme
│       │   ├── doc.go
│       │   └── register.go
│       └── typed
│           └── samplecontroller
│               └── v1alpha1
│                   ├── doc.go
│                   ├── fake
│                   │   ├── doc.go
│                   │   ├── fake_foo.go
│                   │   └── fake_samplecontroller_client.go
│                   ├── foo.go
│                   ├── generated_expansion.go
│                   └── samplecontroller_client.go
├── informers
│   └── externalversions
│       ├── factory.go
│       ├── generic.go
│       ├── internalinterfaces
│       │   └── factory_interfaces.go
│       └── samplecontroller
│           ├── interface.go
│           └── v1alpha1
│               ├── foo.go
│               └── interface.go
└── listers
    └── samplecontroller
        └── v1alpha1
            ├── expansion_generated.go
            └── foo.go

Client-go components

  • Reflector: 定义在type Reflector inside package cache,
    通过Kubernetes API监控特定资源,资源可以为kubernetes内建资源也可以是自定义资源.
    当reflector收到来自API的资源更新或创建通知,它将通过相应的API创建一个对象,并将它推入Delta Fifo队列.

  • Informer: 定义在base controller inside package cache,
    它从Delta Fifo队列弹出一个对象保存下来,同时调用我们的controller并传递该对象.

  • Indexer: 定义在type Indexer inside package cache.
    为对象提供索引功能,一个典型的使用场景是基于对象的labels创建索引.
    Indexer维护索引通过一系列的索引函数,并使用一个线程安全的池子保存对象和它们的key.

Custom Controller components

  • Informer reference: Informer实例引用,
    需要我们在我们的controller代码中自己创建.

  • Indexer reference: Indexer实例引用,
    需要我们在我们的controller代码中自己创建,我们在执行处理过程中通过它来取回对象.

  • Resource Event Handlers: 一系列调函数,通过它们Informer传递对象给我们的controller.
    一个典型的模式为该回调函数获得对象的key后将对象的key推入workqueue为接下来的处理.

  • Work queue: 解耦对象传递和对象处理过程的队列.

  • Process Item: 对象具体的处理过程: 一般会使用Indexer reference或者Listing包装函数通过对象key取回对象.

import (
    "context"
    
    samplecontrollerv1alpha1 "k8s.io/sample-controller/pkg/apis/samplecontroller/v1alpha1"
    clientset "k8s.io/sample-controller/pkg/generated/clientset/versioned"
    informers "k8s.io/sample-controller/pkg/generated/informers/externalversions/samplecontroller/v1alpha1"
    listers "k8s.io/sample-controller/pkg/generated/listers/samplecontroller/v1alpha1"

    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/util/wait"
    "k8s.io/client-go/tools/cache"
    "k8s.io/client-go/util/workqueue"
)

type Controller struct {
    // 客户端,用于操作CRD
    clientset clientset.Interface
    
    // Informer 机制,确保 CRD 数据的及时获取和同步到内存中
    fooInformer informers.FooInformer
    fooLister   listers.FooLister
    
    // 工作队列,用于控制协程数目和并发度
    workqueue workqueue.RateLimitingInterface
    
    // 自定义对象转换接口
    scheme *runtime.Scheme
}

func NewController(clientset clientset.Interface, fooInformer informers.FooInformer) *Controller {
    c := &Controller{
        clientset: clientset,
        fooInformer: fooInformer,
        fooLister: fooInformer.Lister(),
        workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Foos"),
        scheme: runtime.NewScheme(),
    }
    // 注册自定义 API 对象到 scheme 中
    samplecontrollerv1alpha1.AddToScheme(c.scheme)
    // 绑定各种事件触发的回调函数
    fooInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
        AddFunc: c.enqueueFoo,
        UpdateFunc: func(old, new interface{}) {
            c.enqueueFoo(new)
        },
        DeleteFunc: c.enqueueFooForDelete,
    })
    return c
}

func (c *Controller) Run(stopCh <-chan struct{}) error {
    defer c.workqueue.ShutDown()

    if !cache.WaitForCacheSync(stopCh, c.fooInformer.Informer().HasSynced) {
        return fmt.Errorf("failed to wait for caches to sync")
    }

    // 启动多个协程,处理工作队列中待处理的任务
    for i := 0; i < numWorkers; i++ {
        go wait.Until(c.runWorker, time.Second, stopCh)
    }

    <-stopCh
    return nil
}

func (c *Controller) runWorker() {
    for c.processNextWorkItem() {
    }
}

func (c *Controller) processNextWorkItem() bool {
    obj, shutdown := c.workqueue.Get()
    if shutdown {
        return false
    }
    defer c.workqueue.Done(obj)

    key := obj.(string)
    namespace, name, err := cache.SplitMetaNamespaceKey(key)
    if err != nil {
        c.workqueue.Forget(obj)
        return true
    }

    foo, err := c.fooLister.Foos(namespace).Get(name)
    if err != nil {
        if errors.IsNotFound(err) {
            c.workqueue.Forget(obj)
            return true
        }
        c.workqueue.AddRateLimited(obj)
        return true
    }

    // TODO: 根据业务逻辑处理自定义对象 foo
    
    // 处理完一个任务,将其从队列中删除
    c.workqueue.Forget(obj)
    return true
}

func (c *Controller) enqueueFoo(obj interface{}) {
    foo := obj.(*samplecontrollerv1alpha1.Foo)
    key, err := cache.MetaNamespaceKeyFunc(foo)
    if err != nil {
        return
    }
    c.workqueue.Add(key)
}

func (c *Controller) enqueueFooForDelete(obj interface{}) {
    foo, ok := obj.(*samplecontrollerv1alpha1.Foo)
    if !ok {
        tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
        if !ok {
            return
        }
        foo, ok = tombstone.Obj.(*samplecontrollerv1alpha1.Foo)
        if !ok {
            return
        }
    }
    key, err := cache.MetaNamespaceKeyFunc(foo)
    if err == nil {
        c.workqueue.Add(key)
    }
}

Controller Runtime

Controller-runtime是Kubernetes官方提供的一种Go语言工具包,用于编写自定义控制器。该工具包基于Kubernetes API机制,封装了常见的控制器开发模式,如Watch Reconcile、Leader Election、Event Emitting等。

Controller-runtime为Kubernetes控制器的开发提供了很多便利,使得开发者可以更加专注于核心业务逻辑的实现。

Controller-runtime Architecture

  1. Manager:管理器

Manager 是整个框架的核心,其负责以下几个任务:

  • 创建并启动各种控制器(Controller)
  • 启动 Web 服务器,提供健康检查和指标监控等服务
  • 提供客户端(Client)实例,用于操作 kubernetes API
  • 可以通过配置文件或环境变量来自定义控制器运行参数
  1. Client:客户端

Client 是操作 kubernetes API 的客户端,通常情况下我们使用 kubernetes/client-go 库即可,但是在 controller-runtime 框架中,它对原生的 client-go 库进行了封装,提供了更加易用的接口,同时还支持对多个版本的 API 对象进行操作。

  1. Cache:缓存

Cache 是用于缓存 kubernetes API 对象的组件,它可以高效地从 kubernetes API 中获取对象,并将其同步到内存中,以便控制器快速地获取和处理对象。

  1. Controller:控制器

Controller 是控制器的核心组件,用于监听和处理 kubernetes API 对象的变化,并在需要时调用 Reconciler 进行协调。

  1. Reconciler:协调器

Reconciler 是一个接口,用于协调处理 kubernetes API 对象的状态。在 controller-runtime 中,每个控制器都必须关联一个 Reconciler 实现,以便在对象出现变化时调用该 Reconciler 进行处理。Reconciler 接口定义如下:

type Reconciler interface {
    // 计算出与期望状态不同的部分
    Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error)
}

在 Reconcile 函数中,我们可以根据业务逻辑计算出当前对象的期望状态,然后与 kubernetes API 中的实际状态进行比较,如果两者不同,则更新实际状态,否则直接返回。

Cotroller-runtime 提供了一套完整的架构,封装了大量的功能代码,让开发 controller 变得更加简单方便。在使用 controller-runtime 构建 controller 时,我们继续只需要关注 Reconciler接口的实现即可。

Write Controller with controller-runtime

import (
    "context"

    samplecontrollerv1alpha1 "k8s.io/sample-controller/pkg/apis/samplecontroller/v1alpha1"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/util/wait"
    "k8s.io/apimachinery/pkg/api/errors"

    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

func main() {
    // 获取 kubernetes 集群的 rest.Config 对象
    config, err := rest.InClusterConfig()
    if err != nil {
        panic(err)
    }

    // 创建 kubernetes API 对象的编解码器
    scheme := runtime.NewScheme()
    samplecontrollerv1alpha1.AddToScheme(scheme)

    // 创建 manager 实例
    mgr, err := ctrl.NewManager(config, ctrl.Options{
        Scheme: scheme,
    })
    if err != nil {
        panic(err)
    }

    // 创建 Reconciler 实现
    reconciler := &FooReconciler{
        client: mgr.GetClient(),
        scheme: scheme,
    }

    // 创建控制器对象并注册到manager
    ctrl.NewControllerManagedBy(mgr).
        For(&samplecontrollerv1alpha1.Foo{}).
        WithEventFilter(predicate.GenerationChangedPredicate{}).
        Complete(reconciler)

    // 启动 manager,开始运行 controller
    if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
        panic(err)
    }
}

// FooReconciler 实现了 Reconciler 接口
type FooReconciler struct {
    client client.Client
    scheme *runtime.Scheme
}

func (r *FooReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    // 获取 kubernetes API 对象
    foo := &samplecontrollerv1alpha1.Foo{}
    if err := r.client.Get(ctx, req.NamespacedName, foo); err != nil {
        if errors.IsNotFound(err) {
            return ctrl.Result{}, nil
        }
        return ctrl.Result{}, err
    }

    // 更新或创建 kubernetes API 对象
    if err := controllerutil.SetControllerReference(foo, foo, r.scheme); err != nil {
        return ctrl.Result{}, err
    }
    _, err := r.client.CreateOrUpdate(ctx, foo)
    if err != nil {
        return ctrl.Result{}, err
    }

    return ctrl.Result{}, nil
}
阅读全文