惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The GitHub Blog
The GitHub Blog
云风的 BLOG
云风的 BLOG
T
Threatpost
WordPress大学
WordPress大学
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
PCI Perspectives
PCI Perspectives
T
The Exploit Database - CXSecurity.com
Y
Y Combinator Blog
雷峰网
雷峰网
爱范儿
爱范儿
The Hacker News
The Hacker News
Last Week in AI
Last Week in AI
Simon Willison's Weblog
Simon Willison's Weblog
T
Tor Project blog
S
Securelist
宝玉的分享
宝玉的分享
L
LangChain Blog
O
OpenAI News
AI
AI
P
Privacy International News Feed
L
LINUX DO - 最新话题
D
DataBreaches.Net
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Attack and Defense Labs
Attack and Defense Labs
罗磊的独立博客
M
MIT News - Artificial intelligence
Security Archives - TechRepublic
Security Archives - TechRepublic
月光博客
月光博客
博客园 - 【当耐特】
T
Tailwind CSS Blog
C
Cybersecurity and Infrastructure Security Agency CISA
H
Help Net Security
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园_首页
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Hacker News - Newest:
Hacker News - Newest: "LLM"
腾讯CDC
Jina AI
Jina AI
The Last Watchdog
The Last Watchdog
K
Kaspersky official blog
Webroot Blog
Webroot Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Blog — PlanetScale
Blog — PlanetScale
MyScale Blog
MyScale Blog
MongoDB | Blog
MongoDB | Blog
P
Proofpoint News Feed
Recorded Future
Recorded Future
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
博客园 - 三生石上(FineUI控件)
The Cloudflare Blog

陈少文的网站

巨变与机遇的未来十年 Kubernetes 平台管理软件压力测试方案 使用镜像部署 Hexo 静态页面 终于等到你 - GitHub 镜像仓库服务(ghcr.io) 一起来学 Go --(6)Interface 一起来学 Go --(5)Goroutine 和 Channel 什么是函数式编程 如何在 Kubernetes 集群集成 Kata 柯里化与偏函数 使用 PyGithub 自动创建 Label 软件产品是团队能力的输出 Helm 2 、Helm 3 比较 IoT 变现 Kubernetes 中的 DNS 服务 国内的 Helm 镜像源 Harbor 使用自签证书支持 Https 访问 DevOps 工具链之 Prow 如何使用 kfctl 安装 Kubeflow VS Code 无法下载 Go 插件的工具包 工程师更应具有服务精神 你不知道的 Docker 使用技巧 使用 Docker 运行 Tensorflow 论中国 什么是左移 如何清空 Git 仓库全部历史记录 一禅小和尚 有风吹过厨房 时间的玫瑰 如何在 CentOS 安装 GPU 驱动 开发 Tips(19) 使用 Velero 备份 Kubernetes 集群 Kubernetes Cheat Sheet 开发 Tips(18) 如何构建一个 Java 工程 开发 Tips(17) KubeSpray 安装 Kubernetes 报错 ip in ansible_all_ipv4_addresses 基于 Kubernetes 和 Jenkins 搭建自动化测试系统 在 Kubernetes 上动态创建 Jenkins Slave 使用 Jenkins 进行服务拨测 开发 Tips(16) Kubernetes 签发 Ingress 证书及日常故障运维 Kubernetes 中 Deployment 的基本操作 Kubernetes 中的证书 如何使用 KubeBuilder 开发一个 Operator Kubernetes 1.6.0 安装问题汇总 镜像管理工具 -- Harbor 开发 Tips(15) Docker 如何拉取镜像 开发 Tips(14) 使用 Helm 安装 harbor 开发 Tips(13) 使用 S2I 构建云原生应用 在 Kubernetes 中使用 emptyDir、hostPath、localVolume 开发 Tips(12) 开发 Tips(11) 代码质量分析工具 SonarQube 使用 Kubeadm 安装 Kubernetes 集群 一起来学 Go --(4)常用函数 Kubernetes 中的 Ceph Kubernetes 之 Volumes Kubernetes 之 Labels、Selectors 开发 Tips(10) 开源正在重构商业模式 Kubernetes 之网络 Kubernetes 之 API 使用 Helm 和 Operator 快速部署 Prometheus Kubernetes 复杂有状态应用管理框架 -- Operator Kubernetes 的包管理器 -- Helm 一起来学 Go --(3)Go Modules 如何一步一步地优化博客方案 kubectl 实用指南 Kubernetes 中的基本概念 搭建远程 Kubernetes 开发环境 大公司和小公司的 ToB 思路 开发 Tips(9) Go 入门指南 一起来学 Go --(2)数据与逻辑结构 如何预防 Web 富文本中的 XSS 攻击 django-xss-cleaner 云工作时代 一起来学 Go --(1)背景与特点 SaaS 开发团队的不同阶段 你不知道的 Git 使用技巧 输出既服务 微服务设计 继续奔跑 开发 Tips(8) 从账户安全到二次验证 Django 性能之数据库查询优化 Django 性能之分库分表 敏捷开发之研发流程 打造一致性的团队 开发 Tips(7) Pytest 进阶学习之 Mock PaaS 部署之 buildpack Go 开发配置 领域输出才是 PaaS 的核心竞争力 Pytest 入门学习 开发 Tips(6) 如何使用 Jenkins、Docker、GitLab 搭建 Django 自动化部署流程
Tekton 优化之定制集群调度器
微信公众号 · 2023-04-26 · via 陈少文的网站

