理解 K8S 的设计精髓之 list-watch - koala bear

博主是蘑菇街的员工,写了不少kubernetes的文章。这篇的原文链接:http://wsfdl.com/kubernetes/2019/01/10/list_watch_in_k8s.html

至今清楚的记得,当了解到 K8S 组件之间仅采用 HTTP 协议通信,没有依赖中间件时,我非常好奇它是如何做到的。

在 K8S 内部通信中,肯定要保证消息的实时性。之前以为方式有两种:客户端(kubelet, scheduler, controller-manager 等)轮询 apiserver,或者 apiserver 通知客户端。如果采用轮询,势必会大大增加 apiserver 的压力,同时实时性很低。如果 apiserver 主动发 HTTP 请求,又如何保证消息的可靠性,以及大量端口占用问题?

当阅读完 list-watch 源码后,先是所有的疑惑云开雾散,进而为 K8S 的设计理念所折服。List-watch 是 K8S 统一的异步消息处理机制,保证了消息的实时性,可靠性,顺序性,性能等等,为声明式风格的 API 奠定了良好的基础,它是优雅的通信方式,是 K8S 架构的精髓。

List-Watch 是什么

Etcd 存储集群的数据信息,apiserver 作为统一入口,任何对数据的操作都必须经过 apiserver。客户端(kubelet/scheduler/ontroller-manager)通过 list-watch 监听 apiserver 中资源(pod/rs/rc 等等)的 create, update 和 delete 事件,并针对事件类型调用相应的事件处理函数。

那么 list-watch 具体是什么呢,顾名思义,list-watch 有两部分组成,分别是 list 和 watch。list 非常好理解,就是调用资源的 list API 罗列资源,基于 HTTP 短链接实现;watch 则是调用资源的 watch API 监听资源变更事件,基于 HTTP 长链接实现,也是本文重点分析的对象。以 pod 资源为例,它的 list 和 watch API 分别为:

List API,返回值为 PodList,即一组 pod。

GET /api/v1/pods

Watch API,往往带上 watch=true,表示采用 HTTP 长连接持续监听 pod 相关事件,每当有事件来临,返回一个 WatchEvent

GET /api/v1/watch/pods

K8S 的 informer 模块封装 list-watch API,用户只需要指定资源,编写事件处理函数,AddFunc, UpdateFunc 和 DeleteFunc 等。如下图所示,informer 首先通过 list API 罗列资源,然后调用 watch API 监听资源的变更事件,并将结果放入到一个 FIFO 队列,队列的另一头有协程从中取出事件,并调用对应的注册函数处理事件。Informer 还维护了一个只读的 Map Store 缓存,主要为了提升查询的效率,降低 apiserver 的负载。

list watch

Watch 是如何实现的

List 的实现容易理解,那么 Watch 是如何实现的呢?Watch 是如何通过 HTTP 长链接接收 apiserver 发来的资源变更事件呢?

秘诀就是 Chunked transfer encoding(分块传输编码),它首次出现在 HTTP/1.1 。正如维基百科所说:

HTTP 分块传输编码允许服务器为动态生成的内容维持 HTTP 持久链接。通常,持久链接需要服务器在开始发送消息体前发送Content-Length消息头字段,但是对于动态生成的内容来说,在内容创建完之前是不可知的。使用分块传输编码,数据分解成一系列数据块,并以一个或多个块发送,这样服务器可以发送数据而不需要预先知道发送内容的总大小。

当客户端调用 watch API 时,apiserver 在 response 的 HTTP Header 中设置 Transfer-Encoding 的值为 chunked,表示采用分块传输编码,客户端收到该信息后,便和服务端该链接,并等待下一个数据块,即资源的事件信息。例如:

$ curl -i http://{kube-api-server-ip}:8080/api/v1/watch/pods?watch=yes
HTTP/1.1 200 OK
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 02 Jan 2019 20:22:59 GMT
Transfer-Encoding: chunked

{"type":"ADDED", "object":{"kind":"Pod","apiVersion":"v1",...}}
{"type":"ADDED", "object":{"kind":"Pod","apiVersion":"v1",...}}
{"type":"MODIFIED", "object":{"kind":"Pod","apiVersion":"v1",...}}
...

谈谈 List-Watch 的设计理念

当设计优秀的一个异步消息的系统时,对消息机制有至少如下四点要求:

  • 消息可靠性
  • 消息实时性
  • 消息顺序性
  • 高性能

