1.Etcd的容量
集群的节点数的多少除了要顾及性能压力外,还要考虑etcd的存储容量。可以反向的从内存大小去推断一个集群中能够支持的节点数目,以及缓存大小(此处的缓存值反序列化缓存大小)。官方的建议是120G的主机可以支持2000个节点,由此推断,每个节点的容量大概在60M左右,但是随着版本的更新,存入Etcd的数据和结构可能发生变化,该值也是需要调整的。
序列化缓存大小计算:
TargetRAMMB 为主机内存大小
clusterSize := s.GenericServerRunOptions.TargetRAMMB / 60
DeserializationCacheSize = 25 * clusterSize < 1000 ? 1000 :25 * clusterSize
2.controller-manager
- controller-manager启动之初,如果发现apiserver未启动(/healthz地址访问不通),会等待10s.
- controller-manager对控制器的检查都是动态的,所以重启apiserver无需重启controller-manager。
- controller-manager中每个控制器内部都会有一个queue用来存贮watch到的事件,同时这个队列是通过令牌桶做了限流处理,默认情况下最小延迟5毫秒,最大延迟1000秒。
return NewMaxOfRateLimiter( NewItemExponentialFailureRateLimiter(5*time.Millisecond, 1000*time.Second), // 10 qps, 100 bucket size. This is only for retry speed and its only the overall factor (not per item) &BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)}, )
-
controller-manager对pod的gc处理主要包括三种情况
1.gc掉超过阈值限制的pod,按时间排序gc
func (gcc *PodGCController) gcTerminated(pods []*v1.Pod) { terminatedPods := []*v1.Pod{} for _, pod := range pods { if isPodTerminated(pod) { terminatedPods = append(terminatedPods, pod) } } terminatedPodCount := len(terminatedPods) sort.Sort(byCreationTimestamp(terminatedPods)) deleteCount := terminatedPodCount - gcc.terminatedPodThreshold if deleteCount > terminatedPodCount { deleteCount = terminatedPodCount } if deleteCount > 0 { glog.Infof("garbage collecting %v pods", deleteCount) } var wait sync.WaitGroup for i := 0; i < deleteCount; i++ { wait.Add(1) go func(namespace string, name string) { defer wait.Done() if err := gcc.deletePod(namespace, name); err != nil { // ignore not founds defer utilruntime.HandleError(err) } }(terminatedPods[i].Namespace, terminatedPods[i].Name) } wait.Wait() }
2.gc掉孤儿pod:pod上的node信息不在当前可调度的节点上,即没有和有效node绑定
```
func (gcc *PodGCController) gcOrphaned(pods []*v1.Pod) {
glog.V(4).Infof("GC'ing orphaned")
// We want to get list of Nodes from the etcd, to make sure that it's as fresh as possible.
nodes, err := gcc.kubeClient.CoreV1().Nodes().List(metav1.ListOptions{})
if err != nil {
return
}
nodeNames := sets.NewString()
for i := range nodes.Items {
nodeNames.Insert(nodes.Items[i].Name)
}
for _, pod := range pods {
if pod.Spec.NodeName == "" {
continue
}
if nodeNames.Has(pod.Spec.NodeName) {
continue
}
glog.V(2).Infof("Found orphaned Pod %v assigned to the Node %v. Deleting.", pod.Name, pod.Spec.NodeName)
if err := gcc.deletePod(pod.Namespace, pod.Name); err != nil {
utilruntime.HandleError(err)
} else {
glog.V(0).Infof("Forced deletion of orphaned Pod %s succeeded", pod.Name)
}
}
}
3.gc掉没有调度成功的pod:表现在pod的NodeName为空,主要由于资源等条件不满足
func (gcc *PodGCController) gcUnscheduledTerminating(pods []*v1.Pod) {
glog.V(4).Infof("GC'ing unscheduled pods which are terminating.")
for _, pod := range pods {
if pod.DeletionTimestamp == nil || len(pod.Spec.NodeName) > 0 {
continue
}
glog.V(2).Infof("Found unscheduled terminating Pod %v not assigned to any Node. Deleting.", pod.Name)
if err := gcc.deletePod(pod.Namespace, pod.Name); err != nil {
utilruntime.HandleError(err)
} else {
glog.V(0).Infof("Forced deletion of unscheduled terminating Pod %s succeeded", pod.Name)
}
}
}
- controller-manager对node的gc处理
//删除在删除队列中的Node
go wait.Until(gc.runAttemptToDeleteWorker, 1*time.Second, stopCh)
//删除孤儿Node
go wait.Until(gc.runAttemptToOrphanWorker, 1*time.Second, stopCh)
3.kubelet中的GarbageCollect
-
containerGC
是由runtimeManager中的containerGC完成,她通过相关的gc策略移除死亡容器。需要主要的是每个pod都包含sandBox,相关的gc策略不会作用于sandboxes,只有sandboxes内部没有容器的情况下,才会被GC清理掉。
GC的过程遵循以下步骤:
① 获取不活动并且存活时间大于策略中最小时间(MinAge)的可驱逐容器
② 根据策略中每个容器能够保留的旧的实例数(MaxPerPodContainer,默认为1),移除旧的死亡容器
③ 根据策略中全局能够能够保留的旧的实例总数(MaxPerPodContainer,默认为1),移除旧的死亡容器。
④ 获取没有ready,内部不存在容器的sandboxes
⑤ 移除上述sandboxes注意:gc策略不应用于沙盒
-
ImageGC
根据相关的策略,如磁盘剩余低于设定阈值进行回收,
4.实际上即便是kube-apisever自身和etcd通讯也是infomers的接口,只是本身的连接是通过回环网卡实现的。
client, err := internalclientset.NewForConfig(genericConfig.LoopbackClientConfig)
if err != nil {
lastErr = fmt.Errorf("failed to create clientset: %v", err)
return
}
kubeClientConfig := genericConfig.LoopbackClientConfig
sharedInformers = informers.NewSharedInformerFactory(client, 10*time.Minute)
5.Informer封装了对apiserver的事件监听处理操作包括AddFunc,UpdateFunc,DeleteFunc。
6.node的亲和与反亲和
有nodeAffinity和nodeSelector两种方式去控制的。
nodeSelector是必须要满足的情况。
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
env: test
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
nodeSelector:
disktype: ssd
nodeAffinity有两种类型
- requiredDuringSchedulingIgnoredDuringExecution(对应预选过程,为硬性条件必须满足)
后缀IgnoredDuringExecutionn:表示如果labels发生改变,即便不满足规则,也继续运行。
会有多个nodeSelectorTerms ,nodeSelectorTerms 下会有多个matchExpressions,在nodeSelectorTerms 维度需要满足一个,但是需要满足的nodeSelectorTerms下的matchExpressions必须全部满足。 - preferredDuringSchedulingIgnoredDuringExecution(对应优选过程,涉及打分过程)
后缀RequiredDuringExecution:表示如果labels发生改变,不满足规则,kubelet执行驱逐(未实现)。spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: label-node-resource #表示该机器上需要有的资源,通过label标注其值 operator: Exists values: - gpu preferredDuringSchedulingIgnoredDuringExecution: - weight: 10 preference: matchExpressions: - key: label-category operator: In values: - loadbalance
6.在大规模场景下的API Server优化
- 心跳请求
API Server 处理心跳请求是非常大的开销。原有的 Update Node Status方式每次需要发送大于10K数据包,而开启 NodeLease 之后,Kubelet 会使用非常轻量的 NodeLease 对象 (0.1 KB) ,集群 API Server 开销的 CPU 大约降低了一半。新版本默认已开启[root@node3 bin]# ./kubelet -h |grep NodeLease NodeLease=true|false (BETA - default=true)
- 限流功能
①API Server只限制最大读和写并发数,而没有限制特定客户端请求并发量的功能,因此建议所有客户端使用 Informer 去 List/Watch 资源,禁止在处理逻辑里面直接调用 Client 去向 API Server List 资源。
②随着Kubelet 和Daemonset 会增加 List Pod 请求的请求量,可以用 Bookmark 特性来解决这个问题,在未上线 Bookmark 的集群内,可以调整 API Server 对资源的更新事件缓存量来缓解该问题 (如 --watch-cache-sizes=node#1000,pod#5000),同时调整 Daemonset Re-Watch 的时间间隔:informerOpts := informers.WithTweakListOptions(func(options *v1.ListOptions) { if options.TimeoutSeconds != nil { // default [5min, 10min), overwrite this timeout value // it's too frequently for apiserver to process "List pods" requests // in a large cluster. minWatchTimeout := 24 * time.Hour clientReadTimeout := int64(minWatchTimeout.Seconds() * (1 + rand.Float64())) options.TimeoutSeconds = &clientReadTimeout } })
bookmark:可以认为是watch的资源,watch相当于给apiserver增加一个需要过滤的标签
7.什么样的容器需要放在一个Pod中
前者依赖后者关系非常紧密的:
- 容器之间需要文件交换的,比如日志文件
- 需要通过localhost或者本地socket通信的
- 容器中间分厂频繁的RPC调用
- 需要共享网络协议栈
8.sidecar的应用场景
- 应用初始化init-contianer
- 应用debug
- 日志采集
- ssh镜像
- 网络代理:代理容器,同一个网络协议栈,物业性能锁好
- 业务 adapter,接口升级情况等
9.configmap的限制大小为1M(时间上是ETCD的要求),manifest无法使用
10.PV和PVC
PV分为静态和动态,静态需要预先创建
-
PersistentVolume的访问模式
ReadWriteOnce(RWO) – 只有一个节点可以读写
ReadOnlyMany(ROX) – 多个节点可读
ReadWriteMany(RWX) – 多个节点读写 -
PV的回收策略
Retain – 保留,手工维护
Recycle – 不保留直接删除 (rm -rf /thevolume/*),可以被重新挂载
Delete – 直接删除volumePV可以通过节点的亲和性(NodeAffinity)限制哪些node可以挂载
11.debug的调试方式、
- 集群中的应用需要本地应用调试时,可以将本地应用代理到集群中的一个service上
Telepresence --swap -deployment {{deploymenet-name}}
- 本地医用需要依赖集群中的应用时,通过port-forward将远程应用代理到本地
kubectl -n {{namespace}} port-forward svc/aapp
- kubectl debug 无侵入性调试
12.Pod中的容器是如何共享namespace
实际上容器共享并不是共享所有内容如UTC只共享主机名称
func modifyHostOptionsForContainer(nsOpts *runtimeapi.NamespaceOption, podSandboxID string, hc *dockercontainer.HostConfig) {
sandboxNSMode := fmt.Sprintf("container:%v", podSandboxID)
hc.NetworkMode = dockercontainer.NetworkMode(sandboxNSMode)
hc.IpcMode = dockercontainer.IpcMode(sandboxNSMode)
hc.UTSMode = ""
if nsOpts.GetNetwork() == runtimeapi.NamespaceMode_NODE {
hc.UTSMode = namespaceModeHost
}
}
主要步骤如下
- 通过pause创建PodSandbox并返回PodSandboxId(容器ID)
podSandBoxID, err := m.runtimeService.RunPodSandbox(podSandboxConfig, runtimeHandler)
- 启动其他容器通过PodSandboxId获取ns相关项,配置到hostconfig中
func modifyHostOptionsForContainer(nsOpts *runtimeapi.NamespaceOption, podSandboxID string, hc *dockercontainer.HostConfig) { sandboxNSMode := fmt.Sprintf("container:%v", podSandboxID) hc.NetworkMode = dockercontainer.NetworkMode(sandboxNSMode) hc.IpcMode = dockercontainer.IpcMode(sandboxNSMode) hc.UTSMode = "" if nsOpts.GetNetwork() == runtimeapi.NamespaceMode_NODE { hc.UTSMode = namespaceModeHost } }
13.QOS
- Guaranteed:Pod 里的每个容器都必须有内存/CPU 限制和请求,而且值必须相等。如果一个容器只指明limit而未设定request,则request的值等于limit值。
- Burstable:Pod 里至少有一个容器有内存或者 CPU 请求且不满足Guarantee 等级的要求,即内存/CPU 的值设置的不同。
- BestEffort:容器必须没有任何内存或者 CPU 的限制或请求。
三种 QoS 优先级,从高到低(从左往右)Guaranteed --> Burstable --> BestEffort
回收顺序:BestEffort --> Burstable --> Guaranteed
14.Informer 关键逻辑解析
https://blog.csdn.net/chaosj/article/details/83831623
15.Request和Limit对应Cgroups的实现
-
requests
- requests用于schedule阶段,在调度pod保证所有pod的requests总和小于node能提供的计算能力
- requests.cpu被转成docker的--cpu-shares参数,与cgroup cpu.shares功能相同
- 设置容器的cpu的相对权重
- 该参数在CPU资源不足时生效,根据容器requests.cpu的比例来分配cpu资源
- CPU资源充足时,requests.cpu不会限制container占用的最大值,container可以独占CPU
- requests.memory没有对应的docker参数,作为k8s调度依据
- 使用requests来设置各容器需要的最小资源
-
limits
limits限制运行时容器占用的资源
limits.cpu会被转换成docker的–cpu-quota参数。与cgroup cpu.cfs_quota_us功能相同
限制容器的最大CPU使用率。
cpu.cfs_quota_us参数与cpu.cfs_period_us结合使用,后者设置时间周期
k8s将docker的–cpu-period参数设置100毫秒。对应着cgroup的cpu.cfs_period_us
limits.cpu的单位使用m,千分之一核
limits.memory会被转换成docker的–memory参数。用来限制容器使用的最大内存
当容器申请内存超过limits时会被终止
15.Node的异常和恢复
-
Node的异常
(1)Node状态变为NotReady
(2)Pod 5分钟之内状态无变化,5分钟之后的状态变化:
Daemonset的Pod状态变为Nodelost,
Deployment、Statefulset和Static Pod的状态先变为NodeLost,然后马上变为Unknown。
Deployment的pod会recreate,但是Deployment如果是node selector停掉kubelet的node,则recreate的pod会一直处于Pending的状态。
Static Pod和Statefulset的Pod会一直处于Unknown状态。 -
Kubelet恢复
(1)Node状态变为Ready。
(2)Daemonset的pod不会recreate,旧pod状态直接变为Running。 (3)Deployment的则是将kubelet进程停止的Node删除(原因可能是因为旧Pod状态在集群中有变化,但是Pod状态在变化时发现集群中Deployment的Pod实例数已经够了,所以对旧Pod做了删除处理)
(4)Statefulset的Pod会重新recreate。
(5)Staic Pod没有重启,但是Pod的运行时间会在kubelet起来的时候置为0。
在kubelet停止后,statefulset的pod会变成nodelost,接着就变成unknown,但是不会重启,然后等kubelet起来后,statefulset的pod才会recreate。
原文
网友评论