hello云胜

技术与生活

0%

报错

使用kubesphere流水线过程中,遇到一个奇葩问题。偶尔会有几个任务执行失败。

报错如下:

1
2
3
4
+ mvn clean package -Dmaven.test.skip=true -Ptest
Error: missing `server' JVM at `/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.232.b09-0.el7_7.i386/jre/lib/i386/server/libjvm.so'.
Please install or use the JRE or JDK that contains these missing components.
script returned exit code 4

因为是同样的代码,偶尔发生,所以基本排除是代码问题。

在网上发现同样的报错,有文章说是服务器文件系统是xfs,并且磁盘超过1T的情况下,会触发操作系统bug。

经过复现,发现当流水线调度到某台机器确实会失败,查看这台机器配置如下

image-20240711145404837

centos 7.9.2009

Cenos 7文件系统默认使用XFS格式

当调度到的服务器满足以下条件。文件系统格式是xfs,且挂载点磁盘大小超过1T

会触发这个bug

解决

查看pod的yaml,发现已经配置了node亲和

1
2
3
4
5
6
7
8
9
10
11
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- preference:
matchExpressions:
- key: node-role.kubernetes.io/worker
operator: In
values:
- ci
weight: 1

找了几台磁盘小于1T的,打上标签

1
kubectl label node t-paas-k8s-0-node-5 node-role.kubernetes.io/worker=ci --overwrite

避坑

这是个坑啊,跑jenkins流水线的机器,不要用xfs文件格式。或者磁盘不要超过1T。

kubesphere 对接 企业OAuth2登录

亲身实践kubesphere 对接 企业OAuth2登录。踩了不少坑。网上也没有太多资料。

如果你也在开发这块功能,我相信这篇文章应该会提供一些帮助。

开发provider

我们需要开发一个自定义的认证provider,作用是去调用我们自己的OAuth服务。

因为每个公司自己的OAuth服务的接口定义和返回值可能存在些许的差异,所以必须编写这个适配的provider。

这一块的代码开发难度不大。因为官方已经给出了GitHub 的 GitHubIdentityProvider 和阿里云IDaaS的 AliyunIDaasProvider。

只要照葫芦画瓢即可。

具体步骤

  1. 创建目录

    pkg/apiserver/authentication/identityprovider/ 目录下创建一个插件的包。

  2. 主要就是实现几个接口。

​ 我是直接把GitHubIdentityProvider 的代码复制了过来,改成自己公司的名字。

​ ![image-20230804142445964](D:\github\docs\云原生\kubesphere\kubesphere 对接 企业OAuth2登录.assets\image-20230804142445964.png)

主要修改的地方就是这里的结构定义,需要根据你自己公司的auth服务来改。

![image-20230804142545972](D:\github\docs\云原生\kubesphere\kubesphere 对接 企业OAuth2登录.assets\image-20230804142545972.png)

这里根据实际情况来改。要说的是在username上我吃了个大亏。后面你会看到。

至于其他代码就不写了,基本不需要改动。

  1. 注册自己的provider

    在pkg/apiserver/authentication/options.go里import刚才编写的类

    1
    _ "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider/xxx"

提供OAuth服务的接口

我们需要3个OAuth相关的url地址

1
2
3
4
5
const (
authURL = "OAuth进行登录的页面"
tokenURL = "根据登录的页面返回的code,来获取access_token的接口"
userInfoURL = "根据access_token获取用户信息的接口"
)

另外,还需要进行OAuth认证的clientID和clientSecret。这个找你们公司服务OAuth服务的工程师要,肯定有的。

我在开发调试过程中发现公司提供的tokenURL和userInfoURL同kubesphere对接有点问题。

kubesphere发起tokenURL是用的post请求。参数名为code

然后userInfoURL是把access_token放在header里,名字为authorization

(一部血泪史)

所以又自己开发了这两个接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@PostMapping("/getToken")
public TokenResponse getToken(@RequestParam String code) {
log.info("============getToken============");
TokenResponse token = authSdkService.getToken(code);
return token;
}

@GetMapping("/getUserInfo")
public AccountResponse getUserInfo(HttpServletRequest request) {
String authorization = request.getHeader("authorization");
// authorization带着bear xxxxx +++++
String[] s = authorization.split(" ");
String token = s[1];
RestResponse<AccountResponse> accountResponseRestResponse = authSdkService.accountInfo(token);
AccountResponse data = accountResponseRestResponse.getData();
return data;
}

(用java写的。因为我们公司auth团队只提供java版本的sdk

这个代码和apiserver无关。)

打镜像并部署

开发完apiserver,就需要打自己的apiserver镜像,并部署到自己的k8s中。

步骤比较简单,但如果你是第一次打kubesphere的镜像,可能会遇到一些问题。

我这边记录下我遇到的问题

  1. 依赖的问题

    kubesphere使用vendor管理依赖。

需要执行

1
2
3
go mod tidy
go mod download
go mod vendor