首先消息必须是可靠的,list 和 watch 一起保证了消息的可靠性,避免因消息丢失而造成状态不一致场景。具体而言,list API 可以查询当前的资源及其对应的状态(即期望的状态),客户端通过拿期望的状态和实际的状态进行对比,纠正状态不一致的资源。Watch API 和 apiserver 保持一个长链接,接收资源的状态变更事件并做相应处理。如果仅调用 watch API,若某个时间点连接中断,就有可能导致消息丢失,所以需要通过 list API 解决消息丢失的问题。从另一个角度出发,我们可以认为 list API 获取全量数据,watch API 获取增量数据。虽然仅仅通过轮询 list API,也能达到同步资源状态的效果,但是存在开销大,实时性不足的问题。

消息必须是实时的,list-watch 机制下,每当 apiserver 的资源产生状态变更事件,都会将事件及时的推送给客户端,从而保证了消息的实时性。

消息的顺序性也是非常重要的,在并发的场景下,客户端在短时间内可能会收到同一个资源的多个事件,对于关注最终一致性的 K8S 来说,它需要知道哪个是最近发生的事件,并保证资源的最终状态如同最近事件所表述的状态一样。K8S 在每个资源的事件中都带一个 resourceVersion 的标签,这个标签是递增的数字,所以当客户端并发处理同一个资源的事件时,它就可以对比 resourceVersion 来保证最终的状态和最新的事件所期望的状态保持一致。

List-watch 还具有高性能的特点,虽然仅通过周期性调用 list API 也能达到资源最终一致性的效果,但是周期性频繁的轮询大大的增大了开销,增加 apiserver 的压力。而 watch 作为异步消息通知机制,复用一条长链接,保证实时性的同时也保证了性能。

最后

List-Watch 基于 HTTP 协议,是 K8S 重要的异步消息通知机制。它通过 list 获取全量数据,通过 watch API 监听增量数据,保证消息可靠性,实时性,性能和顺序性。而消息的实时性,可靠性和顺序性又是实现声明式设计的良好前提。它简洁优雅,功能强大,是 K8S 的精髓之一。本人读后,叹为观止。


kubernetes 调度实践之节点标签/污点、应用容忍/nodeSelector/配额/反亲和/lifecycle

平时常给节点添加标签/污染,还有给应用添加容忍、配额和反亲和,这篇文章记录一些常用的命令,方便查阅。

本文中的特性基于kubernetes 1.10.

一、节点标签

节点标签的用处主要是便于标识每台机器由哪个应用团队使用,也可用于调度方面。

添加:

kubectl label nodes <node-name> app=wechat.kelu.org

删除:

命令行最后指定 Label 的 key 名并与一个减号相连:

kubectl label nodes <node-name> app- 

修改:

加上–overwrite参数:

kubectl label nodes <node-name> app=blog.kelu.org --overwrite

二、污点和容忍

污点和容忍常搭配用来做容器调度的,可以将某几台机器指定用于特定的应用。需要注意的是,默认情况下污点和容忍对daemonset不生效。

为节点添加污点:

kubectl taint node <node-name> app=blog.kelu.org:NoSchedule

为 deployment 添加容忍,与 yaml 文件的containers平级

"tolerations": [
          {
            "key": "app",
            "operator": "Equal",
            "value": "blog.kelu.org",
            "effect": "NoSchedule"
          }
        ]

其中effect的值可以为NoSchedule ,PreferNoSchedule ,NoExecute

查看节点的描述:

kubectl describe nodes <node-name>

删除污点:

kubectl taint nodes <node-name> app:NoSchedule-

三、选择节点

pod选择制定节点运行

        "nodeSelector": {
          "app": "app=blog.kelu.org"
        },

四、资源限制

可以通过 ResourceQuotaLimitRange 对namespace里的pod进行限制。 ResourceQuota 是 namespace 中所有 Pod 占用资源的 request 和 limit, LimitRange 是 namespace 中单个 Pod 的默认资源 request 和 limit。

同时,也可以在 deployment 中对单个 pod 进行限制。

ResourceQuota限制资源占用

apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute-resources
  namespace: spark-cluster
spec:
  hard:
    pods: "20"
    requests.cpu: "20"
    requests.memory: 100Gi
    limits.cpu: "40"
    limits.memory: 200Gi

ResourceQuota限制资源数量

apiVersion: v1
kind: ResourceQuota
metadata:
  name: object-counts
  namespace: spark-cluster
spec:
  hard:
    configmaps: "10"
    persistentvolumeclaims: "4"
    replicationcontrollers: "20"
    secrets: "10"
    services: "10"
    services.loadbalancers: "2"