1. 受限的构建环境无法满足构建需求

Tekton 是基于 Kubernetes 集群的 CICD 引擎,相较于 Jenkins 更加云原生。说人话就是,更好开发插件、更好扩容、更好可观测性、更好玩儿。

由于代码仅能落盘公司内网,导致构建集群仅能部署于办公内网。这导致了很多受限:

  • 硬件资源,没有弹性扩容能力
  • 网络受限,访问 github.com、docker.io、dl-cdn.alpinelinux.org 很慢
  • 可靠性受限,机房稳定环境得不到保障、硬件故障率高
  • 运维受限,维护系统必须先接入公司内网

但这些都是 CICD 研发的事情,没人会关注,我只能默默承受,想办法解决,谁让我是个打工人。

直到忍无可忍的业务研发,开始公开喷 “为什么 CDN 上传任务这么慢,以前不是这样的” 。那是因为,以前我没入职,我来了早就慢下来了。

没办法,业务研发是构建系统的用户,作为平台开发者只能再想想办法,就调研了一个云连接的方案。每个月 1w 人民币,可以接入华为云的海外 VPN 专线,直连海外。

但花钱的事儿,业务怎么肯干?程序员的事,他们怎么会愿意花钱?

2. 跨区域的 Kubernetes 构建集群

有意思的是,我上班的公司,是多区域办公,各个区域的内网互通,但是出口网络不一样。其中有一个区域,有很多海外业务线,出口网络质量格外好。

这种网络环境,正好可以用来搭建跨区域的 Kubernetes 集群,用于构建。如下图:

在访问海外质量好的区域,新增若干构建节点。

如上图,我们计划将访问海外资源的 CICD 任务调度到 Good to Google 节点上执行。因此,我们需要一个 Kubernetes 集群调度器,能够根据不同任务定制调度策略。

3. 常见的几种调度器扩展方式

  • default-scheduler[1]

直接在 scheduler 源码的基础上,进行硬编码修改,然后重新编译 kube-scheduler,替换掉原来的 kube-scheduler

  • custom scheduler

在创建资源时,可以设置 spec.schedulerName 字段来指定使用哪个调度器处理。这种方式下,一个集群共存多个调度器,每个调度器的 sheduler name 不同。

  • scheduler extender[2]

如上图,scheduler extender 提供了几个扩展点,当 kube-scheduler 调度流程进入该扩展阶段时,会向 sheduler extender 发送 http 请求,处理定制逻辑。

部署时,可以使用 Deployment 部署 scheduler extender,修改 kube-scheduler 启动参数指向 scheduler extender 的地址即可。

  • scheduler framework

