5-K8S核心技术pod

k8s.jpg

概述

pod是k8s中最小的单元,是一个抽象的概念,是一组容器的集合,是资源对象模型中用户创建或部署的最小的资源对象模型,也是k8s上运行容器化应用的资源对象,其他对象都是用来支撑或扩展pod对象功能的。每一个pod都有一个特殊的被称为“根容器”的pause容器,pause容器对应的镜像属于k8s平台的一部分。
pause容器又叫infra容器,后面将介绍改容器的作用。
pod

pod基本概念

  • 最小部署单元
  • pod是由一个或多个容器组成
  • 一个pod内的容器共享网络命名空间
  • pod是短暂的
  • 每个pod包含一个或多个紧密相关的用户业务容器

pod存在的意义

  1. 容器是但进程设计,一个容器运行一个应用进程
  2. pod是多进程设计,一个pod中可以有多个容器,可以运行多个应用进程
  3. pod的存在是为了亲密作用
    • 多应用之间进行交互
    • 网络之间的调用
    • 多个应用之间需要频繁调用

pod的设计理念是支持多个容器在pod中共享网络地址和文件系统,可以通过进程通信和文件共享这种简单高效的方式完成组合。可以将pod看做是运行在k8s上的小机器人,k8s的业务可以分为以下几种:

  • 长期伺服型:long-running
  • 批处理:batch
  • 节点后台支撑型:node-deamon
  • 有状态应用型:stateful application
    对应的控制器分别为:deployment、job、deamonSet、statefulSet

pod实现机制

容器本身之间是隔离的,一般通过namespace和group进行隔离,而pod中共享网络、共享存储是通过pause容器来实现的

pause容器

pause容器又叫infra容器,每个pod中都会有一个pause容器,其他容器称为业务容器,这些业务容器共享pause容器的网络和volume挂载卷;因此容器之间的通信和数据交换更为高效,可以充分利用这一点将一组密切关系的服务放在一个pod中;同一个pod中的容器之间使用localhost就能互相通信。这样pause容器就实现了pod内的共享网络与共享存储。

pause提供的功能

  • PID命名空间:作为Pod的“init进程”(pid=1,也即所有其它容器的父容器,所有其它进程的父进程),负责僵尸进程的回收,1.8之后的版本默认不共享PID
  • 网络命名空间:pod中的多个通容器可以访问同一个IP和端口范围
  • IPC命名空间:pod中的多个容器能够使用systemV IPC或POSIX消息队列进行通信
  • UTS命名空间:pod中的多个容器共享一个主机名、volume

pause代码

github pause代码

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
/*
Copyright 2016 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define STRINGIFY(x) #x
#define VERSION_STRING(x) STRINGIFY(x)

#ifndef VERSION
#define VERSION HEAD
#endif

static void sigdown(int signo) {
psignal(signo, "Shutting down, got signal");
exit(0);
}

static void sigreap(int signo) {
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}

int main(int argc, char **argv) {
int i;
for (i = 1; i < argc; ++i) {
if (!strcasecmp(argv[i], "-v")) {
printf("pause.c %s\n", VERSION_STRING(VERSION));
return 0;
}
}

if (getpid() != 1)
/* Not an error because pause sees use outside of infra containers. */
fprintf(stderr, "Warning: pause should be the first process\n");

if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
return 1;
if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
return 2;
if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,
.sa_flags = SA_NOCLDSTOP},
NULL) < 0)
return 3;

for (;;)
pause();
fprintf(stderr, "Error: infinite loop terminated\n");
return 42;
}

共享namespace

在Linux系统中,创建一个新的子进程的时候,该进程就会集成父进程的namespace,k8s的pod就是模拟这种方式。创建pod时,里面第一个被创建的容器永远都是pause容器(系统自动创建),pause容器创建完成后才会创建用户业务容器,系统会为pause创建namespace,同一个pod内创建的业务容器都加入到pause容器的namespace,这样就处于同一个namespace中。从pause代码中可以发现最后是一个死循环+pause函数调用,pause函数的作用是让当前进程暂停,因为要维持namespace的存在就必须有一个属于这个namespace的进程或者文件存在,所以为了维护Pod里面pause创建的Namespace,pause就必须一直存在。

充当系统init进程

在Linux系统中,pid=1的进程我们称之为“init”进程,是内核启动的第一个用户级进程,现在比较新的Linux发行版的init进程就是systemd进程。这个进程有许多工作,其中一个重要工作就是负责“收养孤儿进程”,防止产生太多僵尸进程。简单介绍一下相关的基本概念:Linux系统维护了一个进程表,记录每个进程的状态信息和退出码(exit code),当一个进程退出时,它在表中的信息会一直保留,直到其父进程调用wait(包括waitpid)获取其退出码。所谓僵尸进程就是进程已经退出了,但它的信息还在进程表里面。正常情况下,进程退出时父进程会马上查询该表,并回收子进程的相关资源,所以僵尸进程的持续状态一般都很短。但如果(1)父进程启动子进程之后没有调用wait或者(2)父进程先于子进程挂掉了,那子进程就会变成僵尸进程。如果是第2种情况,即父进程先于子进程死掉了,那操作系统就会把init进程设置为该父进程所有子进程的父进程,即init进程收养了该父进程的所有子进程。当这些子进程退出时,init进程就会充当父进程的角色,从而避免长时间的僵尸进程。但对于第1种情况,一般认为是代码有缺陷,这种情况因为子进程的父进程存在(只是没有调用wait而已),init进程是不会做处理的。此时子进程会成为僵尸进程长期存在,如果要消除这种僵尸进程,只能kill掉父进程。
而pause容器的第二个功能就是充当这个init进程,负责回收僵尸进程。从上面的代码可以看到,pause启动的时候会判断自己的pid是否为1。不过如果要实现该功能,则Pod内的所有容器必须和pause共享PID Namespace。在K8s 1.8版本之前默认是开启PID Namespace共享的,之后版本默认关闭了,用户可以通过–docker-disable-shared-pid=true/false自行设置。开启PID Namespace的好处就是可以享受pause回收僵尸进程的功能,并且因为容器同处于一个PID Namespace,进程间通信也会变得非常方便。但也有一些弊端,比如有些容器进程的PID也必须为1(比如systemd进程),这就会和pause容器产生冲突,另外也会涉及一些安全问题。