LimitRange 配置单个 Pod 的 CPU和内存

apiVersion: v1
kind: LimitRange
metadata:
  namespace: blog
  name: mem-cpu-res-limit
spec:
  limits:
  - default:
      memory: 5G
      cpu: 5
    defaultRequest:
      memory: 1Gi
      cpu: 1
    max:
      memory: 1024Mi
      cpu: 1
    min:
      memory: 128Mi
      cpu: 0.5
    type: Container    
  • default:即该命名空间配置resourceQuota时,创建容器的默认限额上限
  • defaultRequest:即该命名空间配置resourceQuota时,创建容器的默认请求上限
  • max:即该命名空间下创建容器的资源最大值
  • min:即该命名空间下创建容器的资源最小值

###在 deployment 中对 pod 限制

apiVersion: extensions/v1beta1
kind: Deployment
spec:
  template:
    spec:
      containers:
      - name: xxx
        image: xxx
        resources:
          limits:
            cpu: 4
            memory: 5Gi
          requests:
            cpu: 100m
            memory: 1Gi

五、亲和性调度

Affinity 是“亲和性”, Anti-Affinity,翻译成“互斥”或“反亲和”。

我们常用反亲和来让同一类Pod平均分布在不同的机器上。

目前有两种主要的 node affinity:

  • requiredDuringSchedulingIgnoredDuringExecution :pod 必须部署到满足条件的节点上,如果没有满足条件的节点,就不断重试;
  • preferredDuringSchedulingIgnoredDuringExecution :优先部署在满足条件的节点上,如果没有满足条件的节点,就忽略这些条件,按照正常逻辑部署。

requiredDuringSchedulingIgnoredDuringExecution

     affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchLabels:
                io.service: blog
            "topologyKey": "kubernetes.io/hostname"

preferredDuringSchedulingIgnoredDuringExecution

      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: io.service
                  operator: In
                  values:
                  - blog
              topologyKey: kubernetes.io/hostname

更详细的说明请参考官方文档:

六、lifecycle

在容器调度时,如果容器正在处理请求,粗暴的杀死会造成应用的不稳定。为了应对这种情况,让Pod更优雅地退出/调度,kubernetes 提供了lifecycle 的特性。目前 lifecycle 可以在容器生命周期定义了两个钩子:

  • PostStart, 在容器创建后运行(不能保证会在entrypoint前运行)
  • PreStop 在容器终止前调用

我们可以通过 PreStop 钩子,通过延迟容器终止,让容器运行完可能的请求再退出:

apiVersion: extensions/v1beta1
kind: Deployment
spec:
  template:
    spec:
      containers:
      - name: xxx
        image: xxx
	    lifecycle:
          preStop:
            exec:
              command: ["sleep","5s"]

延迟退出的时间需要更多的应用测试来把握。

参考资料


linux 命令 ip

ip 命令跟 ifconfig 命令有些类似,但要强力的多,它有许多新功能。

ifconfig 是 net-tools 中已被废弃使用的一个命令,许多年前就已经没有维护了。iproute2 套件里提供了许多增强功能的命令,ip 命令是其中之一。

Net tools vs Iproute2

假设我们服务器的网卡名为 enp0s3。

网卡 ip link

检查网卡的诸如 IP 地址,子网等网络信息,使用 ip addr show 命令:

$ ip a

这会显示系统中所有可用网卡的相关网络信息,不过如果你想查看某块网卡的信息,则命令为:

$ ip addr show eth0

这里 eth0 是网卡的名字。

启用/禁用网卡:

使用 ip 命令来启用一个被禁用的网卡:

$ ip link set enp0s3 up

而要禁用网卡则使用 down 触发器:

$ ip link set enp0s3 down

IP ip addr

要为网卡分配 IP 地址,我们使用下面命令:

$ ip addr add 192.168.0.50/255.255.255.0 dev eth0
$ ip addr add 192.168.0.193/24 dev eth0

也可以使用 ip 命令来设置广播地址。默认是没有设置广播地址的,设置广播地址的命令为:

$ ip addr add broadcast 192.168.0.255 dev enp0s3

我们也可以使用下面命令来根据 IP 地址设置标准的广播地址, brd 代替 broadcast 来设置广播地址:

$ ip addr add 192.168.0.10/24 brd + dev enp0s3

若想从网卡中删掉某个 IP,使用如下 ip 命令:

$ ip addr del 192.168.0.10/24 dev enp0s3