如上图,scheduler framework 也提供了几个扩展点,根据处理的阶段,实现对应的接口。

部署时,直接替换 kube-scheduler 的镜像,添加配置文件说明启用哪些 plugin 即可。

4. 定制集群调度器

上述四种方式,第一种硬编码太硬核,第二种适用于多租户场景。第三种和第四种都是基于扩展点,但第三种需要部署额外的组件,第四种直接替换 kube-scheduler 镜像。

第三种调度时需要发起 http 请求、创建集群 Client 维护 Informer ,第四种效率应该会更高,也更新更有未来。网上相关的教程挺详细,这里主要记录下遇到的坑。

4.1 如何新建项目

打开 https://github.com/kubernetes-sigs/scheduler-plugins/ 切换到集群对应的 tag,构建集群是 Kubernetes 1.21 ,因此选择 v0.21.6。

这个项目下有很多可以参考的 plugin,我们可以直接拿来用,也可以根据自己的需求修改定制。

拷贝 go.mod 文件 replace 部分,新建一个自己的 go 项目。

这样做的目的是:

  • 让调度器的代码依赖版本与集群版本保持一致,否则跨版本太多会有参数不兼容问题
  • 避免编译时,依赖报错,处理起来很费时

4.2 快速设计

  • 怎么指定命名空间、任务,到特定节点
1
2
3
kubectl annotate namespace kube-system com.chenshaowen.scheduler-plugin.filterimage=cdn
kubectl annotate namespace kube-system com.chenshaowen.scheduler-plugin.nodes=node2
kubectl annotate namespace kube-system com.chenshaowen.scheduler-plugin.ns=default

由于是全局配置,直接将配置信息写在某个命名空间的 annotation 中,当数据库用。

通过 Pod 的镜像识别出是否为 CDN 任务。但并不是每个项目的 CDN 任务都需要走海外节点,还有上传国内的 CDN 任务。

上面数据下,最终的效果应该是,允许 default 命名空间下,镜像包含 cdn 字符串的 Pod 优先调度到 node2 节点;禁止镜像不包含 cdn 字符串的 Pod 调度到 node2 节点,禁止其他命名空间调度到 node2 节点。

  • 需要进行哪些扩展

Filter、Score 这两个扩展点就够了。如果不清楚使用哪些扩展,可以直接看一下 Plugin 的接口定义,查看接口参数和返回。

Filter 需要禁止非指定的命名空间 Pod 调度到指定节点,放行指定命名空间的特殊 Pod 的调度。

Score 需要优先将指定命名空间的特殊 Pod 调度到指定节点。

4.3 写 main.go 及 plugin 相关代码

  • main
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func main() {
	rand.Seed(time.Now().UnixNano())
	command := app.NewSchedulerCommand(
		app.WithPlugin(image.Name, image.New),
	)
	if err := command.Execute(); err != nil {
		println(os.Stderr, "%v\n", err)
		os.Exit(1)
	}
}

main 使用 default-scheduler 的代码启动,然后注册自己的 plugin,可以注册多个。

  • 定义插件 ImageNode
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type ImageNode struct {
	handle framework.Handle
}

// var _ = framework.PreFilterPlugin(&ImageNode{})
var _ = framework.FilterPlugin(&ImageNode{})
var _ = framework.ScorePlugin(&ImageNode{})

const Name = "ImageNode"

func (i *ImageNode) Name() string {
	return Name
}

func New(_ runtime.Object, h framework.Handle) (framework.Plugin, error) {
	return &ImageNode{
		handle: h,
	}, nil
}