影响pod调度的属性

根据request进行调度

1
2
3
4
5
6
7
resources: #资源管理
requests: #容器运行时,最低资源需求,也就是说最少需要多少资源容器才能正常运行
cpu: 0.1 #CPU资源(核数),两种方式,浮点数或者是整数+m,0.1=100m,最少值为0.001核(1m)
memory: 32Mi #内存使用量
limits: #资源限制,表示最大占用的资源
cpu: 0.5
memory: 32Mi

根据nodeSelector调度

1
2
nodeSelector:
KEY: VALUE #会将pod调度到设定有该标签的node上

Label 是 Kubernetes 系统中另一个核心概念。一个 Label 是一个 key=value 的键值对,其中 key 与 value 由用户自己指 定。Label 可以附加到各种资源对象上,如 Node、Pod、Service、RC,一个资源对象可以定义任意数量的 Label, 同一个 Label 也可以被添加到任意数量的资源对象上,Label 通常在资源对象定义时确定,也可以在对象创建后动态 添加或删除。

1
2
# 为node节点设定标签
kubectl label node NODE_NAME KEY: VALUE

节点亲和性 nodeAffinity

硬亲和

1
2
3
4
5
6
7
8
9
10
affinity: 
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms: #写多个满足其中一条就可以
- matchExpressions: #可以写多个满足必须同时满足
- key: KEY #node节点的标签
operator: In # 运算关系
values:
- VALUE1
- VALUE2

通过requiredDuringSchedulingIgnoredDuringExecution硬亲和,约束条件必须满足。
上面的例子则表示为pod将会调度到标签KEY为VALUE1或VALUE2的节点上。

软亲和

1
2
3
4
5
6
7
8
9
10
11
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 60 #权重,1-100越大优先级越高
preference:
matchExpressions:
- {key: xxx, operator: In, values: xxx}
- weight: 30
preference:
matchExpressions:
- {key: xxx, operator: Exists, values: xxx}

preferredDuringSchedulingIgnoredDuringExecution软亲和为尝试满足约束条件

operator参数 说明
In label 的值在某个列表中
NotIn label 的值不在某个列表中
Gt label 的值大于某个值
Lt label 的值小于某个值
Exists 某个 label 存在
DoesNotExist 某个 label 不存在

污点与污点容忍

污点

使用kubectl taint可以为node设置污点,node被设置污点后将会与pod存在一种互斥关系,可以让node拒绝pod的调度,甚至将已经存在的pod驱逐出去
污点的组成 KEY=VALUE:EFFECT 其中value可以为空,EFFECT描述污点的作用,有以下三个选项:

  • NoSchedule : 不会将pod调度到具有该污点的node上
  • PreferNoSchedule:尽量避免将pod调度到有该污点的node上
  • NoExecute:不会将pod调度到有该污点的node上,同时会将node上已有的pod驱逐
1
2
3
4
5
6
7
8
# 设置污点
kubectl taint nodes NODENAME KEY=VAULE:EFFECT

# 查看node的污点
kubectl describe node NODENAME|grep Taint

# 去除污点
kubectl taint nodes NODENAME KEY:EFFECT-

污点容忍

设置了污点的node和pod之间产生互斥关系,pod将一定程度上不会调度到这个node上,但可以为pod设置容忍度,设置了容忍度的pod将会容忍指定的污点,从而可以调度到该node上,污点与污点容忍组合使用,避免pod调度到不适合的node上。每个node可以有一个或多个污点,不能忍受这些污点的pod是不会被节点接受的

1
2
3
4
5
6
7
8
9
10
11
12
13
tolerations:
- key: “key1” #要与污点key保持一致才会生效
operator: “Equal”
value: “value1” #要与污点value保持一致才会生效
effect: “NoExecute”
tolerationSeconds: 3600 # 表示pod即使被驱逐可以继续运行时间
- key: “key1”
operator: “Equal”
value: “value1”
effect: “NoExecute”
- key: “key2”
operator: “Exists” #若为Exists,则会忽略value
effect: “NoSchedule”

当有多个master时,防止资源浪费,可以进行设置,让master也启动pod

1
kubectl taint nodes NODENAME node-role.kubernetes.io/master=:PreferNoSchedule

当node有多个污点时,pod必须忍受所有的污点才可以调度到该node