这会将依赖拷贝到vendor目录下,否则编译go会找不到

  1. 无执行权限的问题

    1
    chmod +x hack/*.sh
  2. build镜像时需要传参

    以确定下载的helm的安装包

    TARGETARCH : amd64

    TARGETOS : linux

好了,然后在代码的根目录执行

1
2
3
docker build -f build/ks-apiserver/Dockerfile -t harbor-test.xxx.net/kubesphere/ks-apiserver:1.0.0 --build-arg TARGETOS=linux --build-arg TARGETARCH=amd64 .

docker push harbor-test.xxx.net/kubesphere/ks-apiserver:1.0.0

部署到集群中。修改ks-apiserver

1
kubectl -n kubesphere-system edit deploy ks-apiserver

如果不出意外,到现在我们完成了apiserver的部署。

配置kubesphere

修改ks-install

1
kubectl -n kubesphere-system edit cc ks-installer

对authentication进行修改

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
spec:
authentication:
jwtSecret: ''
authenticateRateLimiterMaxTries: 10
authenticateRateLimiterDuration: 10m0s
oauthOptions:
accessTokenMaxAge: 1h
accessTokenInactivityTimeout: 30m
identityProviders:
- name: zzzz
type: xxxIDaaSProvider
mappingMethod: auto
provider:
clientID: 'f7d8ba1339b07988xxxxxx6'
clientSecret: '0ae8e4f66358cd8xxxxx97726968'
redirectURL: http://xxxx/oauth/redirect/zzzz
endpoint:
tokenURL: http://xxxx/auth/getToken
authURL: https://iam.xxxx.net/
userInfoURL: http://xxxx/auth/getUserInfo

这里必须要说的是redirectURL。这里我一开始是不知道填什么的,也没有文档告知。

后来看github和ali的那个测试代码

![image-20230804145554881](D:\github\docs\云原生\kubesphere\kubesphere 对接 企业OAuth2登录.assets\image-20230804145554881.png)

发现他们的跳转地址写的都是ks-console的地址加上/oauth/redirect/后面接上自己provider的名字。

这才猜测应该这么写。着实坑人。

还有type就是写你的provider的类类型。

后来看了ks-console的代码,才确认是这么写没错。

给你们看看

![image-20230804145831136](D:\github\docs\云原生\kubesphere\kubesphere 对接 企业OAuth2登录.assets\image-20230804145831136.png)

/oauth/redirect之后会调到ks-apiserver的/oauth/callback接口

![image-20230804145921044](D:\github\docs\云原生\kubesphere\kubesphere 对接 企业OAuth2登录.assets\image-20230804145921044.png)

调到ks-apiserver的/oauth/callback接口又在哪里呢?

pkg/kapis/oauth/register.go 这里是注册router的地方

![image-20230804150102310](D:\github\docs\云原生\kubesphere\kubesphere 对接 企业OAuth2登录.assets\image-20230804150102310.png)

而这个oauthCallback,经过调用最终会到我们在第一步编写的provider里

![image-20230804150218639](D:\github\docs\云原生\kubesphere\kubesphere 对接 企业OAuth2登录.assets\image-20230804150218639.png)

![image-20230804150248944](D:\github\docs\云原生\kubesphere\kubesphere 对接 企业OAuth2登录.assets\image-20230804150248944.png)

所以,经过这一遭,你应该明白整个跳转流程了吧。

好,我来总结一下

  1. 经过这一番改造。打开kubesphere,会显示通过xxx登录的按钮

![image-20230804150434512](D:\github\docs\云原生\kubesphere\kubesphere 对接 企业OAuth2登录.assets\image-20230804150434512.png)

  1. 点击之后,就会跳到对应的认证页面

    根据的是authURL

    以github为例

    ![github-login-page](D:\github\docs\云原生\kubesphere\kubesphere 对接 企业OAuth2登录.assets\github-login-page.png)

  2. 在这里登录之后

​ 根据auth2的交互流程。会返回一个授权码code。

  1. 登录成功就会进行跳转

    跳转的地址就是redirectURL

  2. 认证

    kubesphere(以后简称ks)这个跳转接口会进行回调

    在回调里触发我们编写的provider

​ 根据tokenURL + code获取了access_token

​ 再使用userInfoURL和access_token获取userInfo

ok,到此为止。成功完成auth登录。

参考资料: OAuth 2.0身份提供者 (kubesphere.io)

fluent-bit-core 现大量僵尸进程

现象

使用top命令可以看到红框的74个僵尸进程。

![image-20230912093342597](D:\github\docs\云原生\kubesphere\fluent-bit-core 现大量僵尸进程.assets\image-20230912093342597.png)

处理

想要看详细的都有哪些僵尸进程,方法也比较多

  1. top 按状态S排序

top 后按 f键进到过滤面板。 选到S 再按s键确认 按这个排序。然后退出过滤面板

![image-20230912093647635](D:\github\docs\云原生\kubesphere\fluent-bit-core 现大量僵尸进程.assets\image-20230912093647635.png)

就可以看到僵尸进程都排到前面了

![image-20230912093808385](D:\github\docs\云原生\kubesphere\fluent-bit-core 现大量僵尸进程.assets\image-20230912093808385.png)

  1. ps grep Z状态的进程
1
ps -A -ostat,ppid,pid,cmd | grep -e '^[Zz]'

-A 参数列出所有进程

-o 自定义输出字段 我们设定显示字段为 stat(状态), ppid(进程父id), pid(进程id),cmd(命令)这四个参数

因为状态为 z或者Z的进程为僵尸进程,所以我们使用grep抓取stat状态为zZ进程

![image-20230911180444081](D:\github\docs\云原生\kubesphere\fluent-bit-core 现大量僵尸进程.assets\image-20230911180444081.png)

3, ps grep defunct关键字

1
ps -ef | grep defunct

因为僵尸进程的关键字是defunct

父进程是200441这个

![image-20230911180543277](D:\github\docs\云原生\kubesphere\fluent-bit-core 现大量僵尸进程.assets\image-20230911180543277.png)

就是fluent-bit进程

杀死僵尸进程的方法就是,杀死其父进程

所以,一定要确认从业务层面上,父进程是可以杀死重启的。

我这个是k8s集群以Daemonset 运行的fluent-bit

所以我可以果断重启

杀死父进程

1
for i in `kubectl -n kubesphere-logging-system get pod  | grep fluent-bit | awk '{print $1}'`;do kubectl -n kubesphere-logging-system delete pod $i; done;

部署rook并创建ceph集群

准备软件包

1
2
3
wget https://github.com/rook/rook/archive/v1.4.4.tar.gz
tar -zxvf v1.4.4.tar.gz
cd rook-1.4.4/cluster/examples/kubernetes/ceph

修改operator.yaml

rook需要在master主机上部署node,master默认被打上了NoSchedule的taint,需要在operator上面添加容忍

1
2
3
4
5
6
7
8
[root@d-paas-k8s-master-0 ceph-image]# vi operator.yaml
CSI_PLUGIN_TOLERATIONS: |
- effect: NoSchedule
key: node-role.kubernetes.io/master
operator: Exists
- effect: NoExecute
key: node-role.kubernetes.io/etcd
operator: Exists

加载ceph-csi image

部署ceph的cluster时会部署ceph-csi,csi的docker image都是放在quay.io仓库下的。在我实际部署中发现,我们的网络环境即使配置了docker镜像加速也访问不了quay.io下的image,导致image pulling失败,csi pods启动不了。

所以我事先将ceph-csi所需的image打包成tgz,放在跳板机 /home/api/ceph-image下

部署rook之前,先将如下的image都scp到集群内所有的主机上

1
2
3
4
5
6
7
8
9
10
[api@kfxqtyglpt ceph-image]$ pwd
/home/api/ceph-image
[api@kfxqtyglpt ceph-image]$ ll
总用量 1230580
-rwxr-xr-x. 1 api api 1049964544 11月 11 13:48 cephcsi.tgz
-rwxr-xr-x. 1 api api 47385088 11月 11 13:49 csi-attacher.tgz
-rwxr-xr-x. 1 api api 18313728 11月 11 13:49 csi-node-driver-register.tgz
-rwxr-xr-x. 1 api api 49535488 11月 11 13:49 csi-provisioner.tgz
-rwxr-xr-x. 1 api api 47319040 11月 11 13:49 csi-resizer.tgz
-rwxr-xr-x. 1 api api 47581696 11月 11 13:49 csi-snapshotter.tgz

在每台主机上执行

1
2
[root@d-paas-k8s-master-0 ~]# for i in `ls *.tgz`;do docker load -i $i;done
[root@d-paas-k8s-master-0 ~]# yum install lvm2.x86_64 -y

部署rook operator

1
2
3
4
5
[root@d-paas-k8s-master-0 ~]# cd /root/rook-1.4.4/cluster/examples/kubernetes/ceph
[root@d-paas-k8s-master-0 ceph]# kubectl create -f common.yaml
[root@d-paas-k8s-master-0 ceph]# kubectl create -f operator.yaml
#等待rook-ceph下的pod都处于running状态
[root@d-paas-k8s-master-0 ceph]# kubectl get pods -n rook-ceph

部署ceph cluster

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
[root@d-paas-k8s-master-0 ceph]# kubectl create -f cluster.yaml

#等待rook-ceph下的pod都处于running或者completed状态
[root@d-paas-k8s-master-0 ceph]# kubectl get pods -n rook-ceph
NAME READY STATUS RESTARTS AGE
csi-cephfsplugin-4j7tt 3/3 Running 0 19h
csi-cephfsplugin-l99bm 3/3 Running 0 19h
csi-cephfsplugin-n5xvw 3/3 Running 0 19h
csi-cephfsplugin-provisioner-598854d87f-n5ltz 6/6 Running 0 19h
csi-cephfsplugin-provisioner-598854d87f-vnjbq 6/6 Running 0 19h
csi-cephfsplugin-psmfq 3/3 Running 0 19h
csi-rbdplugin-2zt76 3/3 Running 0 19h
csi-rbdplugin-9jdwx 3/3 Running 0 19h
csi-rbdplugin-ddzpk 3/3 Running 0 19h
csi-rbdplugin-provisioner-dbc67ffdc-8xsbd 6/6 Running 0 19h
csi-rbdplugin-provisioner-dbc67ffdc-jspd9 6/6 Running 0 19h
csi-rbdplugin-wmqvm 3/3 Running 0 19h
rook-ceph-crashcollector-d-paas-k8s-0-node-0-7b696f9f8d-zqb49 1/1 Running 0 19h
rook-ceph-crashcollector-d-paas-k8s-0-node-1-645b49b659-gcrjz 1/1 Running 0 19h
rook-ceph-crashcollector-d-paas-k8s-0-node-2-dbb5978b6-6pwhv 1/1 Running 0 19h
rook-ceph-mgr-a-5977cf7cd7-dlmnj 1/1 Running 0 19h
rook-ceph-mon-a-6cfc9f64cc-k8vdp 1/1 Running 0 19h
rook-ceph-mon-b-574d74f4c9-lgl76 1/1 Running 0 19h
rook-ceph-mon-c-fd6fcb588-rtzfz 1/1 Running 0 19h
rook-ceph-operator-667756ddb6-rjr9v 1/1 Running 0 19h
rook-ceph-osd-0-95dd775b6-757w5 1/1 Running 0 19h
rook-ceph-osd-1-69c45949b5-8fphv 1/1 Running 0 19h
rook-ceph-osd-2-847cb97d55-n87d4 1/1 Running 0 19h
rook-ceph-osd-3-78b76c9475-2qsgw 1/1 Running 0 19h
rook-ceph-osd-4-55c4cb85d8-zqvlq 1/1 Running 0 19h
rook-ceph-osd-5-576db964d8-bwml7 1/1 Running 0 19h
rook-ceph-osd-prepare-d-paas-k8s-0-node-0-ndr7g 0/1 Completed 0 21m
rook-ceph-osd-prepare-d-paas-k8s-0-node-1-nfmjm 0/1 Completed 0 21m
rook-ceph-osd-prepare-d-paas-k8s-0-node-2-xt9hf 0/1 Completed 0 21m
rook-ceph-tools-7cc7fd5755-c44p9 1/1 Running 0 19h
rook-discover-ktjcr 1/1 Running 0 19h
rook-discover-mhcv7 1/1 Running 0 19h
rook-discover-xppd6 1/1 Running 0 19h


#检查ceph状态
[root@d-paas-k8s-master-0 ceph]# kubectl create -f toolbox.yaml

#进入toolbox pod
[root@d-paas-k8s-master-0 ceph]# kubectl -n rook-ceph exec -it $(kubectl -n rook-ceph get pod -l "app=rook-ceph-tools" -o jsonpath='{.items[0].metadata.name}') -- bash

#检查ceph集群状态,osd数量,osd状态
[root@rook-ceph-tools-7cc7fd5755-c44p9 /]# ceph status
cluster:
id: b56328d7-2256-4faa-a6f0-f8a684e1ab70
health: HEALTH_OK

services:
mon: 3 daemons, quorum a,b,c (age 19h)
mgr: a(active, since 24m)
osd: 6 osds: 6 up (since 19h), 6 in (since 19h) #集群内总共有几块裸盘就应该有几个osd,本例三台node每台两块裸盘,总共6个裸盘,所以有6块osd

data:
pools: 2 pools, 33 pgs
objects: 1.66k objects, 4.8 GiB
usage: 20 GiB used, 6.5 TiB / 6.5 TiB avail
pgs: 33 active+clean

io:
client: 1.3 MiB/s wr, 0 op/s rd, 83 op/s wr

#退出rook-ceph-tool容器
[root@rook-ceph-tools-7cc7fd5755-c44p9 /]# exit

创建storage class

1
2
3
[root@d-paas-k8s-master-0 rbd]# kubectl apply -f /root/rook-1.4.4/cluster/examples/kubernetes/ceph/csi/rbd/storageclass.yaml
#设置为默认storageclass
[root@d-paas-k8s-master-0 rbd]# kubectl patch storageclass rook-ceph-block -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

本例中使用的storageclass是rbd也就是块存储,ceph还提供了文件存储的storage-class,/root/rook-1.4.4/cluster/examples/kubernetes/ceph/csi/cephfs/storageclass.yaml

rbd相较于cephfs性能要更优,但是不支持多mount,也就是每个pvc只能被一个pod挂载。

cephfs在读写大文件时性能比较优秀,读写小文件时性能较差,但是其支持多mount,所以在需要多个pod共享存储时就需要选用cephfs作为storage-class

创建pvc 验证csi

1
2
3
4
5
6
7
8
9
[root@d-paas-k8s-master-0 rbd]# kubectl apply -f /root/rook-1.4.4/cluster/examples/kubernetes/ceph/csi/rbd/pvc.yaml
persistentvolumeclaim/rbd-pvc created
# 查看在default namespace下是否生成了name为rbd-pvc的pvc,status为bound
[root@d-paas-k8s-master-0 rbd]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
rbd-pvc Bound pvc-5f1b5ce4-9d5b-40fb-b08d-5d37de576ea2 1Gi RWO rook-ceph-block 4s

# 验证完成删除rdb-pvc
[root@d-paas-k8s-master-0 rbd]# kubectl delete pvc rbd-pvc

遇到的坑

部署完rook和ceph cluster后,尝试创建一个pvc,发现pvc状态一直时pending状态,describe pvc 提示等待csi创建pv。检查rook-ceph下的pod,发现并没有创建csi pod (csi-xxxx)。csi-pod是创建ceph cluster时,operater创建的,所以检查rook-operator log

1
2
3
4
[root@d-paas-k8s-master-0 ~]# kubectl logs rook-ceph-operator-667756ddb6-rjr9v -nrook-ceph
##发现有错误如下所示
E | ceph-csi: invalid csi version. failed to run CmdReporter rook-ceph-csi-detect-version successfully. failed to delete existing results ConfigMap rook-ceph-csi-detect-version. failed to delete ConfigMap rook-ceph-csi-detect-version. etcdserver: request timed out
failed to complete ceph CSI version job

原因时operater在部署csi时会先启动一个rook-ceph-csi-detect-version 的job去和etcd做交互,具体交互什么内容目前还不清楚。但是需要这个job完成后才会去创建csi。这个job使用的image是上文提到的quay.io下的所以image无法拉下来,导致job长时间无法完成,最终超时。operator不会去创建csi。

这个问题解决方法就是安照本节开始的方法,提前把csi使用到的image都load到每台机器本地。

由于rook-ceph-csi-detect-version是和etcd做交互,那么这两方任意一个有问题都会导致csi无法创建。所以如果etcd在这期间出现了不稳定的状态,如leader重新选举导致无法提供服务,也会影响csi的创建。见https://github.com/rook/rook/issues/6291

如果node节点没有挂载裸盘,会有如下pod运行失败,影响后续storage class创建:

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
NAME                                                   READY   STATUS             RESTARTS   AGE
csi-cephfsplugin-6865x 3/3 Running 0 6m53s
csi-cephfsplugin-7hdg7 3/3 Running 0 6m53s
csi-cephfsplugin-pcmnh 3/3 Running 0 6m53s
csi-cephfsplugin-provisioner-598854d87f-5h79c 6/6 Running 0 6m53s
csi-cephfsplugin-provisioner-598854d87f-9r6z2 6/6 Running 0 6m53s
csi-rbdplugin-9fkkp 3/3 Running 0 6m54s
csi-rbdplugin-fjc86 3/3 Running 0 6m54s
csi-rbdplugin-provisioner-dbc67ffdc-qsvs5 6/6 Running 0 6m54s
csi-rbdplugin-provisioner-dbc67ffdc-zrthq 6/6 Running 0 6m54s
csi-rbdplugin-x4gl4 3/3 Running 0 6m54s
rook-ceph-crashcollector-t-docker02-659696b779-sjjtf 1/1 Running 0 5m55s
rook-ceph-crashcollector-t-docker03-5856b9458-4rrm2 1/1 Running 0 5m5s
rook-ceph-crashcollector-t-docker04-ff475547f-6mrvr 1/1 Running 0 4m27s
rook-ceph-mgr-a-74d7d89b9-2bz72 1/1 Running 0 4m27s
rook-ceph-mon-a-9d56b548-zgfqv 1/1 Running 0 5m55s
rook-ceph-mon-b-d5f999ffb-64bs5 1/1 Running 0 5m49s
rook-ceph-mon-c-56856c4cff-knb8g 1/1 Running 0 5m5s
rook-ceph-operator-667756ddb6-nhbkk 1/1 Running 0 12m
rook-ceph-osd-prepare-t-docker02-5rpfp 0/1 CrashLoopBackOff 5 4m26s
rook-ceph-osd-prepare-t-docker03-kd5jt 0/1 CrashLoopBackOff 5 4m26s
rook-ceph-osd-prepare-t-docker04-qg9kx 0/1 CrashLoopBackOff 5 4m25s
rook-discover-kwwgq 1/1 Running 0 12m
rook-discover-m4fb2 1/1 Running 0 12m
rook-discover-rnpc5 1/1 Running 0 12m

如果osd创建失败,可以考虑重装rook或者重新挂在裸盘,清理请参考如下链接: https://rook.io/docs/rook/v1.4/ceph-teardown.html

读书:深入刨析Kubernetes(一)

Docker解决的核心问题是应用的打包。

容器本身没有价值,有价值的是容器编排

1
我的想法:不认可作者说的容器本身没有价值。容器标准化了应用的包。才有了后面容器编排调度的发展

Docker的底层原理

Docker的底层原理是利用了linux的Cgroups和Namespace技术。cgroups是用来制造约束的主要手段,而namespace技术是用来修改进程视图(隔离)的主要方法。

namespace

linux中船舰一个新的进程:

1
int pid = clone(main_function, stack_size, SIGCHLD, NULL);

在创建时可以传一个参数CLONE_NEWPID

1
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);

这时,新创建的这个进程将会“看到”一个全新的进程空间,在这个进程空间里,它的 PID 是 1。

docker这个听起来很玄的技术,实际上就是在创建一个容器进程时,指定了这个进程所需要启用的一组Namespace参数。这样容器就只能看到当前namespace限定的资源、文件、设备、网络等等资源。而对于宿主机以及其他不相干的程序完全看不到。

总之,容器只是一个特殊一点的进程而已。

image-20220121170136227

进一步理解,docker容器不是一个虚拟机,并没有一个所谓的docker容器运行在宿主机上,用户进程还是那个用户进程,只不过docker帮我们加上了各种namespace参数。Docker 项目在这里扮演的角 色,更多的是旁路式的辅助和管理工作。

也可以看到docker和虚拟机相比的区别,虚拟化需要一个Hypervisor 来负责创建虚拟机,这个虚拟机是真实存在的,并且他里面真的要运行一个操作系统。而容器本质上仍然仅仅是宿主机操作系统上的一个进程而已。

namespace参数

为了隔离不同类型的资源,Linux 内核里面实现了以下几种不同类型的 namespace。

UTS,对应的宏为 CLONE_NEWUTS,表示不同的 namespace 可以配置不同的 hostname。

User,对应的宏为 CLONE_NEWUSER,表示不同的 namespace 可以配置不同的用户 和组。

Mount,对应的宏为 CLONE_NEWNS,表示不同的 namespace 的文件系统挂载点是 隔离的

PID,对应的宏为 CLONE_NEWPID,表示不同的 namespace 有完全独立的 pid,也即 一个 namespace 的进程和另一个 namespace 的进程,pid 可以是一样的,但是代表不 同的进程。

Network,对应的宏为 CLONE_NEWNET,表示不同的 namespace 有独立的网络协议 栈

后台启动一个busybox容器

1
2
3
4
5
6
7
8
# docker run -it -d busybox
68f11c135bcdf9d5f793a6f12e90e37b5bca07735b9d715c2a02a163d3715c77
# docker exec -it 68f11c135b /bin/sh
# ps
PID USER TIME COMMAND
1 root 0:00 sh
6 root 0:00 /bin/sh
11 root 0:00 ps

进入容器查看ps,看到当前主进程号是1。隔离。

使用docker inpect查看下在宿主机上真正的PID。 “Pid”: 32694

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# docker inspect 68f11c135b
···
"State": {
"Status": "running",
"Running": true,
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": 32694,
"ExitCode": 0,
"Error": "",
"StartedAt": "2022-01-21T08:48:45.55505132Z",
"FinishedAt": "0001-01-01T00:00:00Z"
},
···

在宿主机上查看

1
2
3
# ps -ef | grep 32694
root 28685 2366 0 16:52 pts/0 00:00:00 grep --color=auto 32694
root 32694 32675 0 16:48 pts/0 00:00:00 sh

查看32694进程的namespace资源

1
2
3
4
5
6
7
8
# ls -l /proc/32694/ns
总用量 0
lrwxrwxrwx 1 root root 0 1月 21 16:49 ipc -> ipc:[4026534209]
lrwxrwxrwx 1 root root 0 1月 21 16:49 mnt -> mnt:[4026534207]
lrwxrwxrwx 1 root root 0 1月 21 16:48 net -> net:[4026534212]
lrwxrwxrwx 1 root root 0 1月 21 16:49 pid -> pid:[4026534210]
lrwxrwxrwx 1 root root 0 1月 21 16:53 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 1月 21 16:49 uts -> uts:[4026534208]

Cgroup

Cgroup的全称是Control Group,作用是限制一个进程组能够使用的资源上限,比如CPU,内存,磁盘,网络带宽等

linux的实现方式是在一个特定的目录下有特定的配置文件。/sys/fs/cgroup/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# ls -l /sys/fs/cgroup/
总用量 0
drwxr-xr-x 5 root root 0 7月 7 2021 blkio
lrwxrwxrwx 1 root root 11 7月 7 2021 cpu -> cpu,cpuacct
lrwxrwxrwx 1 root root 11 7月 7 2021 cpuacct -> cpu,cpuacct
drwxr-xr-x 5 root root 0 7月 7 2021 cpu,cpuacct
drwxr-xr-x 4 root root 0 7月 7 2021 cpuset
drwxr-xr-x 5 root root 0 7月 7 2021 devices
drwxr-xr-x 4 root root 0 7月 7 2021 freezer
drwxr-xr-x 4 root root 0 7月 7 2021 hugetlb
drwxr-xr-x 5 root root 0 7月 7 2021 memory
lrwxrwxrwx 1 root root 16 7月 7 2021 net_cls -> net_cls,net_prio
drwxr-xr-x 4 root root 0 7月 7 2021 net_cls,net_prio
lrwxrwxrwx 1 root root 16 7月 7 2021 net_prio -> net_cls,net_prio
drwxr-xr-x 4 root root 0 7月 7 2021 perf_event
drwxr-xr-x 5 root root 0 7月 7 2021 pids
drwxr-xr-x 6 root root 0 7月 7 2021 systemd

这一个个的文件夹就是cgroup可以限制的资源类型。比如cpu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ls -l /sys/fs/cgroup/cpu/
总用量 0
-rw-r--r-- 1 root root 0 7月 7 2021 cgroup.clone_children
--w--w--w- 1 root root 0 7月 7 2021 cgroup.event_control
-rw-r--r-- 1 root root 0 7月 7 2021 cgroup.procs
-r--r--r-- 1 root root 0 7月 7 2021 cgroup.sane_behavior
-r--r--r-- 1 root root 0 7月 7 2021 cpuacct.stat
-rw-r--r-- 1 root root 0 7月 7 2021 cpuacct.usage
-r--r--r-- 1 root root 0 7月 7 2021 cpuacct.usage_percpu
-rw-r--r-- 1 root root 0 7月 7 2021 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 7月 7 2021 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 7月 7 2021 cpu.rt_period_us
-rw-r--r-- 1 root root 0 7月 7 2021 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 7月 7 2021 cpu.shares
-r--r--r-- 1 root root 0 7月 7 2021 cpu.stat
drwxr-xr-x 5 root root 0 1月 21 16:46 docker
drwxr-xr-x 5 root root 0 7月 15 2021 kubepods
-rw-r--r-- 1 root root 0 7月 7 2021 notify_on_release
-rw-r--r-- 1 root root 0 7月 7 2021 release_agent
drwxr-xr-x 218 root root 0 1月 21 16:46 system.slice
-rw-r--r-- 1 root root 0 7月 7 2021 tasks

比如cfs_period_us和cfs_quota_us参数就是,限制进程在长度为 cfs_period 的一段时间内,只 能被分配到总量为 cfs_quota 的 CPU 时间。

image-20220121171625696

注意圈出的docker文件夹。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ls -l /sys/fs/cgroup/cpu/docker
总用量 0
drwxr-xr-x 2 root root 0 1月 21 16:41 51868296b5adcf5fa6bd0e85c72fc004ba8abf676159c6a74f06e276b7c229dd
drwxr-xr-x 2 root root 0 1月 21 16:48 68f11c135bcdf9d5f793a6f12e90e37b5bca07735b9d715c2a02a163d3715c77
drwxr-xr-x 2 root root 0 10月 12 13:26 afcc1b255416ebf7b3303904e5aee41afd281073fe00d5eb065dd9f73e31269b
-rw-r--r-- 1 root root 0 7月 19 2021 cgroup.clone_children
--w--w--w- 1 root root 0 7月 19 2021 cgroup.event_control
-rw-r--r-- 1 root root 0 7月 19 2021 cgroup.procs
-r--r--r-- 1 root root 0 7月 19 2021 cpuacct.stat
-rw-r--r-- 1 root root 0 7月 19 2021 cpuacct.usage
-r--r--r-- 1 root root 0 7月 19 2021 cpuacct.usage_percpu
-rw-r--r-- 1 root root 0 7月 19 2021 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 7月 19 2021 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 7月 19 2021 cpu.rt_period_us
-rw-r--r-- 1 root root 0 7月 19 2021 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 7月 19 2021 cpu.shares
-r--r--r-- 1 root root 0 7月 19 2021 cpu.stat
-rw-r--r-- 1 root root 0 7月 19 2021 notify_on_release
-rw-r--r-- 1 root root 0 7月 19 2021 tasks

还记得我们之前建的busybox容器吗?68f11c135bcdf9d5f793a6f12e90e37b5bca07735b9d715c2a02a163d3715c77

对这个容器的资源限制就在这里

1
2
# cat cpu.cfs_quota_us
-1

创建的时候我们没有限制资源,所以这里显式是-1,即不限制。

我们再运行一个Ubuntu容器

1
2
# docker run -it -d  --cpu-period=100000 --cpu-quota=20000 busybox
cf367a47f6cd9766effe154e9d725a21657663a3d14f731077ea7e09153cc35a

period=100000 配合 cpu-quota=20000的含义是:在每 100 ms 的时间里,被该控制组 限制的进程只能使用 20 ms 的 CPU 时间,即限制只能使用20%的cpu算力。

再去查看period和quota

1
2
3
4
# cat cpu.cfs_period_us
100000
# cat cpu.cfs_quota_us
20000

容器是一个单进程模型。一个容器的本质是一个进程。用户的应用进程实际上就是容器里 PID=1 的进程,也是其他后续创建的所有进程的父进程。

这就意味着,在一个容器中,你没办法同时运行两个不同的应 用。这是因为容器本身的设计,就是希望容器和应用能够同生命周期,这个概念对后续的容器编排非常重要。否则,一旦出现类似于“容器是正常运行的,但是里面的应用早已经挂了”的情况,编排系统处理起来就非常麻烦了

对 Docker 项目来说,它最核心的原理实际上就是为待创建的用户进程:

  1. 启用 Linux Namespace 配置;

  2. 设置指定的 Cgroups 参数;

  3. 切换进程的根目录(Change Root)。

需要明确的是,rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。所以说,rootfs 只包括了操作系统的“躯壳”,并没有包括操作系统的“灵魂”。**实际上,同一台机器上的所有容器,都共享宿主机操作系统的内核。**这就意味着,如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互,你就需要注意了:这些操作和依赖的对象,都是宿主机操作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身

Kubernetes架构

image-20210811154142473

两类节点:master和node

master节点:负责 API 服务的 kube-apiserver、负责调度的 kube-scheduler,以及负责容器编排的 kube-controller-manager。整个集群的持久化数据,则由 kube-apiserver 处理后保存在 Ectd 中。

node节点:最核心的是kubelet组件。kubelet主要负责同容器运行时交互,例如docker。这个交互是通过cri标准接口进行的。所以容器运行时不一定非要用docker,只要遵循cri接口就可以接入k8s。而容器运行时又通过oci同底层的linux操作系统进行交互。也就是把cri的请求翻译成对linux系统的调用。

device plugin插件时k8s用来管理gpu等宿主机物理设备的组件,所以基于k8s进行机器学习等要关注这个组件。

image-20210811172904096

Service 服务声明的 IP 地址等信息是“终生不变”的。这个Service 服务的主要作用,就是作为 Pod 的代理入口(Portal),从而代替 Pod 对外暴露一个固定的网络地址。Service 后端真正代理的 Pod 的 IP 地址、端口等信息的自动更新、维护,则是 Kubernetes 项目的职责。

master组件的部署

kubelet是在服务器上直接部署的,其他的组件在容器中启动。通过yaml部署的。

master组件的yaml放在 /etc/kubernetes/manifests

image-20210812093719676

这些 YAML 文件出现在被 kubelet 监视的 /etc/kubernetes/manifests 目录下,kubelet 就会自动创建这些 YAML 文件中定义的 Pod,即 Master 组件的容器

pod实现原理

首先,关于 Pod 最重要的一个事实是:它只是一个逻辑概念。Kubernetes 真正处理的,还是宿主机操作系统上 Linux 容器Namespace 和Cgroups,而并不存在一个所谓的 Pod 的边界或者隔离环境。

Pod,其实是一组共享了某些资源的容器。

具体的说:Pod 里的所有容器,共享的是同一个 Network Namespace,并且可以声明共享同一个Volume

也就是说比如pod里有容器A和B,可以使用A join进B的network 和volumn的方式实现,但是这样A和B就不是对等关系了

所以,在 Kubernetes 项目里,Pod 的实现需要使用一个中间容器,这个容器叫作 Infra 容器。在这个 Pod 中,Infra 容器永远都是第一个被创建的容器,而其他用户定义的容器,则通过 Join Network Namespace 的方式,与 Infra 容器关联在一起。

image-20210812142046890

Infra 容器一定要占用极少的资源,所以它使用的是一个非常特殊的镜像,叫作:k8s.gcr.io/pause。这个镜像是一个用汇编语言编写的、永远处于“暂停”状态的容器,解压后的大小也只有 100~200 KB 左右。

在 Infra 容器“Hold 住”Network Namespace 后,用户容器就可以加入到 Infra 容器的Network Namespace 当中了。

所以Pod 的生命周期只跟 Infra 容器一致,而与容器 A 和 B 无关

编排

控制器模式

编排的过程,

  1. 比如Deployment控制器从etcd中获取带某个标签的pod的数量,这就是实际状态
  2. 和yaml中的replicas字段就是期望状态
  3. 根据期望状态和实际状态的比较,确定下一步的动作

被控制对象的定义,则来自于一个模板。比如yaml中的template,称为pod模板

image-20210813105711300

Deployment 控制器实际操纵的,是 ReplicaSet 对象,而不是 Pod 对象。

image-20210813110718323

在用户提交了一个 Deployment 对象后,Deployment Controller 就会立即创建一个 Pod 副本个数为 3 的 ReplicaSet。这个 ReplicaSet 的名字,则是由 Deployment 的名字和一个随机字符串共同组成 `

滚动升级的流程

image-20210813134916854

滚动升级时,是新起一个rs,然后起pod,停掉旧rs的pod。最后停旧rs

kubectl rollout undo 命令,就能把整个 Deployment 回滚到上一个版本

StatefulSet

有状态的状态分类:拓扑状态(如主从),存储状态

拓扑状态

service是如何被访问的

第一种方式:通过service的vip

第二种方式:通过service的dns。又分两种处理方式,

  • normal service。访问服务域名dns解析到的就是这个服务的vip。后面的和第一种方式流程一样。
  • headless service。这种访问服务域名解析到的直接就是某一个pod的ip。headless service不需要分配一个vip,直接dns解析出具体的pod的ip

headless service

在yaml文件中,headless service的定义是通过clusterIP:none。这样定义出的service就是headless service。没有vip这个头。

当你按照这样的方式创建了一个 Headless Service 之后,它所代理的所有 Pod 的 IP 地址,都会被绑定一个这样格式的 DNS 记录,如下所示:

...svc.cluster.local

这个 DNS 记录,正是 Kubernetes 项目为 Pod 分配的唯一的“可解析身份”

有了这个“可解析身份”,只要你知道了一个 Pod 的名字,以及它对应的 Service 的名字,你就可以非常确定地通过这条 DNS 记录访问到 Pod 的 IP 地址。

StatefulSet 正是使用这个 DNS 记录来维持 Pod 的拓扑状态

StatefulSet 给它所管理的所有 Pod 的名字,进行了编号,编号规则是-数字

更重要的是,这些 Pod 的创建,也是严格按照编号顺序进行的。

通过这种严格的对应规则,StatefulSet 就保证了 Pod 网络标识的稳定性。

存储状态

PVC&PV

PVC由开发人员根据自己的需求定义

PV由运维人员提前维护

K8S会给pvc绑定pv

类似一种接口和实现的关系。pvc是接口,pv是具体的实现。

DaemonSet

DaemonSet类型pod的特征

  1. 这个 Pod 运行在 Kubernetes 集群里的每一个节点(Node)上;

  2. 每个节点上只有一个这样的 Pod 实例;

  3. 当有新的节点加入 Kubernetes 集群后,该 Pod 会自动地在新节点上被创建出来;而当旧节点被删除后,它上面的 Pod 也相应地会被回收掉。

场景:网络插件的agent组件,存储插件的agent组件,监控组件,日志组件

工作原理

DaemonSet Controller,首先从 Etcd 里获取所有的 Node 列表,然后遍历所有的 Node。检查node上是否有一个这个DaemonSet pod。这就是典型的控制器模型

对于创建来说,需要保证在指定的node上创建pod, DaemonSet Controller 会在创建 Pod 的时候,自动在这个 Pod 的 API 对象里,加上一个 nodeAffinity 定义。其中,需要绑定的节点名字,正是当前正在遍历的这个Node

当然,DaemonSet 并不需要修改用户提交的 YAML 文件里的 Pod 模板,而是在向Kubernetes 发起请求之前,直接修改根据模板生成的 Pod 对象。

出现这个问题的原因我还不清楚

操作是在ks-installer的cc中添加了一个错误的配置。之后发现ks-installer和ks-apiserver的pod全部crash。

查看报错信息如下:

1
2
3
4
2023-12-05T16:43:00+08:00 INFO     : MSTOR Create new metric shell_operator_live_ticks
2023-12-05T16:43:00+08:00 ERROR : error getting GVR for kind 'ClusterConfiguration': unable to retrieve the complete list of server APIs: metrics.k8s.io/v1beta1: the server is currently unable to handle the request
2023-12-05T16:43:00+08:00 ERROR : Enable kube events for hooks error: unable to retrieve the complete list of server APIs: metrics.k8s.io/v1beta1: the server is currently unable to handle the request
2023-12-05T16:43:03+08:00 INFO : TASK_RUN Exit: program halts.

查看apiservice

1
kubectl get apiservice

image-20231205170451670

可以看到metrics.k8s.io确实有问题

删了

1
kubectl delete apiservice v1beta1.metrics.k8s.io

删除之后,重启pod就好了

自定义crd供普通用户使用

对业务用户来说,deployment、service、ingress等概念的学习成本还是太高。我们可以自定义自己的资源描述,来对用户屏蔽这些细节。

使用operator来实现这一需求。

img

清理无用的镜像和关闭的容器

使用命令

1
2
docker system df
docker system prune -a

docker system df 命令,类似于 Linux上的 df 命令,用于查看 Docker 的磁盘使用情况:

img

TYPE列出了 Docker 使用磁盘的 4 种类型:

  • Images :所有镜像占用的空间,包括拉取下来的镜像,和本地构建的。
  • Containers :运行的容器占用的空间,表示每个容器的读写层的空间。
  • Local Volumes :容器挂载本地数据卷的空间。
  • Build Cache :镜像构建过程中产生的缓存空间(只有在使用 BuildKit 时才有,Docker 18.09 以后可用)。

最后的 RECLAIMABLE 是可回收大小。

  • docker system prune : 可以用于清理磁盘,删除关闭的容器、无用的数据卷和网络,以及 dangling 镜像(即无 tag 的镜像)。
  • docker system prune -a : 清理得更加彻底,可以将没有容器使用 Docker镜像都删掉。
    注意,这两个命令会把你暂时关闭的容器,以及暂时没有用到的 Docker 镜像都删掉了。

清理有问题的容器

如果磁盘水位还是比较高。那大概率是某个容器有问题,比如疯狂往磁盘上记日志。

使用命令

1
docker system df -v

可以看到每次容器的size。比如这个玩意

image-20231228104406512

然后就是docker exec -it 进去看看喽

最后反馈给开发,不用往本地目录里打日志文件了。

替换node节点操作

移除node

停止调度

1
kubectl cordon <节点名称>

驱逐pod

1
kubectl drain --force --ignore-daemonsets --delete-local-data <节点名称>

–force
当一些pod不是经 ReplicationController, ReplicaSet, Job, DaemonSet 或者 StatefulSet 管理的时候
就需要用–force来强制执行 (例如:kube-proxy)

–ignore-daemonsets
忽略DaemonSet管理下的Pod

–delete-local-data
如果有mount local volumn的pod,会强制杀掉该pod并把料清除掉
另外如果跟本身的配置讯息有冲突时,drain就不会执行

删除node

1
kubectl delete node <节点名称>

重新reset节点

到node节点上执行

1
kubeadm reset

去优化node服务器。。。

加入node

生成join命令

查看一下token

1
kubeadm token list

没有的话就要再重新生成下

1
kubeadm token create --print-join-command

到node节点执行

1
kubeadm join apiserver.cluster.local:6443 --token p6sxoz.h1izgkfmxpnbgfz9     --discovery-token-ca-cert-hash sha256:3cc46f16728c774c8381ad5bdd771b00977d023bf29328bab509ac5fb8d0dba1

成功截图

image-20240111164345256

问题

PodDisruptionBudget导致的驱逐失败

1
Cannot evict pod as it would violate the pod's disruption budget

解决:

查看所有的PodDisruptionBudget

1
kubectl get poddisruptionbudget -A

如果业务很重要的话,就要具体问题具体分析了。

可以增加副本数,在其他机器上多拉起来几个副本。

也可以修改poddisruptionbudget的配置,是不是不合理。

暴力的话,就直接删除了pdb

1
kubectl  -n xxx  delete poddisruptionbudget  xxxx

有状态应用 StatefulSet

难点:网络不变,存储不变,顺序不变

网络:

恒定的网络标识,通过无头service实现

headless service ClusterIP:None 不分配ip

业务pod访问使用:mysql-0.svc名,namespace名.svc.cluster.local

存储:

共享

顺序:

顺序性保证

statefulset创建的pod会加上顺序编号

启动和删除都会按顺序来