添加别名,即为网卡添加不止一个 IP,执行下面命令:

$ ip addr add 192.168.0.20/24 dev enp0s3 label enp0s3:1

ipv6

$ ip -6 addr show [dev <接口名>]

路由表 ip route

查看路由信息会给我们显示数据包到达目的地的路由路径。要查看网络路由信息,执行下面命令:

$ ip route show

在上面输出结果中,我们能够看到所有网卡上数据包的路由信息。我们也可以获取特定 IP 的路由信息,方法是:

$ ip route get 192.168.0.1

我们也可以使用 IP 来修改数据包的默认路由。方法是使用 ip route 命令:

$ ip route add default via 192.168.0.150/24

这样所有的网络数据包通过 192.168.0.150 来转发,而不是以前的默认路由了。若要修改某个网卡的默认路由,执行:

$ ip route add 172.16.32.32 via 192.168.0.150/24 dev enp0s3

要删除之前设置的默认路由,打开终端然后运行:

$ ip route del 192.168.0.150/24

注意: 用上面方法修改的默认路由只是临时有效的,在系统重启后所有的改动都会丢失。要永久修改路由,需要修改或创建 route-enp0s3 文件。将下面这行加入其中:

$ vi /etc/sysconfig/network-scripts/route-enp0s3172.16.32.32 via 192.168.0.150/24 dev enp0s3

保存并退出该文件。

若你使用的是基于 Ubuntu 或 debian 的操作系统,则该要修改的文件为 /etc/network/interfaces,然后添加 ip route add 172.16.32.32 via 192.168.0.150/24 dev enp0s3 这行到文件末尾。

ARP ip neigh

ARP,是地址解析协议Address Resolution Protocol的缩写,用于将 IP 地址转换为物理地址(也就是 MAC 地址)。所有的 IP 和其对应的 MAC 明细都存储在一张表中,这张表叫做 ARP 缓存。

要查看 ARP 缓存中的记录,即连接到局域网中设备的 MAC 地址,则使用如下 ip 命令:

$ ip neigh

删除 ARP 记录的命令为:

$ ip neigh del 192.168.0.106 dev enp0s3

若想往 ARP 缓存中添加新记录,则命令为:

$ ip neigh add 192.168.0.150 lladdr 33:1g:75:37:r3:84 dev enp0s3 nud perm

这里 nud 的意思是 “neghbour state”(网络邻居状态),它的值可以是:

  • perm - 永久有效并且只能被管理员删除
  • noarp - 记录有效,但在生命周期过期后就允许被删除了
  • stale - 记录有效,但可能已经过期
  • reachable - 记录有效,但超时后就失效了

清空ARP表(不影响永久条目)

ip neigh flush all

查看网络信息

通过 ip 命令还能查看网络的统计信息,比如所有网卡上传输的字节数和报文数,错误或丢弃的报文数等。使用 ip -s link 命令来查看:

$ ip -s link

监控netlink消息

也可以使用ip命令查看netlink消息。monitor选项允许你查看网络设备的状态。比如,所在局域网的一台电脑根据它的状态可以被分类成REACHABLE或者STALE。使用下面的命令:

$ ip monitor all

常见的黑入网站的链接

如果你手头有网站放在互联网上,只要搜索一遍 nginx 或者 apache 的日志,肯定看到很多奇怪的请求链接。

我也是如此。这篇文章纯粹做个记录,遇到新的请求也会记录更新本文。


debian 下简单的时间同步

以前写过一个 CentOS 下的 ntp 时间同步,centos 一般公司的服务器会使用。就我个人还是比较习惯 Debian 系的,所以我Debian 系的一些文章都是相对简易的,主要目标就是 It Just Work 即可。

说一下写这篇的背景,我给自己服务器中安装了 Etcd 集群,使用集群的时候就有一个重要的前提——时钟一致。一般的做法是在集群中指定一台机器作为ntp服务器,由这台向远端同步,其它机器同步这台。

个人服务要求不高的话,可以在所有机器中按照如下方式实现:

  1. 安装
     $ sudo apt-get update
     $ sudo apt-get install ntpdate -y
    
  2. 同步

     ntpdate time.windows.com
    

    当然也可以用阿里的服务器 ntp.aliyun.com

  3. 定时同步 每小时同步一次

     $ sudo crontab -e
    
     在最后一行添加
     5 * * * * ntpdate time.windows.com > /dev/null 2>&1
    

1 2 3 4 5 6 86 87 88 89 90