这个格式是固定的,var _ = framework.FilterPlugin(&ImageNode{}) 是为了 ImageNode 一定要实现 FilterPlugin 接口,否则编译不通过。这里需要实现 FilterPlugin 和 ScorePlugin 接口。

  • 实现 FilterPlugin 接口
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func (i *ImageNode) Filter(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
	if pod == nil {
		return framework.NewStatus(framework.Error, "pod is nil")
	}
	node := nodeInfo.Node()
	if node == nil {
		klog.Infof("node is nil")
		return framework.NewStatus(framework.Error, "node is nil")
	}
	nodes, nss, filterImage := isSpecialNS(i.handle.ClientSet(), pod.Namespace)

	if len(nodes) == 0 || len(nss) == 0 || len(filterImage) == 0 {
		klog.Infof("nodes, nss, filterImage is nil")
		return framework.NewStatus(framework.Success, "default")
	}

	workload := ""
	if len(pod.ObjectMeta.OwnerReferences) > 0 {
		workload = pod.ObjectMeta.OwnerReferences[0].Kind
	}
	if workload == "DaemonSet" {
		klog.Info("DaemonSet pass")
		return framework.NewStatus(framework.Success, "DaemonSet pass")
	}

	if isStringInList(node.Name, nodes) {
		if isStringInList(pod.Namespace, nss) && isSpecialImage(pod, filterImage) {
			klog.Infof("plugin hit")
			return framework.NewStatus(framework.Success, "plugin hit")
		} else {
			klog.Info(fmt.Printf("plugin disable pod %s special node %s", pod.Name, node.Name))
			return framework.NewStatus(framework.Unschedulable, "plugin disable special node")
		}
	}
	klog.Info("default pass")
	return framework.NewStatus(framework.Success, "default")
}

这里有一个特殊的逻辑,放行 DaemonSet 的 Pod,否则会导致 DaemonSet 的 Pod 无法在标记的节点上运行。

  • 实现 ScorePlugin 接口
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
func (i *ImageNode) Score(ctx context.Context, cycleState *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) {
	nodes, nss, filterImage := isSpecialNS(i.handle.ClientSet(), pod.Namespace)
	if len(nodes) > 0 && len(filterImage) > 0 && len(nss) > 0 {
		if isStringInList(nodeName, nodes) && isSpecialImage(pod, filterImage) {
			retScore := framework.MaxNodeScore - rand.Int63n(10)
			klog.Infof("special node score %d", retScore)
			return retScore, framework.NewStatus(framework.Success, "special node score")
		}
	}
	retScore := rand.Int63n(framework.MaxNodeScore - 50)
	klog.Infof("rand node score %d", retScore)
	return retScore, framework.NewStatus(framework.Success, "rand node score")
}

func (i *ImageNode) ScoreExtensions() framework.ScoreExtensions {
	return i
}

func (i *ImageNode) NormalizeScore(ctx context.Context, state *framework.CycleState, pod *v1.Pod, scores framework.NodeScoreList) *framework.Status {
	// Find highest and lowest scores.
	var highest int64 = -math.MaxInt64
	var lowest int64 = math.MaxInt64
	for _, nodeScore := range scores {
		if nodeScore.Score > highest {
			highest = nodeScore.Score
		}
		if nodeScore.Score < lowest {
			lowest = nodeScore.Score
		}
	}

	// Transform the highest to lowest score range to fit the framework's min to max node score range.
	oldRange := highest - lowest
	newRange := framework.MaxNodeScore - framework.MinNodeScore
	for i, nodeScore := range scores {
		if oldRange == 0 {
			scores[i].Score = framework.MinNodeScore
		} else {
			scores[i].Score = ((nodeScore.Score - lowest) * newRange / oldRange) + framework.MinNodeScore
		}
	}

	return nil
}

Score 就是给节点打分,分数高的优先被调度。这里的处理比较粗糙,因为 CICD 任务的 Req 都不高,default-scheduler 的打分本来就不准确,直接给了随机分。

为了避免 CDN 任务一直被调度到同一个标记的节点,引入了一定的波动,随机给节点减去一定分值。

4.4 调试和部署

这是新手动手时,最花时间的地方。阅读一两篇文档很容易理解,但是实际操作时,往往会遇到各种各样的问题。

  • 本地新建一个文件 scheduler-plugin.yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
clientConnection:
  kubeconfig: "/Users/shaowenchen/.kube/config"
profiles:
  - schedulerName: default-scheduler
    plugins:
      filter:
        enabled:
          - name: ImageNode
      score:
        enabled:
          - name: ImageNode

apiVersion 需要根据集群版本进行调整。kubeconfig 指向本地的 kubeconfig 文件。filter 和 score 都需要开启 ImageNode 插件。

  • 启动 kube-scheduler
1
2
3
4
5
6
7
8
go run main.go \
    --leader-elect=true \
    --feature-gates=RotateKubeletServerCertificate=true,TTLAfterFinished=true,ExpandCSIVolumes=true,CSIStorageCapacity=true \
    --authentication-kubeconfig=/Users/shaowenchen/.kube/config \
    --authorization-kubeconfig=/Users/shaowenchen/.kube/config \
    --kubeconfig=/Users/shaowenchen/.kube/config \
    --config=/Volumes/Data/Code/Github/demo/scheduler-plugin/scheduler-plugin.yaml \
    --v=5

启动参数,不同版本的集群可能不同,需要去集群上查看。加上 --v=5 能够看到更详细的日志。

  • 编译镜像

Dockerfile 如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
FROM golang:1.19 AS build

WORKDIR /go/src/kube-scheduler
COPY . .

RUN CGO_ENABLED=0 GOOS=linux go build -o kube-scheduler .

FROM alpine:3.14

COPY --from=build /go/src/kube-scheduler/kube-scheduler /usr/bin/kube-scheduler

CMD ["/usr/bin/kube-scheduler"]

执行编译命令

1
docker build -t shaowenchen/scheduler-plugin:latest .
  • 部署[每个 Master 节点]

新建文件 /etc/kubernetes/scheduler-plugin.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
clientConnection:
  kubeconfig: "/etc/kubernetes/scheduler.conf"
profiles:
  - schedulerName: default-scheduler
    plugins:
      filter:
        enabled:
          - name: ImageNode
      score:
        enabled:
          - name: ImageNode

此时的 kubeconfig 应该指向集群的 kubeconfig 文件。

编辑 /etc/kubernetes/manifests/kube-scheduler.yaml,添加如下内容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: Pod
metadata:
  name: kube-scheduler
  namespace: kube-system
spec:
  containers:
  - command:
    - kube-scheduler
    ...
    - --config=/etc/kubernetes/scheduler-plugin.yaml
    image: shaowenchen/scheduler-plugin:latest
    imagePullPolicy: Always
    volumeMounts:
    - mountPath: /etc/kubernetes/scheduler-plugin.yaml
      name: scheduler-plugin
      readOnly: true
  volumes:
  - hostPath:
      path: /etc/kubernetes/scheduler-plugin.yaml
      type: File
    name: scheduler-plugin

--config=/etc/kubernetes/scheduler-plugin.yaml 指定配置文件,volumeMounts、volumes 用于挂载配置文件。

需要注意的是,kube-scheduler 是一个静态 pod,需要编辑一下文件才能真正重启。使用 kubectl -n kube-system delete pod kube-scheduler-master1 仅能重启容器,不会使用最新的镜像。

5. 总结

本篇从 Kubernetes 集群调度的角度来优化基于 Tekton 的 CICD 系统,将指定的任务调度到指定的节点上,更好的利用现有的网络资源。

主要内容如下:

  • 鉴于业务诉求,我们准备使用多区域的节点进行构建,因此需要定制调度器
  • 调研了常见的四种调度器,最终选择了 scheduler framework 的扩展方式
  • 使用 sheduler framework 的方式,实现了一个简单的调度器插件
  • 本地调试和部署

但由此可定制的内容还有很多,比如:

  • 根据任务的优先级,选择不同的节点
  • 根据主机的负载,选择不同的节点
  • 根据任务的资源需求,选择不同的节点

这进一步扩展了 CICD 系统可以定制和优化的空间。

6. 参考

  1. https://duyanghao.github.io/scheduler-extend/
  2. https://www.infoq.cn/article/lYUw79lJH9bZv7HrgGH5
  3. https://github.com/shaowenchen/demo/tree/master/scheduler-plugin