Kubernetes调度之亲和性和反亲和性

背景

Kubernetes中的调度策略可以大致分为两种,一种是全局的调度策略,要在启动调度器时配置,包括kubernetes调度器自带的各种predicates和priorities算法,具体可以参看文章《Kubernetes调度详解》;另一种是运行时调度策略,包括nodeAffinity(主机亲和性),podAffinity(POD亲和性)以及podAntiAffinity(POD反亲和性)。

nodeAffinity 主要解决POD要部署在哪些主机,以及POD不能部署在哪些主机上的问题,处理的是POD和主机之间的关系。

podAffinity 主要解决POD可以和哪些POD部署在同一个拓扑域中的问题(拓扑域用主机标签实现,可以是单个主机,也可以是多个主机组成的cluster、zone等。),podAntiAffinity主要解决POD不能和哪些POD部署在同一个拓扑域中的问题。它们处理的是Kubernetes集群内部POD和POD之间的关系。

三种亲和性和反亲和性策略的比较如下表所示:

未分类

本文主要介绍如何使用亲和性和反亲和性做资源调度。

使用场景

nodeAffinity使用场景 :

  • 将S1服务的所有Pod部署到指定的符合标签规则的主机上。
  • 将S1服务的所有Pod部署到除部分主机外的其他主机上。

podAffinity使用场景 :

  • 将某一特定服务的pod部署在同一拓扑域中,不用指定具体的拓扑域。
  • 如果S1服务使用S2服务,为了减少它们之间的网络延迟(或其它原因),把S1服务的POD和S2服务的pod部署在同一拓扑域中。

podAntiAffinity使用场 景:

  • 将一个服务的POD分散在不同的主机或者拓扑域中,提高服务本身的稳定性。
  • 给POD对于一个节点的独占访问权限来保证资源隔离,保证不会有其它pod来分享节点资源。
  • 把可能会相互影响的服务的POD分散在不同的主机上。

对于亲和性和反亲和性,每种都有三种规则可以设置:

RequiredDuringSchedulingRequiredDuringExecution :在调度期间要求满足亲和性或者反亲和性规则,如果不能满足规则,则POD不能被调度到对应的主机上。在之后的运行过程中,如果因为某些原因(比如修改label)导致规则不能满足,系统会尝试把POD从主机上删除(现在版本还不支持)。

RequiredDuringSchedulingIgnoredDuringExecution :在调度期间要求满足亲和性或者反亲和性规则,如果不能满足规则,则POD不能被调度到对应的主机上。在之后的运行过程中,系统不会再检查这些规则是否满足。

PreferredDuringSchedulingIgnoredDuringExecution :在调度期间尽量满足亲和性或者反亲和性规则,如果不能满足规则,POD也有可能被调度到对应的主机上。在之后的运行过程中,系统不会再检查这些规则是否满足。

使用示例

使用POD亲和性调度时要先开启Kubernetes调度器的MatchInterPodAffinity筛选功能,具体的操作方式是修改调度器的配置文件,在predicates中增加如下内容:

{"name": "MatchInterPodAffinity"}

测试环境的主机信息如下:

未分类

其中每个主机上都有 beta.kubernetes.io/arch、beta.kubernetes.io/os、kubernetes.io/hostname这几个标签,在测试过程中把这些标签当做拓扑域使用。

nodeAffinity 使用示例:

使用nodeAffinity把POD部署到主机mesos-slave1和mesos-slave2上,yaml定义如下:

{
  "nodeAffinity": {
    "requiredDuringSchedulingIgnoredDuringExecution": {
      "nodeSelectorTerms": [
        {
          "matchExpressions": [
            {
              "key": "kubernetes.io/hostname",
              "operator": "In",
              "values": [
                "mesos-slave1",
                "mesos-slave2"
              ]
            }
          ]
        }
      ]
    }
  }
}

创建一个有6个POD的RC,结果如下:

未分类

从结果可以看出POD被部署到了mesos-slave1和mesos-slave2上,mesos-slave3上没有部署POD。

podAffinity使用示例 :

使用kubernetes.io/hostname作为拓扑域,把pod创建在同一主机上。其中matchExpressions中填写内容对应到RC中POD自身的标签。可以通过修改需要匹配的标签内容来控制把一个服务中的POD和其它服务的POD部署在同一主机上。

yaml中的定义如下:

{
  "podAffinity": {
    "requiredDuringSchedulingIgnoredDuringExecution": [
      {
        "labelSelector": {
          "matchExpressions": [
            {
              "key": "name",
              "operator": "In",
              "values": [
                "node-rc"
              ]
            }
          ]
        },
        "topologyKey": "kubernetes.io/hostname"
      }
    ]
  }
}

创建一个有3个POD的RC,结果如下:

未分类

所有创建的POD集中在同一个主机上,具体的主机是哪个不需要指定。

podAntiAffinity 使用示例:

使用kubernetes.io/hostname作为拓扑域,把pod创建在不同主机上,每个主机上最多只有一个同类型的POD(同类型用标签区分)。其中matchExpressions中填写内容对应到RC中POD自身的标签。可以通过修改需要匹配的标签内容来控制把一个服务中的POD和其它服务的POD部署在不同主机上。

yaml中的定义如下:

{
  "podAntiAffinity": {
    "requiredDuringSchedulingIgnoredDuringExecution": [
      {
        "labelSelector": {
          "matchExpressions": [
            {
              "key": "name",
              "operator": "In",
              "values": [
                "node-rc"
              ]
            }
          ]
        },
        "topologyKey": "kubernetes.io/hostname"
      }
    ]
  }
}

创建一个有4个POD的RC,结果如下:

未分类

三个主机上都有一个POD运行,因为每个主机上最多只能运行一个这种类型的POD,所以有一个POD一直处于Pending状态,不能调度到任何节点。

上边的例子中可以通过修改topologyKey来限制拓扑域的范围,实现把相关服务部署在不同的容灾域等其它功能。

总结

Kubernetes提供了丰富的调度策略,包括静态的全局调度策略,以及动态的运行时调度策略,用户可以根据需要自由组合使用这些策略来实现自己的需求。在调度过程中,使用nodeAffnity决定资源可以部署在哪些主机上,使用podAffinity和podAntiAffinity决定哪些资源需要部署在同一主机(拓扑域)或者不能部署在同一主机。

kubernetes集群使用Ceph

https://blog.csdn.net/aixiaoyang168/article/details/78999851

经实验,在一个node上多个Pod是可以以ReadWrite模式挂载同一个CephRBD,但是跨node则不行,会提示image xxx is locked by other nodes。而我们的应用场景是需要多个node挂载一个ceph的,在我们的应用场景需要使用CephFS。

使用cephfs的场景:创建一个fs,挂载的时候指定path。

kubernetes使用CephFS的两种方式:

1.直接通过pod挂载

apiVersion: v1
kind: Pod
metadata:
name: cephfs2
spec:
containers:
- name: cephfs-rw
image: busybox
command: ["sleep", "60000"]
volumeMounts:
- mountPath: "/mnt/cephfs"
name: cephfs
volumes:
- name: cephfs
cephfs:
monitors:
- '<your_etcd_ip>:6789'
user: admin
secretRef:
name: ceph-secret
readOnly: false

2.通过创建pv、pvc挂载

在ceph集群上找到key:

[cephd@<your_ceph_machine> ~]$ ceph auth get-key client.admin | base64
QVFBNEhnNWJpQmN1RWhBQUhWSmJKZTVtOG9jWUdkNmlYMnA5dmc9PQ==

创建secret:

apiVersion: v1
kind: Secret
metadata:
name: ceph-secret
data:
key: QVFBNEhnNWJpQmN1RWhBQUhWSmJKZTVtOG9jWUdkNmlYMnA5dmc9PQ==

PV:

apiVersion: v1
kind: PersistentVolume
metadata:
name: cephfs-pv
spec:
capacity:
storage: 1Gi
accessModes:
– ReadWriteMany
cephfs:
monitors:
– <your_etcd_ip>:6789

path: /sns
user: admin
secretRef:
name: ceph-secret
readOnly: false
persistentVolumeReclaimPolicy: Recycle

PVC:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: cephfs-pv-claim
spec:
accessModes:
– ReadWriteMany
resources:
requests:
storage: 1Gi

创建POD:

apiVersion: v1
kind: Pod
metadata:
labels:
test: cephfs-pvc-pod
name: cephfs-pv-pod1
spec:
containers:
– name: cephfs-pv-busybox1
image: busybox
command: [“sleep”, “60000”]
volumeMounts:
– mountPath: “/mnt/cephfs”
name: cephfs-vol1
readOnly: false
volumes:
– name: cephfs-vol1
persistentVolumeClaim:
claimName: cephfs-pv-claim

遇到的问题:

1.映射到内核的时候报错RBD image feature set mismatch

http://blog.51cto.com/hipzz/1888048

–image-format format-id

format-id取值为1或2,默认为 2。

format 1 – 新建 rbd 映像时使用最初的格式。此格式兼容所有版本的 librbd 和内核模块,但是不支持较新的功能,像克隆。

format 2 – 使用第二版 rbd 格式, librbd 和 3.11 版以上内核模块才支持(除非是分拆的模块)。此格式增加了克隆支持,使得扩展更容易,还允许以后增加新功能。

解决方案1:

更改为格式1,重新映射。

注意:需要重新建立镜像。

[root@ceph1 ~]# rbd create block1 –image-format 1 –size 1024
rbd: image format 1 is deprecated
[root@ceph1 ~]# rbd ls
block1
block
[root@ceph1 ~]# rbd map block1
/dev/rbd0
[root@ceph1 ~]#

d.如上所示,映射正确。

解决方案2:

根据官网介绍,新建rbd默认格式2的rbd 块支持如下特性,并且默认全部开启:

–image-feature:

layering: 支持分层

striping: 支持条带化 v2

exclusive-lock: 支持独占锁

object-map: 支持对象映射(依赖 exclusive-lock )

fast-diff: 快速计算差异(依赖 object-map )

deep-flatten: 支持快照扁平化操作

journaling: 支持记录 IO 操作(依赖独占锁)

接下来尝试少开启一些特性:

[root@ceph1 ~]# rbd create block2 –image-feature layering –size 1024
[root@ceph1 ~]# rbd map block2
/dev/rbd1

2.创建pod挂载的时候遇到rbd: map failed executable file not found in $PATH

k8s集群内的节点上需要安装ceph-client:

yum install ceph–common

3.umount的时候出现target is busy

umount -l xxx

https://www.cnblogs.com/dkblog/archive/2012/07/18/2597192.html

https://blog.csdn.net/u012207077/article/details/21159339

4.如果k8s的node跟ceph集群的node不一样,则需要在k8s的node上部署ceph-common

yum install ceph-common

5.创建pod的时候提示,mount过去的时候提示libceph: bad option

k8s secret 认证 key 需要使用 base64 编码,有可能是secret文件里的key没有base64编码:

在ceph节点上ceph auth get-key client.admin |base64

填到secret文件里面。

6.如果mount fail,则去机器上查看kubelet的日志

7.多用户隔离

https://www.jianshu.com/p/96a34485f0fc

需要用pool,给user指定目录和权限,之后在pv中使用。

8.mount子目录

https://www.spinics.net/lists/ceph-devel/msg34698.html

mount -t ceph >> 172.24.0.4:6789:/volumes/kubernetes/test1 /tmp/mnt -o >> name=bar,secret=AQA+ln9Yfm6DKhAA10k7QkdkfIAKqmM6xeCsxA==

9.写入到共享存储的时候提示File Exists

目录权限问题,需要与Dockerfile中指定的USER的权限一样

HTTPS服务的Kubernetes ingress配置实践

在公有云被广泛接纳的今天,数据传输安全问题日益凸显,因为在公有云提供商的经典网络(二层互通)中,即便是内部网络通信也要考虑网络嗅探等hack手段,这也是公有云主推所谓“专用网络(二层隔离)”的原因之一。从应用的角度,我们应该尽量通过技术手段保证数据通信的安全性。而目前最常用的方式就是基于SSL/TLS的安全通信方式了,在七层,对应的就是https了。

这样,下面的仅在负载均衡/反向代理入口做加密通信的传统模型越来越无法满足数据安全性的需要了(nginx与backend service之间是基于明文的http通信):

传统安全通信模型:

client --- (via https) ---> nginx ---- (via http) ----> upstream backend services

我们需要下面的模型:

更为安全的通信模型:

client --- (via https) ---> nginx ---- (via https) ----> upstream backend services

在Kubernetes集群中,这种情况稍好些,首先,业务负载运行在集群的“虚拟网络”中,其次,一些K8s的网络插件实现是支持跨节点网络加密的(有一定的网络性能损耗),比如weave。但永远没有绝对的安全,作为业务应用的设计和实现人员,我们要尽可能的保证数据的通信安全,因此在面向七层的应用中,要尽可能的使用基于HTTPS的通信模型。本篇就来实践一下如何为Kubernetes集群内的HTTPS服务进行ingress的配置。

一. 例子概述与环境准备

在《实践kubernetes ingress controller的四个例子》一文中,我讲解了四种基本的kubernetes ingress配置方式。在这些例子中,有些例子的ingress controller(nginx)与backend service之间使用的是https,但client到ingress controller之间的通信却一直是基于http的。在本文中,我们的目标就是上面提到的那个更为安全的通信模型,即client与ingress controller(nginx)、nginx与backend service之间均使用的是https通信。这里在《实践kubernetes ingress controller的四个例子》一文例子的基础上,我们创建一个新的nginx ingress controller: nginx-ingress-controller-ic3,并将后端的svc7~svc9三个不同类型的服务暴露给client,如下图所示:

未分类

  • svc7: 是对传统通信模型的“复现”,即client与ingress controller(nginx)间采用https加密通信,但ingress controller(nginx)与svc7间则是明文的http通信;
  • svc8: 是ssl-termination的安全配置模型,即client与svc8的https通信分为“两段”,client与nginx建立https连接后,nginx将client提交的加密请求解密后,再向svc8发起https请求,并重新加密请求数据。这种client端ssl的过程在反向代理或负载均衡器终结的https通信方式被称为“ssl-termination”。
  • svc9: 是ssl-passthrough的安全配置模型,即nginx不会对client的https request进行解密,而是直接转发给backend的svc9服务,client端的ssl过程不会终结于nginx,而是在svc9对应的pod中终结。这种https通信方式被称为”ssl-passthrough”。这种配置模型尤其适合backend service对client端进行client certificate验证的情况,同时也降低了nginx加解密的性能负担。

本文基于下面环境进行实验:kubernetes 1.10.3、weave networks 2.3.0、nginx-ingress-controller:0.15.0。关于本文涉及的例子的源码、chart包以及ingress controllers的yaml源文件可以在这里下载到。

二. 建立新的ingress-nginx-controller:nginx-ingress-controller-ic3

为了更好地进行例子说明,我们建立一个新的ingress-nginx-controller:nginx-ingress-controller-ic3,svc7~svc9都通过该ingress controller进行服务入口的暴露管理。要创建nginx-ingress-controller-ic3,我们首先需要在ic-common.yaml中为Role: nginx-ingress-role添加一个resourceName: “ingress-controller-leader-ic3″,并apply生效:

// ic-common.yaml
... ...
    resourceNames:
      # Defaults to "<election-id>-<ingress-class>"
      # Here: "<ingress-controller-leader>-<nginx>"
      # This has to be adapted if you change either parameter
      # when launching the nginx-ingress-controller.
      - "ingress-controller-leader-ic1"
      - "ingress-controller-leader-ic2"
      - "ingress-controller-leader-ic3"
... ...

# kubectl apply -f ic-common.yaml

我们为nginx-ingress-controller-ic3创建nodeport service,新nodeport为:30092:

// ic3-service-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx-ic3
  namespace: ingress-nginx-demo
spec:
  type: NodePort
  ports:
  - name: https
    port: 443
    targetPort: 443
    nodePort: 30092
    protocol: TCP
  selector:
    app: ingress-nginx-ic3

注意:ingress-nginx-ic3 service的nodeport映射到ic3 ingress controller的443端口,也就是支持安全通信的端口,而不是明文的80端口。

最后创建nginx-ingress-controller-ic3 pod,可以复制一份ic2-mandatory.yaml,然后将内容中的ic2全部修改为ic3即可:

# kubectl apply -f ic3-mandatory.yaml

如无意外,nginx-ingress-controller-ic3应该已经正常地运行在你的k8s cluster中了。

三. svc7: 使用ssl termination,但nginx与backend服务之间采用明文传输(http)

加密Web流量有两个主要配置方案:SSL termination和SSL passthrough。

使用SSL termination时,客户端的SSL请求在负载均衡器/反向代理中解密,解密操作将增加负载均衡器的工作负担,较为耗费CPU,但简化了SSL证书的管理。至于负载均衡器和后端之间的流量是否加密,需要nginx另行配置。

SSL Passthrough,意味着client端将直接将SSL连接发送到后端(backend)。与SSL termination不同,请求始终保持加密,并且解密负载分布在后端服务器上。但是,这种情况的SSL证书管理略复杂,证书必须在每台服务器上自行管理。另外,在这种方式下可能无法添加或修改HTTP header,可能会丢失X-forwarded-* header中包含的客户端的IP地址,端口和其他信息。

我们先来看一种并不那么“安全”的“传统模型”:在nginx上暴露https,但nginx到backend service(svc7)采用http。

我们先来创建相关的密钥和公钥证书,并以一个Secret:ingress-controller-demo-tls-secret存储密钥和证书数据:

// ingress-controller-demo/manifests下面

# openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ic3.key -out ic3.crt -subj "/CN=*.tonybai.com/O=tonybai.com"
# kubectl create secret tls ingress-controller-demo-tls-secret --key  ic3.key --cert ic3.crt

svc7几乎是和svc1一样的程序(输出的字符串标识不同),但svc7的ingress与svc1大不相同,因为我们需要通过https访问svc7的ingress:

// svc7的values.yaml
... ...
replicaCount: 1

image:
  repository: bigwhite/ingress-controller-demo-svc7
  tag: v0.1
  pullPolicy: Always

service:
  type: ClusterIP
  port: 443

ingress:
  enabled: true
  annotations:
    kubernetes.io/ingress.class: ic3
  path: /
  hosts:
    - svc7.tonybai.com
  tls:
    - secretName: ingress-controller-demo-tls-secret
      hosts:
        - svc7.tonybai.com
... ...

与svc1的values.yaml不同的是,我们使用的ingress controller是ic3,我们开启了tls,secret用的就是我们上面创建的那个secret:ingress-controller-demo-tls-secret。创建ic3-svc7后,我们看到ingress controller内部的nginx.conf中有关svc7的配置输出如下:

# kubectl exec nginx-ingress-controller-ic3-67f7cf7845-2tnc9 -n ingress-nginx-demo -- cat /etc/nginx/nginx.conf

        # map port 442 to 443 for header X-Forwarded-Port
        map $pass_server_port $pass_port {
                442              443;
                default          $pass_server_port;
        }

        upstream default-ic3-svc7-http {
                least_conn;

                keepalive 32;

                server 192.168.28.13:8080 max_fails=0 fail_timeout=0;

        }

## start server svc7.tonybai.com
        server {
                server_name svc7.tonybai.com ;

                listen 80;

                listen [::]:80;

                set $proxy_upstream_name "-";

                listen 442 proxy_protocol   ssl http2;

                listen [::]:442 proxy_protocol  ssl http2;

                # PEM sha: 248951b75535e0824c1a7f74dc382be3447057b7
                ssl_certificate                         /ingress-controller/ssl/default-ingress-controller-demo-tls-secret.pem;
                ssl_certificate_key                     /ingress-controller/ssl/default-ingress-controller-demo-tls-secret.pem;

                ssl_trusted_certificate                 /ingress-controller/ssl/default-ingress-controller-demo-tls-secret-full-chain.pem;
                ssl_stapling                            on;
                ssl_stapling_verify                     on;

                location / {
                        ... ...
                        proxy_pass http://default-ic3-svc7-http;

                        proxy_redirect                          off;

                }
           ... ...
        }
        ## end server svc7.tonybai.com

可以看到30092(nodeport) 映射的ingress controller的443端口在svc7.tonybai.com这个server域名下已经有了ssl标识,并且ssl_certificate和ssl_certificate_key对应的值就是我们之前创建的ingress-controller-demo-tls-secret。

我们通过curl访问以下svc7服务:

# curl -k https://svc7.tonybai.com:30092
Hello, I am svc7 for ingress-controller demo!

此时,如果再用http方式去访问svc7,你会得到下面错误结果:

# curl http://svc7.tonybai.com:30092
<html>
<head><title>400 The plain HTTP request was sent to HTTPS port</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<center>The plain HTTP request was sent to HTTPS port</center>
<hr><center>nginx/1.13.12</center>
</body>
</html>

四. svc8: 使用ssl termination,但nginx与backend服务之间采用加密传输(https)

前面说过,SSL termination配置场景中,负载均衡器和后端之间的流量是否加密,需要nginx另行配置。svc7采用了未加密的方式,nginx -> backend service存在安全风险,我们要将其改造为也通过https进行数据加密传输,于是有了svc8这个例子。

svc8对应的程序本身其实是上一篇文章《实践kubernetes ingress controller的四个例子》中的svc2的clone(唯一修改就是输出的log中的标识)。

在svc8对应的chart中,我们将values.yaml改为:

// ingress-controller-demo/charts/svc8/values.yaml

replicaCount: 1

image:
  repository: bigwhite/ingress-controller-demo-svc8
  tag: v0.1
  pullPolicy: Always

service:
  type: ClusterIP
  port: 443

ingress:
  enabled: true
  annotations:
    # kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/secure-backends: "true"
    kubernetes.io/ingress.class: ic3
  path: /
  hosts:
    - svc8.tonybai.com
  tls:
    - secretName: ingress-controller-demo-tls-secret
      hosts:
        - svc8.tonybai.com

... ...

与svc7不同点在于values.yaml中的新annotation: nginx.ingress.kubernetes.io/secure-backends: “true”。这个annotation让nginx以https的方式去访问backend service: svc8。安装svc8 chart后,ingress nginx controller为svc8生成的配置如下:

## start server svc8.tonybai.com
        server {
                server_name svc8.tonybai.com ;

                listen 80;

                listen [::]:80;

                set $proxy_upstream_name "-";

                listen 442 proxy_protocol   ssl http2;

                listen [::]:442 proxy_protocol  ssl http2;

                # PEM sha: 248951b75535e0824c1a7f74dc382be3447057b7
                ssl_certificate                         /ingress-controller/ssl/default-ingress-controller-demo-tls-secret.pem;
                ssl_certificate_key                     /ingress-controller/ssl/default-ingress-controller-demo-tls-secret.pem;

                ssl_trusted_certificate                 /ingress-controller/ssl/default-ingress-controller-demo-tls-secret-full-chain.pem;
                ssl_stapling                            on;
                ssl_stapling_verify                     on;

                location / {
                     ... ...
                        proxy_pass https://default-ic3-svc8-https;

                        proxy_redirect                          off;

                }

        }
        ## end server svc8.tonybai.com

        upstream default-ic3-svc8-https {
                least_conn;

                keepalive 32;

                server 192.168.28.14:8080 max_fails=0 fail_timeout=0;

        }

使用curl访问svc8服务(-k: 忽略对server端证书的校验):

# curl -k https://svc8.tonybai.com:30092
Hello, I am svc8 for ingress-controller demo!

五. svc9: 使用ssl passthrough, termination at pod

某些服务需要通过对client端的证书进行校验的方式,进行身份验证和授权,svc9就是这样一个对client certification进行校验的双向https校验的service。针对这种情况,ssl termination的配置方法无法满足需求,我们需要使用ssl passthrough的方案。

在ingress nginx controller开启ssl passthrough方案需要在ingress controller和ingress中都做一些改动。

首先我们需要为nginx-ingress-controller-ic3添加一个新的命令行参数:–enable-ssl-passthrough,并重新apply生效:

// ic3-mandatory.yaml
... ...
spec:
      serviceAccountName: nginx-ingress-serviceaccount
      containers:
        - name: nginx-ingress-controller-ic3
          image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.15.0
          args:
            - /nginx-ingress-controller
            - --default-backend-service=$(POD_NAMESPACE)/default-http-backend
            - --configmap=$(POD_NAMESPACE)/nginx-configuration-ic3
            - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services-ic3
            - --udp-services-configmap=$(POD_NAMESPACE)/udp-services-ic3
            - --publish-service=$(POD_NAMESPACE)/ingress-nginx-ic3
            - --annotations-prefix=nginx.ingress.kubernetes.io
            - --enable-ssl-passthrough
            - --ingress-class=ic3
... ...

然后在svc9的chart中,为ingress添加新的annotation
nginx.ingress.kubernetes.io/ssl-passthrough: “true”

// ingress-controller-demo/charts/svc9/values.yaml

replicaCount: 1

image:
  repository: bigwhite/ingress-controller-demo-svc9
  tag: v0.1
  pullPolicy: Always

service:
  type: ClusterIP
  port: 443

ingress:
  enabled: true
  annotations:
    kubernetes.io/ingress.class: ic3
    nginx.ingress.kubernetes.io/ssl-passthrough: "true"

  path: /
  hosts:
    - svc9.tonybai.com
  tls:
    - secretName: ingress-controller-demo-tls-secret
      hosts:
        - svc9.tonybai.com
... ...

isntall svc9 chart之后,我们用curl来访问以下svc9:

# curl -k  https://svc9.tonybai.com:30092
curl: (35) gnutls_handshake() failed: Certificate is bad

由于svc9程序对client端的certificate进行验证,没有提供client certificate的curl请求被拒绝了!svc9 pod的日志也证实了这一点:

2018/06/25 05:36:29 http: TLS handshake error from 192.168.31.10:38634: tls: client didn't provide a certificate

我们进入到ingress-controller-demo/src/svc9/client路径下,执行:

# curl -k --key ./client.key --cert ./client.crt https://svc9.tonybai.com:30092
Hello, I am svc9 for ingress-controller demo!

带上client.crt后,svc9通过了验证,返回了正确的应答。

client路径下是一个svc9专用的客户端,我们也可以执行该程序去访问svc9:

# go run client.go
Hello, I am svc9 for ingress-controller demo!

我们再看看采用ssl-passthrough方式下ingress-nginx controller的访问日志,当curl请求发出时,ingress-nginx controller并未有日志输出,因为没有在nginx处ssl termnination,从此也可以证实:nginx将client的ssl过程转发到pod中去了,即passthrough了。

51短信平台:企业级短信平台定制开发专家 https://51smspush.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

实践kubernetes ingress controller的四个例子

我之前并未使用过标准的Kubernetes ingress,而是自己实现了一个基于nginx的、类似ingress controller的服务入口管理程序nginx-kit。这个程序会部署到Kubernetes集群中,以Pod形式运行。该Pod由两个Container组成,一个Container放置了一个由脚本启动的nginx;另外一个Container中放置的是一个conf generator程序,它监听Kubernetes集群service对象的变更,并根据变更情况动态生成nginx的配置文件。第一个Container中的脚本会监听配置文件目录的变化,并reload配置文件信息实现Kubernetes内部服务对外暴露入口的动态管理。关于这个程序的详情可以参考我之前写的两篇文章:《Kubernetes集群中的Nginx配置热更新方案》和《为Kubernetes集群中服务部署Nginx入口服务》。

近期在使用ingress controller对内部服务入口的暴露进行动态管理,使用后发现我之前实现的nginx kit与ingress controller的实现之一: ingress-nginx简直是异曲同工。只是当时对Kubernetes理解还不够深入,在设计nginx-kit时格局“太小了”,只实现了一个满足内部需求的”ingress controller”,而不是一个通用的、可扩展的ingress controller:(。

好了!言归正传,这篇文章是ingress的入门文章,将通过四个例子来说明一下ingress controller的实现之一: ingress-nginx在不同服务暴露场景下的使用和配置方法。

一. 例子概述与环境准备

我们有四个例子,见下图中的a) ~ d):

未分类

  • 例子a): 单ingress-nginx controller。通过ingress-svc1将内部服务svc1的http服务端口暴露到集群外,通过访问http://svc1.tonybai.com:30090即可访问svc1服务。
  • 例子b):单ingress-nginx controller。通过ingress-svc1将内部服务svc1的http服务端口暴露到集群外,通过访问http://svc1.tonybai.com:30090即可访问svc1服务;通过ingress-svc2将内部服务svc2的https服务端口暴露到集群外,通过访问http://svc2.tonybai.com:30090即可访问svc2服务。
  • 例子c):单ingress-nginx controller。除了暴露svc1和svc2之外,还暴露了集群内部的一个tcp(四层)服务:svc3,通过tcp连接svc3.tonybai.com:30070即可访问svc3服务。
  • 例子d): 多ingress-nginx controllers。其中nginx-ingress-controller-ic1负责暴露svc1、svc2和svc3服务(访问方式如上面所描述的);nginx-ingress-controller-ic2负责暴露svc4、svc5和svc6,其中svc4是一个http服务;svc5是https服务,svc6是一个tcp(四层)服务。
    这里我们使用一个Kubernetes 1.10.3的集群来循序渐进地实践一下这四个例子。关于这四个例子的源码、chart包以及ingress controllers的yaml源文件在这里可以下载到:
$tree -L 2 ingress-controller-demo
ingress-controller-demo
├── charts
│   ├── svc1
│   ├── svc2
│   ├── svc3
│   ├── svc4
│   ├── svc5
│   └── svc6
├── manifests
│   ├── ic-common.yaml
│   ├── ic1-mandatory.yaml
│   ├── ic1-service-nodeport.yaml
│   ├── ic2-mandatory.yaml
│   └── ic2-service-nodeport.yaml
└── src
    ├── svc1
    ├── svc2
    ├── svc3
    ├── svc4
    ├── svc5
    └── svc6

其中:

  • src下面存放着svc1~svc6的源码(包括Dockerfile);
  • manifests下面存放的是ingress controllers的yaml源文件;
  • charts下面存放的是svc1~svc6的helm chart安装包源文件。

二. 创建第一个ingress-nginx controller

ingress controller有多种实现,其中应用较广的是kubernetes官方仓库中的ingress-nginx。在bare metal上安装ingress-nginx controller十分方便,只需执行下面命令即可:

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/mandatory.yaml

不过,考虑到我后续在环境中会安装多个ingress-nginx controller,我们需要对mandatory.yaml中的内容做些调整:

  • 首先明确多个ingress-nginx controller及其相关kubernetes object所在的namespace,默认为ingress-nginx,这里统一改为ingress-nginx-demo,yaml描述文件中所有的object的namespace也都改为ingress-nginx-demo,clusterrole、clusterrolebinding对象不归属于任何namespace,因此无需修改;

  • 接下来,将多个ingress-nginx controller能共用的kubernetes object的描述数据从mandatory.yaml中提取出来,放入ic-common.yaml中,包括:namespace: ingress-nginx-demo、deployment: default-http-backend、service: default-http-backend、serviceaccount: nginx-ingress-serviceaccount、clusterrole: nginx-ingress-demo-clusterrole、role: nginx-ingress-role、rolebinding: nginx-ingress-role-nisa-binding以及clusterrolebinding: nginx-ingress-demo-clusterrole-nisa-binding;

  • 将“缩水”后的mandatory.yaml改名为ic1-mandatory.yaml,并将其内容中的kubernetes object的name添加上“-ic1″后缀。

  • 在ic1-mandatory.yaml中nginx-ingress-controller的启动参数列表尾部添加“–ingress-class=ic1”:

// ic1-mandatory.yaml
... ...
    spec:
      serviceAccountName: nginx-ingress-serviceaccount
      containers:
        - name: nginx-ingress-controller-ic1
          image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.15.0
          args:
            - /nginx-ingress-controller
            - --default-backend-service=$(POD_NAMESPACE)/default-http-backend
            - --configmap=$(POD_NAMESPACE)/nginx-configuration-ic1
            - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services-ic1
            - --udp-services-configmap=$(POD_NAMESPACE)/udp-services-ic1
            - --publish-service=$(POD_NAMESPACE)/ingress-nginx-ic1
            - --annotations-prefix=nginx.ingress.kubernetes.io
            - --ingress-class=ic1
... ...
  • ic-common.yaml中的nginx-ingress-role中的resourceNames列表中需添加两项:”ingress-controller-leader-ic1″和”ingress-controller-leader-ic2″:
// ic-common.yaml
... ...
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
  name: nginx-ingress-role
  namespace: ingress-nginx-demo
rules:
  - apiGroups:
      - ""
    resources:
      - configmaps
      - pods
      - secrets
      - namespaces
    verbs:
      - get
  - apiGroups:
      - ""
    resources:
      - configmaps
    resourceNames:
      # Defaults to "<election-id>-<ingress-class>"
      # Here: "<ingress-controller-leader>-<nginx>"
      # This has to be adapted if you change either parameter
      # when launching the nginx-ingress-controller.
      - "ingress-controller-leader-ic1"
      - "ingress-controller-leader-ic2"
... ...

这两个resouceName分别给两个ingress-controller使用,当每个ingress-controller存在多副本(replicas > 1)时,多副本会通过ingress-controller-leader-icX这个configmap资源来进行leader election(选主)。以ingress-controller-ic1为例,当存在多副本时,ingress-controller-ic1的启动日志:

I0621 09:13:20.646426       7 stat_collector.go:34] changing prometheus collector from  to default
I0621 09:13:20.648198       7 status.go:196] new leader elected: nginx-ingress-controller-ic1-7c9bc49cbb-kgjvz
I0621 09:13:20.752485       7 controller.go:177] ingress backend successfully reloaded...

不过,虽然存在leader,但业务流量却是负载分担的。

  • 为ingress-nginx controller pod创建nodeport类型service

如果只是部署了ingress controller,那么外部依然无法连上ingress controller,因为ingress controller自身还没有对应的service将自己暴露到集群外部。官方文档推荐使用NodePort方式,于是我们创建了ic1-service-nodeport.yaml,让流入host:30090的流量进入ingress controller service。

总结一下ingress-controller-ic1这个ingress controller的完整创建步骤:

kubectl apply -f ic-common.yaml
kubectl apply -f ic1-service-nodeport.yaml
kubectl apply -f ic1-mandatory.yaml

三. 创建例子a

svc1是一个在容器8080端口提供http服务的服务程序。在例子a)中,我们在k8s集群中创建svc1,并创建ic1-svc1 ingress将svc1暴露在集群外面,外部请求通过svc1.tonybai.com:30090可以访问到svc1。而做到这一点,我们仅需要使用helm install一下svc1这个chart:

# helm install --name ic1-svc1 ./svc1
NAME:   ic1-svc1
LAST DEPLOYED: Thu Jun 21 20:39:25 2018
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/Service
NAME      TYPE       CLUSTER-IP      EXTERNAL-IP  PORT(S)  AGE
ic1-svc1  ClusterIP  10.103.210.182  <none>       80/TCP   0s

==> v1beta2/Deployment
NAME      DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
ic1-svc1  1        0        0           0          0s

==> v1beta1/Ingress
NAME      HOSTS             ADDRESS  PORTS  AGE
ic1-svc1  svc1.tonybai.com  80       0s

==> v1/Pod(related)
NAME                       READY  STATUS             RESTARTS  AGE
ic1-svc1-5ff84d7bff-5j7tb  0/1    ContainerCreating  0         0s

NOTES:
1. Get the application URL by running these commands:

http://svc1.tonybai.com/

svc1服务以及对应的ic1-svc1 ingress创建后,我们来测试一下:

# curl svc1.tonybai.com:30090
Hello, I am svc1 for ingress-controller demo!

结果符合预期。而这一切实现的关键在于ingress-controller-demo/charts/svc1/values.yaml:

... ...
ingress:
  enabled: true
  annotations:
    # kubernetes.io/ingress.class: nginx
    # kubernetes.io/tls-acme: "true"
    kubernetes.io/ingress.class: ic1
  path: /
  hosts:
    - svc1.tonybai.com
... ...

ingress的enabled改为true,helm才会创建svc1对应的ingress。annotations中的kubernetes.io/ingress.class: ic1很关键,设定ingress的这个annotation,可以使得该ingress归属于我们上面创建的nginx-ingress-controller-ic1 ingress controller,而其他ingress controller会忽略这个ingress。

我们再来看看 ingress-controller-ic1的后台日志,当添加svc1时,日志输出:

I0621 12:39:25.406331       7 event.go:218] Event(v1.ObjectReference{Kind:"Ingress", Namespace:"default", Name:"ic1-svc1", UID:"2176416f-7550-11e8-a0e8-00163e0cd764", APIVersion:"extensions", ResourceVersion:"1877656", FieldPath:""}): type: 'Normal' reason: 'CREATE' Ingress default/ic1-svc1
I0621 12:39:25.517915       7 controller.go:177] ingress backend successfully reloaded...
W0621 12:39:28.739708       7 controller.go:773] service default/ic1-svc1 does not have any active endpoints
I0621 12:39:34.262824       7 controller.go:168] backend reload required
I0621 12:39:34.371479       7 controller.go:177] ingress backend successfully reloaded...
nginx-ingress-controller-ic1会监听到service变化,并reload nginx。

我们可以通过下面命令查看nginx-ingress-controller-ic1内部的nginx的配置文件内容:

# kubectl exec nginx-ingress-controller-ic1-7c9bc49cbb-kgjvz -n ingress-nginx-demo -- cat /etc/nginx/nginx.conf

我们可以看到有关svc1的相关内容如下:

        upstream default-ic1-svc1-http {
                least_conn;

                keepalive 32;

                server 192.168.31.9:8080 max_fails=0 fail_timeout=0;

        }

        ## start server svc1.tonybai.com
        server {
                server_name svc1.tonybai.com ;

                listen 80;

                listen [::]:80;

                set $proxy_upstream_name "-";

                location / {

                       ... ...

                        set $proxy_upstream_name "default-ic1-svc1-http";

                        set $namespace      "default";
                        set $ingress_name   "ic1-svc1";
                        set $service_name   "ic1-svc1";

                       ... ...

                        proxy_pass http://default-ic1-svc1-http;

                        proxy_redirect                          off;

                }

        }
        ## end server svc1.tonybai.com

可一看出外部到svc1.tonybai.com:30090的流量被转到service ingress-nginx-ic1:80上,进而到达nginx pod的targetPort(80)上。

四. 创建例子b

有了例子a)作为基础,理解接下来的例子就相对简单了。例子b)与a)最大的不同是svc2是一个https服务。外部通过http协议访问:svc2.tonybai.com:30090后,nginx-ingress-controller-ic1内部的nginx需要以https的方式去访问svc2。ingress-nginx ingress controller支持这种情况,仅需要在svcb的ingress annotations加上下面这个annotation:nginx.ingress.kubernetes.io/secure-backends: “true”:

// ingress-controller-demo/charts/svc2/values.yaml
... ...
ingress:
  enabled: true
  annotations:
    # kubernetes.io/ingress.class: nginx
    # kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/secure-backends: "true"
    kubernetes.io/ingress.class: ic1
  path: /
  hosts:
    - svc2.tonybai.com
 ... ...

和例子a)一样,使用helm安装svc2这个chart后,svc2这个服务就暴露出来了:

# helm install --name ic1-svc2 ./svc2

# curl http://svc2.tonybai.com:30090
Hello, I am svc2 for ingress-controller demo!

五. 创建例子c

svc3与前面两个服务均不同,因为它直接暴露的是四层的tcp服务。kubernetes ingress无法直接支持四层的服务端口暴露,我们需要在ingress controller上“动手脚”。

首先,四层的暴露的端口不能与之前的七层端口30090重叠(因为不是通过ingress来暴露svc3服务的),我们需要一个新端口:30070,我们需要在ic1-service-nodeport.yaml中增加一组nodeport:

//ingress-controller-demo/manifests/ic1-service-nodeport.yaml

apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx-ic1
  namespace: ingress-nginx-demo
spec:
  type: NodePort
  ports:
  - name: http
    port: 80
    targetPort: 80
    nodePort: 30090
    protocol: TCP
  - name: tcp
    port: 30070
    targetPort: 30070
    nodePort: 30070
    protocol: TCP
  selector:
    app: ingress-nginx-ic1

注意这里两组nodeport中的port不能一样,否则kubernetes会用下面的一组覆盖上面的那组。这里我们暴露30070这个nodeport,service的集群内port也是30070,后面的endpoint中的容器(即nginx-ingress-controller-ic1 pod)监听的也是30070。

接下来,要让nginx-ingress-controller-ic1 pod也监听30070,我们没法用ingress实现,但是ingress-nginx ingress controller支持通过一个名为:tcp-services-ic1的configmap来配置:

//ingress-controller-demo/manifests/ic1-mandatory.yaml
.... ...
spec:
      serviceAccountName: nginx-ingress-serviceaccount
      containers:
        - name: nginx-ingress-controller-ic1
          image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.15.0
          args:
            - /nginx-ingress-controller
            - --default-backend-service=$(POD_NAMESPACE)/default-http-backend
            - --configmap=$(POD_NAMESPACE)/nginx-configuration-ic1
            - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services-ic1
            - --udp-services-configmap=$(POD_NAMESPACE)/udp-services-ic1
            - --publish-service=$(POD_NAMESPACE)/ingress-nginx-ic1
            - --annotations-prefix=nginx.ingress.kubernetes.io
... ...

在ic1-mandatory.yaml中,我们这样更新tcp-services-ic1 configmap的配置:

kind: ConfigMap
apiVersion: v1
metadata:
  name: tcp-services-ic1
  namespace: ingress-nginx-demo
data:
  30070: "default/ic1-svc3:8080"

大家可以看到,在configmap的data中,我们用了一个key:value的格式行,其中key就是nginx要暴露的端口:30070,value则为

<namespace/service name>:<service port>

格式的值,这里我们使用default名字空间下的ic1-svc3服务,服务端口8080。

重新apply ic1-mandatory.yaml和ic1-service-nodeport.yaml后,我们测试一下svc3服务:

# telnet svc3.tonybai.com 30070
Trying 127.0.0.1...
Connected to svc3.tonybai.com.
Escape character is '^]'.
hello
hello
world
world

svc3是一个echo服务,我们看到svc3 echo了我们输入的内容。

在nginx内部,30070是这样被暴露的:

stream {
        log_format log_stream [$time_local] $protocol $status $bytes_sent $bytes_received $session_time;

        access_log /var/log/nginx/access.log log_stream;

        error_log  /var/log/nginx/error.log;

        # TCP services

        upstream tcp-30070-default-ic1-svc3-8080 {

                server                  192.168.28.13:8080;

        }
        server {

                listen                  30070;

                listen                  [::]:30070;

                proxy_timeout           600s;
                proxy_pass              tcp-30070-default-ic1-svc3-8080;

        }

        # UDP services
}

六. 创建例子d

在例子d)对应的图示中,我们建立了另外一个ingress-nginx ingress controller: nginx-ingress-controller-ic2,与nginx-ingress-controller-ic1 不同的是, nginx-ingress-controller-ic2的启动参数中含:

            - --ingress-class=ic2

用以区分ic1。ic2-mandatory.yaml和ic1-mandatory.yaml相比,就是将“rc1”字样整体替换为”ic2″即可。除此之外,有了ic1-service-nodeport.yaml的基础,ic2-service-nodeport.yaml内容也是“雷同”的。建立 nginx-ingress-controller-ic2步骤如下:

# kubectl apply -f ic2-service-nodeport.yaml
# kubectl apply -f ic2-mandatory.yaml

归属于nginx-ingress-controller-ic2的三个服务svc4、svc5和svc6等价于nginx-ingress-controller-ic1的svc1、svc2和svc3,这里就不赘述了。

# curl svc4.tonybai.com:30091
Hello, I am svc4 for ingress-controller demo!
# curl svc5.tonybai.com:30091
Hello, I am svc5 for ingress-controller demo!
# telnet  svc6.tonybai.com 30071
Trying 127.0.0.1...
Connected to svc6.tonybai.com.
Escape character is '^]'.
hello
hello
tony
tony

如果想使得ingress-nginx controller高可用,只需将其pod副本数量调大即可。

盘点Kubernetes网络问题的4种解决方案

由于在企业中部署私有云的场景会更普遍,所以在私有云中运行Kubernetes + Docker集群之前,就需要自己搭建符合Kubernetes要求的网络环境。现在的开源世界里,有很多开源组件可以帮助我们打通Docker容器和容器之间的网络,实现Kubernetes要求的网络模型。当然每种方案都有自己适合的场景,我们要根据自己的实际需要进行选择。

一、Kubernetes + Flannel

Kubernetes的网络模型假定了所有Pod都在一个可以直接连通的扁平的网络空间中,这在GCE(Google Compute Engine)里面是现成的网络模型,Kubernetes假定这个网络已经存在。而在私有云里搭建Kubernetes集群,就不能假定这个网络已经存在了。我们需要自己实现这个网络假设,将不同节点上的Docker容器之间的互相访问先打通,然后运行Kubernetes。

Flannel是CoreOS团队针对Kubernetes设计的一个网络规划服务,简单来说,它的功能是让集群中的不同节点主机创建的Docker容器都具有全集群唯一的虚拟IP地址。而且它还能在这些IP地址之间建立一个覆盖网络(Overlay Network),通过这个覆盖网络,将数据包原封不动地传递到目标容器内。

下面是一张它的网络原理图:

未分类

可以看到,Flannel首先创建了一个名为flannel0的网桥,而且这个网桥的一端连接docker0的网桥,另一端连接一个名为flanneld的服务进程。

Flanneld进程并不简单,它首先上连etcd,利用etcd来管理可分配的IP地址段资源,同时监控etcd中每个Pod的实际地址,并在内存中建立了一个Pod节点路由表;然后下连docker0和物理网络,使用内存中的Pod节点路由表,将docker0发给它的数据包包装起来,利用物理网络的连接将数据包投递到目标flanneld上,从而完成pod到pod之间的直接的地址通信。

Flannel之间的底层通信协议的可选余地有很多,比如UDP、VXlan、AWS VPC等等。只要能通到对端的Flannel就可以了。源Flannel封包,目标Flannel解包,最终docker0看到的就是原始的数据,非常透明,根本感觉不到中间Flannel的存在。

Flannel的安装配置网上讲的很多,在这里就不在赘述了。在这里注意一点,就是flannel使用etcd作为数据库,所以需要预先安装好etcd。

下面说说几个场景:

  1. 同一Pod内的网络通信。在同一个Pod内的容器共享同一个网络命名空间,共享同一个Linux协议栈。所以对于网络的各类操作,就和它们在同一台机器上一样,它们可以用localhost地址直接访问彼此的端口。其实这和传统的一组普通程序运行的环境是完全一样的,传统的程序不需要针对网络做特别的修改就可以移植了。这样做的结果是简单、安全和高效,也能减少将已经存在的程序从物理机或者虚拟机移植到容器下运行的难度。

  2. Pod1到Pod2的网络,分两种情况。Pod1与Pod2不在同一台主机与Pod1与Pod2在同一台主机。

  • 先说Pod1与Pod2不在同一台主机。Pod的地址是与docker0在同一个网段的,但docker0网段与宿主机网卡是两个完全不同的IP网段,并且不同Node之间的通信只能通过宿主机的物理网卡进行。将Pod的IP和所在Node的IP关联起来,通过这个关联让Pod可以互相访问。
  • Pod1与Pod2在同一台主机。Pod1和Pod2在同一台主机的话,由Docker0网桥直接转发请求到Pod2,不需要经过Flannel。
  1. Pod到Service的网络。创建一个Service时,相应会创建一个指向这个Service的域名,域名规则为{服务名}.{namespace}.svc.{集群名称}。之前Service IP的转发由iptables和kube-proxy负责,目前基于性能考虑,全部为iptables维护和转发。iptables则由kubelet维护。Service仅支持UDP和TCP协议,所以像ping的ICMP协议是用不了的,所以无法ping通Service IP。

  2. Pod到外网。Pod向外网发送请求,查找路由表, 转发数据包到宿主机的网卡,宿主网卡完成路由选择后,iptables执行Masquerade,把源IP更改为宿主网卡的IP,然后向外网服务器发送请求。

  3. 集群外部访问Pod或Service

由于Pod和Service是Kubernetes集群范围内的虚拟概念,所以集群外的客户端系统无法通过Pod的IP地址或者Service的虚拟IP地址和虚拟端口号访问到它们。为了让外部客户端可以访问这些服务,可以将Pod或Service的端口号映射到宿主机,以使得客户端应用能够通过物理机访问容器应用。

总结:Flannel实现了对Kubernetes网络的支持,但是它引入了多个网络组件,在网络通信时需要转到flannel0网络接口,再转到用户态的flanneld程序,到对端后还需要走这个过程的反过程,所以也会引入一些网络的时延损耗。另外Flannel默认的底层通信协议是UDP。UDP本身是非可靠协议,虽然两端的TCP实现了可靠传输,但在大流量、高并发应用场景下还需要反复调试,确保不会出现传输质量的问题。特别是对网络依赖重的应用,需要评估对业务的影响。

二、基于Docker Libnetwork的网络定制

容器跨主机的网络通信,主要实现思路有两种:二层VLAN网络和Overlay网络。

  • 二层VLAN网络的解决跨主机通信的思路是把原先的网络架构改造为互通的大二层网络,通过特定网络设备直接路由,实现容器点到点的之间通信。
  • Overlay网络是指在不改变现有网络基础设施的前提下,通过某种约定通信协议,把二层报文封装在IP报文之上的新的数据格式。

Libnetwork是Docker团队将Docker的网络功能从Docker核心代码中分离出去,形成一个单独的库。 Libnetwork通过插件的形式为Docker提供网络功能。 使得用户可以根据自己的需求实现自己的Driver来提供不同的网络功能。

Libnetwork所要实现的网络模型基本是这样的: 用户可以创建一个或多个网络(一个网络就是一个网桥或者一个VLAN ),一个容器可以加入一个或多个网络。 同一个网络中容器可以通信,不同网络中的容器隔离。这才是将网络从docker分离出去的真正含义,即在创建容器之前,我们可以先创建网络(即创建容器与创建网络是分开的),然后决定让容器加入哪个网络。

Libnetwork实现了5种网络模式:

  • bridge:Docker默认的容器网络驱动,Container通过一对veth pair链接到docker0网桥上,由Docker为容器动态分配IP及配置路由、防火墙等。
  • host:容器与主机共享同一Network Namespace。
  • null:容器内网络配置为空,需要用户手动为容器配置网络接口及路由。
  • remote:Docker网络插件的实现,Remote driver使得Libnetwork可以通过HTTP Resful API 对接第三方的网络方案,类似于SocketPlane的SDN方案只要实现了约定的HTTP URL处理函数以及底层的网络接口配置方法,就可以替代Docker原生的网络实现。
  • overlay:Docker原生的跨主机多子网网络方案。

未分类

Docker自身的网络功能比较简单,不能满足很多复杂的应用场景。因此,有很多开源项目用来改善Docker的网络功能,如Pipework、Weave、SocketPlane等。

举例:网络配置工具Pipework

Pipework是一个简单易用的Docker容器网络配置工具。由200多行shell脚本实现。通过使用IP、brctl、ovs-vsctl等命令来为Docker容器配置自定义的网桥、网卡、路由等。有如下功能:

  • 支持使用自定义的Linux Bridge、veth pair为容器提供通信。
  • 支持使用MacVLAN设备将容器连接到本地网络。
  • 支持DHCP获取容器的IP。
  • 支持Open vSwitch。
  • 支持VLAN划分。

Pipework简化了在复杂场景下对容器连接的操作命令,为我们配置复杂的网络拓扑提供了一个强有力的工具。对于一个基本应用而言,Docker的网络模型已经很不错了。然而,随着云计算和微服务的兴起,我们不能永远停留在使用基本应用的级别上,我们需要性能更好且更灵活的网络功能。Pipework是个很好的网络配置工具,但Pipework并不是一套解决方案,我们可以利用它提供的强大功能,根据自己的需求添加额外的功能,帮助我们构建自己的解决方案。

OVS跨主机多子网网络方案

OVS的优势是,作为开源虚拟交换机软件,它相对成熟和稳定,而且支持各类网络隧道协议,经过了OpenStack等项目的考验。这个网上很多,就不再赘述了。

三、Kubernetes集成Calico

Calico是一个纯3层的数据中心网络方案,而且无缝集成像OpenStack这种IaaS云架构,能够提供可控的VM、容器、裸机之间的IP通信。

通过将整个互联网的可扩展IP网络原则压缩到数据中心级别,Calico在每一个计算节点利用Linux Kernel实现了一个高效的vRouter来负责数据转发,而每个vRouter通过BGP协议负责把自己上运行的workload的路由信息像整个Calico网络内传播——小规模部署可以直接互联,大规模下可通过指定的BGP route reflector来完成。这样保证最终所有的workload之间的数据流量都是通过IP路由的方式完成互联的。

Calico节点组网可以直接利用数据中心的网络结构(无论是L2或者L3),不需要额外的NAT,隧道或者Overlay Network。

未分类

Calico基于iptables还提供了丰富而灵活的网络Policy,保证通过各个节点上的ACLs来提供Workload的多租户隔离、安全组以及其他可达性限制等功能。

Calico有两种布署方案,一般集群都配有SSL证书和非证书的情况。

  • 第一种无HTTPS连接etcd方案,HTTP模式部署即没有证书,直接连接etcd
  • 第二种HTTPS连接etcd集群方案,加载etcd https证书模式,有点麻烦

总结:目前Kubernetes网络最快的第一就是Calico,第二种稍慢Flannel,根据自己的网络环境条件来定。 Calico作为一款针对企业级数据中心的虚拟网络工具,借助BGP、路由表和iptables,实现了一个无需解包封包的三层网络,并且有调试简单的特点。虽然目前还有些小缺陷,比如stable版本还无法支持私有网络,但希望在后面的版本中会更加强大。

四、应用容器IP固定(参考网上资料)

Docker 1.9开始支持Contiv Netplugin,Contiv带来的方便是用户可以根据实例IP直接进行访问。

Docker 1.10版本支持指定IP启动容器,并且由于部分数据库应用对实例IP固定有需求,有必要研究容器IP固定方案的设计。

在默认的Kubernetes + Contiv的网络环境下,容器Pod的IP网络连接是由Contiv Network Plugin来完成的,Contiv Master只实现了简单的IP地址分配和回收,每次部署应用时,并不能保证Pod IP不变。所以可以考虑引入新的Pod层面的IPAM(IP地址管理插件),以保证同一个应用多次发生部署时,Pod IP始终是不变的。

作为Pod层面的IPAM,可以把这一功能直接集成在Kubernetes里。Pod作为Kubernetes的最小调度单元,原有的Kubernetes Pod Registry(主要负责处理所有与Pod以及Pod subresource相关的请求:Pod的增删改查,Pod的绑定及状态更新,exec/attach/log等操作)并不支持在创建Pod时为Pod分配IP,Pod IP是通过获取Pod Infra Container的IP来获取的,而Pod Infra Container的IP即为Contiv动态分配得来的。

在原有Kubernetes代码基础上,修改Pod结构(在PodSpec中加入PodIP)并重写了Pod Registry同时引入了两个新的资源对象:

  • Pod IP Allocator:Pod IP Allocator是一个基于etcd的IP地址分配器,主要实现Pod IP的分配与回收。Pod IP Allocator通过位图记录IP地址的分配情况,并且将该位图持久化到etcd;
  • Pod IP Recycler:Pod IP Recycler是一个基于etcd的IP地址回收站,也是实现PodConsistent IP的核心。Pod IP Recycler基于RC全名(namespace + RC name)记录每一个应用曾经使用过的IP地址,并且在下一次部署的时候预先使用处于回收状态的IP。Pod IP Recycler只会回收通过RC创建的Pod的IP,通过其他controller或者直接创建的Pod的IP并不会记录,所以通过这种方式创建的Pod的IP并不会保持不变;同时Pod IP Recycle检测每个已回收IP对象的TTL,目前设置的保留时间为一天。

这里对kubelet也需要进行改造,主要包括根据Pod Spec中指定IP进行相关的容器创建(docker run加入IP指定)以及Pod删除时释放IP操作。

Pod的创建在PaaS里主要有两种情形:

  • 应用的第一次部署及扩容,这种情况主要是从IP pool中随机分配;
  • 应用的重新部署:在重新部署时,已经释放的IP已根据RC全名存放于IP Recycle列表中,这里优先从回收列表中获取IP,从而达到IP固定的效果。

另外为了防止IP固定方案中可能出现的问题,在Kubernetes中加入了额外的REST API:包括对已分配IP的查询,手动分配/释放IP。

容器IP固定方案已测试评估中,运行基本上没问题,但稳定性有待提升。主要表现在有时不能在预期时间内停止旧Pod,从而无法释放IP造成无法复用(初步原因是由于Docker偶尔的卡顿造成无法在规定时间内停止容器),可以手动去修复。但从长期考虑,IP固定方案还需要加强稳定性并根据具体需求进行优化。

总结:目前已有多种支持Kubernetes的网络方案,比如Flannel、Calico、华为的Canal、Weave Net等。因为它们都实现了CNI规范,用户无论选择哪种方案,得到的网络模型都一样,即每个Pod都有独立的 IP,可以直接通信。区别在于不同方案的底层实现不同,有的采用基于VXLAN的Overlay实现,有的则是Underlay,性能上有区别,再有就是是否支持Network Policy了。

Kubernetes监控系列(四):Docker + Kubernetes在WayBlazer的使用案例

【编者的话】本文是关于在生产中使用Kubernetes系列的一部分。第一部分介绍了Kubernetes和监控工具的基础知识;第二部分涵盖了Kubernetes报警的最佳实践;第三部分对Kubernetes故障排除,特别是服务发现进行了介绍,最后一部分是监控Kubernetes的实际使用案例。

将Docker引入生产环境,既是一门科学,同时也是一门艺术。我们与WayBlazer的Kevin Kuhl和Kevin Cashman一起讨论有关Docker和Kubernetes的监控、选择正确的关注指标、决定使用Kubernetes来运行何种程序以及诊断Kubernetes故障转移中的“多米诺效应”。

Update:在写完这篇文章的一年后,我们与Wayblazer的Kevin Kuhl、Kevin Cashman再次进行了讨论,并在他们的Kubernetes部署安全性方面增加了一些新的有洞察力的细节。

WayBlazer介绍

WayBlazer是世界上第一个专注于向旅游行业提供人工智能技术的认知推荐引擎。结合IBM Watson和专有认知计算技术,WayBlazer的AI技术对旅行者搜索历史提供的线索进行分析,以便为他们的旅行提供个性化的酒店推荐、见解、图像和当地文化。个性化内容与酒店推荐相结合可提高在线参与度并提高旅行者转化率。

该公司总部位于得克萨斯州奥斯汀,由经验丰富的旅游、科技企业家领导,其中包括Travelocity的创始人/ Kayak.com的创始人主席,IBM前任总经理Watson,以及Sabre Hospitality Solutions的前总裁。

你的环境是什么样的?

我们是一家AWS商店,所以可以想象我们会大量使用EC2。 同时我们还使用了许多AWS服务,这些服务易于使用,价格可取,而且管理得很好。 这些都能使我们的生活变得更轻松。 不久之前,我们开始使用Kubernetes,现在我们在EC2节点之上运行了十几个Kubernetes工作节点(有时甚至更多)。 每个节点运行约15-20个容器。 另外,我们有许多Kubernetes命名空间,如prod、test以及project-specific。

在上述环境中,我们运行了许多不同的服务。 我们的前端是JavaScript,API服务在Node.js上运行。 同时Java服务处理主要逻辑,还有一些在Python中运行的辅助应用程序。

我们使用Sysdig Monitor和AWS Cloudwatch来监控集群。我们依靠许多Kubernetes和AWS平台功能来保护集群内的应用程序,但同时也开始使用Sysdig Secure来加强集群的运行时防御能力。

你在Kubernetes内运行一切应用吗?

不是的。 我们的方法是首先说“它应该在Kubernetes中运行吗?”,但在有些领域,我们的答案是否定的。 不在Kubernetes中运行的应用,要么是功能需求不匹配,要么是性能达不到要求。

最显而易见的,我们的无状态服务通常运行在Kubernetes中,这简直是个完美的选择。 然而有状态的服务,例如图形数据库,还不是很适合。 也许使用PetSets最终能够做到这一点,但现在不行。 最后,我们的批处理作业或者定时作业通常不会运行在Kubernetes上。 同样,这是Kubernetes正在发展的方向,但现在不能满足我们的需求。

从安全和访问的角度来看,Docker,AWS,Kubernetes和其他服务都需要监控,而且从应用程序性能角度来看也是如此。 是如何做到同时监控这一切呢?

我们先谈谈平台安全保护。 从一开始我们使用kubernetes TLS——这能控制对kube-ctl的访问。 从网络角度来看,Amazon ELB是安全的,并可以保护应用程序免于暴露于外。 这对集群免于外力攻击非常给力。 然而,这些工具无法保证集群内没有发生任何不利事件。接下来当我们深入讨论Sysdig Secure时会再谈这个问题。

从传统的监控角度来看,我们仅依靠AWS Cloudwatch提供的主机级监控。 这在使用Docker和Kubernetes,也就是更静态的体系架构之前是没有问题的。 而且,相对静态的体系架构下,可以从基础设施的监控信息中获得关于应用程序性能的更多信息。 迁移到Kubernetes后发生了改变, 我们需要在容器和服务级别进行监控。

Kubernetes默认提供Heapster和Grafana,这是我们的第一次尝试。 为了使用和管理这些组件,我们投入了大量的时间和努力。 鉴于我们是由两人组成的运维团队,这显然不是值得我们投入大量时间的方式。 我们需要一个功能全面,健壮监控功能,并且易于开展工作的监控工具。

所以对于我们来说,这排除了自己组合组件的方式。 我们最终选择了符合需求的商业产品。 实际上,我们仍然使用CloudWatch监控基本的AWS指标,同时使用Sysdig用于更高级别的内容。 尽管在监控底层基础设施方面存在一些重复,但没有任何影响。

Kubernetes是否会影响Docker的监控? 并保护它?

是的,但Kubernetes影响很多方面,而不仅仅是监控。 如果我们回到为什么选Kubernetes这一问题上,一方面是它能精确地描述它在做什么,并且有它自己的语言来描述它:namespace,deployment等等。 这种“语言”统一了容器技术的使用方式。 这使得与工程师,运维人员或者任何参与到软件开发过程的人员的交流变得容易。

为了获取更加具体的监控信息,到目前为止,迁移到Kubernetes最重要,最明显的原因是对深入了解以Kubernetes为中心和以服务为中心的代码视图。实际上,很多软件可以通过与Docker API交互来监控基础设施级别的Docker容器,但他们不知道容器的具体信息。了解构成API服务的容器组,或者某个容器是某个Pod/ReplicaSet的一部分是非常有价值的,这使我们重新定义了对监控的需求。

对安全性有类似的思路:一个从以服务、Kubernetes为中心角度,且可以了解容器内部所发生事情的安全工具,比只通过镜像名区分容器或容器组的工具强大得多。

好吧,既然我们正在和你一起写这篇文章,我们都可以十分肯定地假设WayBlazer选择了Sysdig。 为什么?

是的。 我们现在使用了整个Sysdig容器智能平台——Sysdig Monitor,Sysdig Secure和开源故障排除工具。 Sysdig是一个优秀的集成基础设施监控、Kubernetes集群监控的工具。 基本上没有其他工具具有如此能力,通常我们都需要这两部分的监控才能在生产环境中运行容器。

Sysdig加强了Kubernetes的语言通用性。 它了解Kubernetes的基本结构。 不需要告诉Sysdig任何信息,也不必去适应它。 一旦部署了Sysdig,就可以获得想要的服务级别的指标信息,命令历史记录以及安全策略违规记录。

所有这些都来自一个统一的Agent。 在添加Sysdig Secure之前,Sysdig Monitor已经运行了大约18个月,我们只需添加一个额外的conf变量到Sysdig agent YAML文件中,Kubernetes Daemonset便会负责剩下的事情。

另外重要的一点是Sysdig可以获得容器内部更多的应用程序或以代码为中心的应用程序视图。 虽然我们十分关注基础架构指标,但同时也关注应用程序的监控信息。 Sysdig可以做到开箱即用,甚至当Kubernetes正在对Docker容器进行扩缩容。

所有这些方面都降低了成本,也提升了我们的效率。 这意味着我们不必为了将Kubernetes元数据转换成衡量指标而花费精力,也不必过多考虑如何使用我们的系统。

一旦应用程序部署在生产环境中,该如何保护呢?

上文已经提到了平台能提供安全性。 但是,我们仍然需要一些能够为运行时行为提供安全性的服务。 这些行为可能发生在主机级别,应用程序级别或系统中其他任何地方。 我欣赏Sysdig Secure,因为除了拥有以服务为中心的策略来提醒或阻止行为之外,还能让我们深入了解系统内发生的所有事情。 明确没有发生的事情的确能使人安心很多。

运行时安全最让人担心的问题是(1)谁在登录系统,他们在做什么;(2)是否有人正在修改系统内重要的文件? Sysdig安全策略默认已经对登录,远程shell连接,未经授权的连接以及其他一些行为进行了防御和提醒,所以只需要为自定义应用程序创建一些特定的策略。 我们需要更高的优先级和更严格定义的安全策略。

就上文所描述的可见性,Secure提供历史命令查看就是一个很好的例子。 它提供查看系统中所执行的一切内容的能力,从微服务、到主机或容器等等,总之,一切我想看的内容。 另外,Sysdig Monitor的另一功能是可以及时查看指标信息,这也非常有用。 现在,如果将这两者结合在一起会非常有用。 如果在监控页面(显示内存,响应时间信息等)中看到不正常信息或事件,便可以回到命令历史记录获得更多信息,如是否有人在该服务或容器上运行了新的东西?

让我们更深入一点。 什么是最重要的监控指标? 你一天到晚最想知道什么?

有趣的是,我们正在讨论Docker监控,但却没有真正地监控Docker。而是专注于监控应用程序,并在某种程度上监控Kubernetes以向我们提供有关应用程序运行的环境信息。 我们经常使用Docker相关信息进行故障排除,这是我们基础架构栈中的另一个层面,并且可能经常提供帮助我们解决棘手问题的信息。 以下是我们在应用程序级别监控的一些关键内容:

  • 请求时间:我们密切关注前端和API的请求和响应时间。如果出现问题或变化,这些是需要首先关注的指标。 例如,一次新构建可能会导致应用程序执行失败,或者,一次网络配置错误可能会对用户产生负面影响。
  • 后端查询响应时间:这些可能直接与数据库或Java后端相关。 当然,这是寻找潜在问题的好地方,也是改善系统的机会。 如果通常情况下,查询运行缓慢,便可以将其返回给团队进行研究和修复。
  • 堆使用和垃圾回收:虽然不经常使用,但我们已经使用这些数据来调试基于JVM级数据的数据仓库。 从应用程序级别来看,我们确实需要关注这些资源指标。

接下来是Kubernetes。 以下是我们关注的内容:

  • Pod重启和容器计数:告诉我们是否有变化发生。
  • 启动失败:这些是可以用来关联其他指标的重要事件,无论是应用还是基础架构。
  • 服务请求延迟:这一点极具挑战。 它涉及汇总来自特定服务的所有容器的延迟信息,并以一个统一的视图呈现。 这是在监控中使用“Kubernetes视点”的最佳例子。
  • 容器平均资源利用率:我们最近在Kubernetes中添加了资源请求和限制机制,这意味着我们可以更有效地追踪资源利用率。 相比根据主机级别CPU和内存信息来监控资源利用率,我们选择根据分配给特定容器的资源来进行监控。 这能在资源分配问题上更早的提示警告,而不是仅仅在主机利用率级别进行监控。

关键节点上高CPU、内存和磁盘使用率,我们已经有了报警机制。 你也需要这些功能。

能谈谈你监控Kubernetes“多米诺骨牌效应”的故事吗?

这是我们在EC2中运行Kubernetes时早就经历的一件事。 我们的第一个警告指标是API高延迟的警报。 但此时延迟时间开始降低,虽然我们已准备好在必要时采取行动,但系统似乎稳定了。

但很快我们明白了问题的严重性。 我们看到了Kubernetes管理的节点缩容的警报。 我们立即查看了Sysdig,注意到蓝线在下降。 这不太好,但事情还没有结束。 Kubernetes正在干掉相关容器,并移到另一个节点。

未分类

然后同时看到磁盘和内存使用率都非常高。 这是最后的警告。 然后整个系统崩溃了。

未分类

基本上这就是多米诺骨牌效应——Kubernetes对第一个节点做了failvoer,它在内存耗尽时清除了所有容器。 此时,某些服务上并不存在对内存使用的请求和限制。

未分类

问题在于集群中的其他节点的磁盘使用已经比较高了,以至于无法为需要迁移的容器保留足够的空间。

未分类

我们很快就能够扩大自动伸缩组并修复一些节点,让系统恢复正常。 但这通常来说比较痛苦,而且很不理想。在之后对此的回顾汇总,我们很有趣的发现,能够通过Sysdig查看到整个事情。 然后在一天内与团队一起发布了一个可靠的关于系统行为的报告。 同时我们整理了一份完整的操作列表,其中有在系统达到特定阈值、限制条件、扩容策略等的具体操作。 这使我们的系统更加健壮。

你能总结一下在Docker监控方面的经验教训吗?

我们的经验表明,采用Kubernetes和Docker会对监控方法产生很大影响。 就像Kubernetes使我们可以从一个更加service-centric的视角来对待整个基础设置,监控策略也会以同样的方式发生变化。

你需要在监控系统中使用基于基础架构的视图(监控的标准方式)和以服务为中心的视图(监控的新方法),以在生产环境中运行容器。

确保你的监控系统能够强化Kubernetes的语言通用性。 它必须了解Kubernetes开箱即用的基本结构。 而且你不应该告诉他任何信息。 一旦部署,就可以获得服务级别的监控信息。

另外重要的一点你的工具应该在容器中看到更多的应用程序信息或以代码为中心的应用程序视图。 尽管我们密切关注基础架构指标,但我们仍然专注于监控应用程序,不管Kubernetes正在启动或停止Docker容器。

所有这些方面都降低了成本,也提升了我们的效率。 这意味着我们不必为了将Kubernetes元数据转换成衡量指标而花费精力,也不必过多考虑如何使用我们的系统。

希望你喜欢Kubernetes监控这一系列文章! 如果你想了解更多,下面我们的首席执行官和创始人Loris Degioanni在CloudNativeCon / KubeCon “You’re Monitoring Kubernetes Wrong”的演讲。

视频: https://v.qq.com/x/page/c0603u1t44l.html

Kubernetes监控系列(三):Kubernetes服务发现与故障排除

【编者的话】本文是Kubernetes监控系列的第三篇,主要讲述了Kubernetes的故障排除以及服务发现。

本文是关于在生产中使用Kubernetes系列的一部分。第一部分介绍了Kubernetes和监控工具的基础知识;第二部分涵盖了Kubernetes报警的最佳实践。这部分将介绍对Kubernetes故障排除,特别是服务发现,最后一部分是监控Kubernetes的实际使用案例。像Kubernetes,DC / OS Mesos或Docker Swarm这样的容器编排平台可以帮助你方便地管理容器,但对解决容器问题并没有什么帮助:

  • 它们是孤立的,你和你要监视的进程之间存在一定的障碍,在主机上运行的传统故障排除工具不了解容器、命名空间和编排平台。
  • 它们带来最小的运行时间,只需要服务及其依赖项,而不需要所有的故障排除工具,想象下只用busybox来进行故障排除!
  • 它们调度在你集群、进行容器移动,扩容或者缩容。随着流程的结束,它们变得非常不稳定,出现并消失。
  • 并通过新的虚拟网络层互相通信。

本文将通过一个真实用例讲诉如何在Kubernetes中进行故障排除。我们将涵盖Kubernetes内部3个不同层的故障排除:

  • 第一部分:Kubernetes故障排除之服务发现
  • 第二部分:Kubernetes故障排除之DNS解析
  • 第三部分:Kubernetes故障排除之使用Docker运行容器

本文场景将使用一个简单的Kubernetes服务,包括3个Nginx pod和一个带有curl命令的客户端。在链接中可以看到场景需要使用的yaml文件 backend.yaml。如果你刚刚接触Kubernetes,可以看下“Understanding how Kubernetes DNS services work”这篇文章来学习如何部署上述服务。实际操作如下:

$ kubectl create namespace critical-app
namespace "critical-app" created
$ kubectl create -f backend.yaml
service "backend" created
deployment "backend" created

然后创建一个客户端来加载我们的后端服务:

$ kubectl run -it --image=tutum/curl client --namespace critical-app --restart=Never

第一部分:Kubernetes故障排除之服务发现

在我们的client容器中可以运行一个简单测试 root@client:/# curl backend 来查看我们的Kubernetes service是如何工作的。但我们不想遗漏下什么,我们认为可以使用FQDN进行服务发现测试,在Kubernetes文档中会发现,每个service都会获得一个默认的DNS:my-svc.my-namespace.svc.cluster.local。因此,我们用它作为FQDN进行测试。

在client容器的shell端运行:root@client:/# curl backend.critical-app.svc.cluster.local,不过这次curl命令卡住了10秒,然后返回了预期网址!作为一个分布式系统工程师,这是最糟糕的事情之一:你希望直接失败或者成功,而不是等待10秒。

为了定位出问题,我们使用了Sysdig。Sysdig是一个开源Linux可视化工具,提供对容器的本地可见性,包括Docker、Kubernetes、DC / OS和Mesos等等。将htop,tcpdump,strace,lsof,netstat等的功能组合在一起,Sysdig提供了Kubernetes基础架构环境中的所有系统调用和应用程序数据。Monitoring Kubernetes with Sysdig中很好地介绍了如何在Kubernetes中使用这个工具。

为了分析上述问题原因,我们将请求sysdig来dump所有信息到捕获文件中:

$ sudo sysdig -k http://127.0.0.1:8080 -s8192 -zw capture.scap

快速解释下每个参数:

  • -k http://localhost:8080 连接Kubernetes API
  • -s8192 扩大IO缓冲区,因为我们需要显示全部内容,否则默认情况下会被切断,
  • -zw capture.scap 将系统调用与元数据信息压缩并dump到文件中

同时,我们将再次运行curl命令来复现这个问题:# curl backend.critical-app.svc.cluster.local。这确保了我们在上面捕获的文件中拥有所有数据,以重现场景并解决问题。

一旦curl返回,我们可以执行Ctrl+C来终止数据捕获,并且我们将拥有一个10s的捕获文件,包括我们的Kubernetes主机中发生的所有事情以及服务发现过程。现在我们可以开始在集群内或集群外解决问题,只要在安装了sysdig的环境中复制文件:

$ sysdig -r capture.scap -pk -NA "fd.type in (ipv4, ipv6) and (k8s.ns.name=critical-app or proc.name=skydns)" | less

快速解释下每个参数:

  • -r capture.scap 读取捕获文件
  • -pk 打印Kubernetes部分到stdout中
  • -NA 显示ASCII输出

以及双引号中是过滤内容。Sysdig能够理解Kubernetes语义,因此我们可以过滤来自名称空间critical-app中的任何容器或任何名为skydns的进程的套接字IPv4或IPv6上的流量。加入proc.name = skydns因为这是内部Kubernetes DNS解析器,并且作为Kubernetes基础结构的一部分运行在我们的命名空间之外。

未分类

Sysdig也有一个交互式的ncurses接口。

为了跟随此服务发现故障排除示例,你可以下载捕获文件capture.scap并使用sysdig自行查看它。

我们立即看到curl如何尝试解析域名,但在DNS查询有效载荷上我们有些奇怪(10049):backend.critical-app.svc.cluster.local.critical-app.svc.cluster.local。看上去由于某些原因,curl不识别已经给定的FQDN,并且决定追加一个搜索域。

[...]
10030 16:41:39.536689965 0 client (b3a718d8b339) curl (22370:13) < socket fd=3(<4>)
10031 16:41:39.536694724 0 client (b3a718d8b339) curl (22370:13) > connect fd=3(<4>)
10032 16:41:39.536703160 0 client (b3a718d8b339) curl (22370:13) < connect res=0 tuple=172.17.0.7:46162->10.0.2.15:53
10048 16:41:39.536831645 1  (36ae6d09d26e) skydns (17280:11) > recvmsg fd=6(<3t>:::53)
10049 16:41:39.536834352 1  (36ae6d09d26e) skydns (17280:11) < recvmsg res=87 size=87 data=
backendcritical-appsvcclusterlocalcritical-appsvcclusterlocal tuple=::ffff:172.17.0.7:46162->:::53
10050 16:41:39.536837173 1  (36ae6d09d26e) skydns (17280:11) > recvmsg fd=6(<3t>:::53)
[...]

SkyDNS发送请求(10097)到etcd的API(/local/cluster/svc/critical-app/local/cluster/svc/critical-app/backend),显然etcd不能识别这个service,然后返回(10167)“Key not found”。这通过DNS查询响应传回给curl。

[...]
10096 16:41:39.538247116 1  (36ae6d09d26e) skydns (4639:8) > write fd=3(<4t>10.0.2.15:34108->10.0.2.15:4001) size=221
10097 16:41:39.538275108 1  (36ae6d09d26e) skydns (4639:8) < write res=221 data=
GET /v2/keys/skydns/local/cluster/svc/critical-app/local/cluster/svc/critical-app/backend?quorum=false&recursive=true&sorted=false HTTP/1.1
Host: 10.0.2.15:4001
User-Agent: Go 1.1 package http
Accept-Encoding: gzip


10166 16:41:39.538636659 1  (36ae6d09d26e) skydns (4617:1) > read fd=3(<4t>10.0.2.15:34108->10.0.2.15:4001) size=4096
10167 16:41:39.538638040 1  (36ae6d09d26e) skydns (4617:1) < read res=285 data=
HTTP/1.1 404 Not Found
Content-Type: application/json
X-Etcd-Cluster-Id: 7e27652122e8b2ae
X-Etcd-Index: 1259
Date: Thu, 08 Dec 2016 15:41:39 GMT
Content-Length: 112

{"errorCode":100,"message":"Key not found","cause":"/skydns/local/cluster/svc/critical-app/local","index":1259}
[...]

curl没有放弃并再次尝试(10242),不过这次用的是backend.critical-app.svc.cluster.local.svc.cluster.local。看起来这次curl尝试将critical-app这个追加搜索域去除,使用一个不同的搜索域。显然,当请求到达etcd(10247)时,再次失败(10345)。

[...]
10218 16:41:39.538914765 0 client (b3a718d8b339) curl (22370:13) < connect res=0 tuple=172.17.0.7:35547->10.0.2.15:53
10242 16:41:39.539005618 1  (36ae6d09d26e) skydns (17280:11) < recvmsg res=74 size=74 data=
backendcritical-appsvcclusterlocalsvcclusterlocal tuple=::ffff:172.17.0.7:35547->:::53
10247 16:41:39.539018226 1  (36ae6d09d26e) skydns (17280:11) > recvmsg fd=6(<3t>:::53)
10248 16:41:39.539019925 1  (36ae6d09d26e) skydns (17280:11) < recvmsg res=74 size=74 data=
0]backendcritical-appsvcclusterlocalsvcclusterlocal tuple=::ffff:172.17.0.7:35547->:::53
10249 16:41:39.539022522 1  (36ae6d09d26e) skydns (17280:11) > recvmsg fd=6(<3t>:::53)
10273 16:41:39.539210393 1  (36ae6d09d26e) skydns (4639:8) > write fd=3(<4t>10.0.2.15:34108->10.0.2.15:4001) size=208
10274 16:41:39.539239613 1  (36ae6d09d26e) skydns (4639:8) < write res=208 data=
GET /v2/keys/skydns/local/cluster/svc/local/cluster/svc/critical-app/backend?quorum=false&recursive=true&sorted=false HTTP/1.1
Host: 10.0.2.15:4001
User-Agent: Go 1.1 package http
Accept-Encoding: gzip


10343 16:41:39.539465153 1  (36ae6d09d26e) skydns (4617:1) > read fd=3(<4t>10.0.2.15:34108->10.0.2.15:4001) size=4096
10345 16:41:39.539467440 1  (36ae6d09d26e) skydns (4617:1) < read res=271 data=
HTTP/1.1 404 Not Found
[...]

curl会再次尝试,我们可以看这次的DNS查询请求(10418),加上了cluster.local变成backend.critical-app.svc.cluster.local.cluster.local。这次etcd请求(10479)同样失败(10524)。

[...]
10396 16:41:39.539686075 0 client (b3a718d8b339) curl (22370:13) < connect res=0 tuple=172.17.0.7:40788->10.0.2.15:53
10418 16:41:39.539755453 0  (36ae6d09d26e) skydns (17280:11) < recvmsg res=70 size=70 data=
backendcritical-appsvcclusterlocalclusterlocal tuple=::ffff:172.17.0.7:40788->:::53
10433 16:41:39.539800679 0  (36ae6d09d26e) skydns (17280:11) > recvmsg fd=6(<3t>:::53)
10434 16:41:39.539802549 0  (36ae6d09d26e) skydns (17280:11) < recvmsg res=70 size=70 data=
backendcritical-appsvcclusterlocalclusterlocal tuple=::ffff:172.17.0.7:40788->:::53
10437 16:41:39.539805177 0  (36ae6d09d26e) skydns (17280:11) > recvmsg fd=6(<3t>:::53)
10478 16:41:39.540166087 1  (36ae6d09d26e) skydns (4639:8) > write fd=3(<4t>10.0.2.15:34108->10.0.2.15:4001) size=204
10479 16:41:39.540183401 1  (36ae6d09d26e) skydns (4639:8) < write res=204 data=
GET /v2/keys/skydns/local/cluster/local/cluster/svc/critical-app/backend?quorum=false&recursive=true&sorted=false HTTP/1.1
Host: 10.0.2.15:4001
User-Agent: Go 1.1 package http
Accept-Encoding: gzip


10523 16:41:39.540421040 1  (36ae6d09d26e) skydns (4617:1) > read fd=3(<4t>10.0.2.15:34108->10.0.2.15:4001) size=4096
10524 16:41:39.540422241 1  (36ae6d09d26e) skydns (4617:1) < read res=267 data=
HTTP/1.1 404 Not Found
[...]

对于未经训练的人来说,可能看起来我们发现了这个问题:一堆无效的请求。但实际上这不是事实,如果我们查看时间戳,第一个etcd请求(10097)和最后一个(10479)之间的差异,第二列中的时间戳相距小于10ms。而我们正在寻找一个秒数问题,而不是毫秒——那么等待的时间在哪里?

当我们继续浏览捕获文件时,我们可以看到curl不停止尝试使用DNS查询到SkyDNS,现在使用backend.critical-app.svc.cluster.local.localdomain(10703)。这个.localdomain不被SkyDNS识别为Kubernetes的内部域,因此它决定将这个查询转发给它的上游DNS解析器(10691),而不是去etcd进行服务发现。

[...]
10690 16:41:39.541376928 1  (36ae6d09d26e) skydns (4639:8) > connect fd=8(<4>)
10691 16:41:39.541381577 1  (36ae6d09d26e) skydns (4639:8) < connect res=0 tuple=10.0.2.15:44249->8.8.8.8:53
10702 16:41:39.541415384 1  (36ae6d09d26e) skydns (4639:8) > write fd=8(<4u>10.0.2.15:44249->8.8.8.8:53) size=68
10703 16:41:39.541531434 1  (36ae6d09d26e) skydns (4639:8) < write res=68 data=
Nbackendcritical-appsvcclusterlocallocaldomain
10717 16:41:39.541629507 1  (36ae6d09d26e) skydns (4639:8) > read fd=8(<4u>10.0.2.15:44249->8.8.8.8:53) size=512
10718 16:41:39.541632726 1  (36ae6d09d26e) skydns (4639:8) < read res=-11(EAGAIN) data=
58215 16:41:43.541261462 1  (36ae6d09d26e) skydns (4640:9) > close fd=7(<4u>10.0.2.15:54272->8.8.8.8:53)
58216 16:41:43.541263355 1  (36ae6d09d26e) skydns (4640:9) < close res=0
[...]

扫描时间戳列时,我们发现SkyDNS发出请求后第一个较大的间隙,然后等待大约4秒(10718-58215)。鉴于.localdomain不是有效的TLD(顶级域名),上游服务器将只是忽略此请求。超时后,SkyDNS再次尝试使用相同的查询(75923),再等待几秒钟(75927-104208)。总的来说,我们一直在等待大约8秒钟,以查找不存在并且被忽略的DNS条目。

[...]
58292 16:41:43.542822050 1  (36ae6d09d26e) skydns (4640:9) < write res=68 data=
Nbackendcritical-appsvcclusterlocallocaldomain
58293 16:41:43.542829001 1  (36ae6d09d26e) skydns (4640:9) > read fd=8(<4u>10.0.2.15:56371->8.8.8.8:53) size=512
58294 16:41:43.542831896 1  (36ae6d09d26e) skydns (4640:9) < read res=-11(EAGAIN) data=
75904 16:41:44.543459524 0  (36ae6d09d26e) skydns (17280:11) < recvmsg res=68 size=68 data=
[...]
75923 16:41:44.543560717 0  (36ae6d09d26e) skydns (17280:11) < recvmsg res=68 size=68 data=
Nbackendcritical-appsvcclusterlocallocaldomain tuple=::ffff:172.17.0.7:47441->:::53
75927 16:41:44.543569823 0  (36ae6d09d26e) skydns (17280:11) > recvmsg fd=6(<3t>:::53)
104208 16:41:47.551459027 1  (36ae6d09d26e) skydns (4640:9) > close fd=7(<4u>10.0.2.15:42126->8.8.8.8:53)
104209 16:41:47.551460674 1  (36ae6d09d26e) skydns (4640:9) < close res=0
[...]

但最后,它一切正常!为什么?curl停止尝试修补事物并应用搜索域。当我们在命令行中输入时,它会逐字尝试域名。SkyDNS通过etcd服务发现API请求解析DNS请求(104406)。打开一个与服务IP地址(107992)的连接,然后使用iptables将其转发给Pod,并且HTTP响应返回到curl容器(108024)。

[...]
104406 16:41:47.552626262 0  (36ae6d09d26e) skydns (4639:8) < write res=190 data=
GET /v2/keys/skydns/local/cluster/svc/critical-app/backend?quorum=false&recursive=true&sorted=false HTTP/1.1
[...]
104457 16:41:47.552919333 1  (36ae6d09d26e) skydns (4617:1) < read res=543 data=
HTTP/1.1 200 OK
[...]
{"action":"get","node":{"key":"/skydns/local/cluster/svc/critical-app/backend","dir":true,"nodes":[{"key":"/skydns/local/cluster/svc/critical-app/back
end/6ead029a","value":"{"host":"10.3.0.214","priority":10,"weight":10,"ttl":30,"targetstrip":0}","modifiedIndex":270,"createdIndex":270}],
"modifiedIndex":270,"createdIndex":270}}
[...]
107992 16:41:48.087346702 1 client (b3a718d8b339) curl (22369:12) < connect res=-115(EINPROGRESS) tuple=172.17.0.7:36404->10.3.0.214:80
108002 16:41:48.087377769 1 client (b3a718d8b339) curl (22369:12) > sendto fd=3(<4t>172.17.0.7:36404->10.3.0.214:80) size=102 tuple=NULL
108005 16:41:48.087401339 0 backend-1440326531-csj02 (730a6f492270) nginx (7203:6) < accept fd=3(<4t>172.17.0.7:36404->172.17.0.5:80) tuple=172.17.0.7:36404->172.17.0.5:80 queuepct=0 queuelen=0 queuemax=128
108006 16:41:48.087406626 1 client (b3a718d8b339) curl (22369:12) < sendto res=102 data=
GET / HTTP/1.1
[...]
108024 16:41:48.087541774 0 backend-1440326531-csj02 (730a6f492270) nginx (7203:6) < writev res=238 data=
HTTP/1.1 200 OK
Server: nginx/1.10.2
[...]

通过系统层面的情况,我们可以得出结论:造成这个问题的根本原因有两个方面。首先,当我给curl一个FQDN时,它不相信我并试图应用搜索域算法。其次,.localdomain不应该存在,因为它在我们的Kubernetes集群中是不可路由的。

如果你认为这是通过使用tcpdump也能完成,只是你还没尝试。我能100%肯定它不会被安装在你的容器内,你可以在主机外部运行它,但祝你找到与对应Kubernetes调度容器的网络命名空间匹配的网络接口。请继续阅读:我们还没有完成故障排除。

第二部分:Kubernetes故障排除之DNS解析

让我们看看resolv.conf文件中的内容。容器可能已经不存在了,或者curl调用后文件可能已经改变。但是我们有一个包含发生了所有事情的捕获文件。

通常情况下,容器的运行时间与进程内运行的时间一样长,当进程结束时容器就消失了。这是对容器进行故障排除最具挑战性的部分之一。我们如何探索已经消失的东西?我们如何重现发生的事情?在这些情况下,Sysdig捕获文件非常有用。

我们来分析捕获文件,但不是过滤网络流量,我们将在这次对resolv文件进行过滤。我们希望看到resolv.conf与curl读取的完全一样,以确认我们的想法,它包含localdomain。

$ sysdig -pk -NA -r capture.scap -c echo_fds "fd.type=file and fd.name=/etc/resolv.conf"
------ Read 119B from  [k8s_client.eee910bc_client_critical-app_da587b4d-bd5a-11e6-8bdb-028ce2cfb533_bacd01b6] [b3a718d8b339]  /etc/resolv.conf (curl)

search critical-app.svc.cluster.local svc.cluster.local cluster.local localdomain
nameserver 10.0.2.15
options ndots:5
[...]

这是使用sysdig的一种新的方式:

-c echo_fds 使用Sysdig chisel——附加脚本——用以聚合信息并格式化输出。此外,过滤器仅包含文件描述符上的IO活动,该文件描述符是一个文件,名称为/etc/resolv.conf,正是我们所要寻找的。

通过系统调用,我们看到有一个选项叫做ndots。 此选项是curl不信任我们的FQDN并试图首先追加所有搜索域的原因。如果你阅读了manpage,就会知道ndots会强制libc,任何小于5个点的域名解析都不会被视为fqdn,但会尝试首先追加所有搜索域。ndots有一个很好的理由,所以我们可以执行 curl backend。但是谁在那里添加了localdomain?

第三部分:Kubernetes故障排除之使用Docker运行容器

我们不想在没有找到这个本地域的罪魁祸首的情况下完成我们的故障排除。 这样,我们可以责怪软件而不是人,是Docker加了搜索域?或者Kubernetes在创建容器时指导Docker这么做?

既然我们知道Kubernetes和Docker之间的所有控制通信都是通过Unix套接字完成的,我们可以使用它来过滤掉所有的东西:

$ sudo sysdig -pk -s8192 -c echo_fds -NA "fd.type in (unix) and evt.buffer contains localdomain"

这一次,我们将使用一个非常棒的过滤器的来捕捉现场 evt.buffer containers。 这个过滤器需要所有的事件缓冲区,如果它包含我们正在寻找的字符串,将被我们的chisel捕获并格式化输出。

现在我需要创建一个新的客户端来窥探容器编排时发生的情况:

$ kubectl run -it --image=tutum/curl client-foobar --namespace critical-app --restart=Never

可以看到,Kubernetes中的hyperkube使用Docker API在/var/run/docker.sock上写了一个HTTP POST请求到/containers/create。如果我们通读它,我们会发现这个请求包含一个选项“DnsSearch”:[“critical-app.svc.cluster.local”,“svc.cluster.local”,“cluster.local”,“localdomain”]。Kubernetes,我们抓到你了!很可能它出于某种原因,就像我的本地开发机器设置了该搜索域一样。无论如何,这是一个不同的故事。

[...]
------ Write 2.61KB to  [k8s-kubelet] [de7157ba23c4]   (hyperkube)

POST /containers/create?name=k8s_POD.d8dbe16c_client-foobar_critical-app_085ac98f-bd64-11e6-8bdb-028ce2cfb533_9430448e HTTP/1.1
Host: docker
[...]
"DnsSearch":["critical-app.svc.cluster.local","svc.cluster.local","cluster.local","localdomain"],
[...]

总结

准确地再现容器内发生的事情可能会非常具有挑战性,因为它们在进程死亡或刚刚结束时终止。Sysdig捕获通过系统调用包含所有信息,包括网络流量、文件系统I/O和进程行为,提供故障排除所需的所有数据。

在容器环境中进行故障排除时,能够过滤和添加容器上下文信息(如Docker容器名称或Kubernetes元数据)使我们的排查过程更加轻松。

Sysdis在所有主流Linux发行版、OSX以及Windows上都能使用,下载它获得最新版本。Sysdig是一个开源工具,但该项目背后的公司也提供了一个商业产品来监视和排除多个主机上的容器和微服务。

Kubernetes监控系列(二):Kubernetes集群的监控报警策略最佳实践

【编者的话】本文是Kubernetes监控系列的第二篇,主要讲述了配置Kubernetes报警的一些最佳实践。

本文将介绍关于如何在Kubernetes集群中配置基础设施层的警报。本文是关于在生产中使用Kubernetes系列的一部分。第一部分介绍了Kubernetes和监控工具的基础知识;这部分涵盖了Kubernetes报警的最佳实践,第三部分将介绍Kubernetes服务发现与故障排除,最后一部分是监控Kubernetes的实际使用案例。

监控是每个优质基础设施的基础,是达到可靠性层次的基础。监控有助于减少对突发事件的响应时间,实现对系统问题的检测、故障排除和调试。在基础设施高度动态的云时代,监控也是容量规划的基础。

有效的警报是监控策略的基石。当你转向容器和Kubernetes编排环境,你的警报策略也需要演进。正是因为以下几个核心原因,其中许多我们在”如何监视Kubernetes”中进行了讲述:

  • 可见性:容器是一个黑盒。传统工具只能针对公共监控端点进行检查。如果想深入监控相关服务,则需要采取不一样的方法。
  • 新的基础架构层:现在服务和主机之间有一个新的层:容器和容器编排服务。这些是你需要监控的新内部服务,你的监控系统需要了解这些服务。
  • 动态重调度:容器没有像之前那样的服务节点,所以传统的监控不能有效地工作。没有获取服务度量标准的静态端点,没有运行服务实例的静态数量(设想一个金丝雀发布或自动伸缩设置)。在某节点中一个进程被kill是正常的,因为它有很大的机会被重新调度到基础设施的其他节点。
  • 元数据和标签:随着服务跨多个容器,为所有这些服务添加系统级别的监控以及服务特定的度量标准,再加上Kubernetes带来的所有新服务,如何让所有这些信息对我们有所帮助?有时你希望看到分布在不同节点容器中的服务的网络请求度量,有时你希望看到特定节点中所有容器的相同度量,而不关心它们属于哪个服务。这就需要一个多维度量系统,需要从不同的角度来看待这些指标。如果通过Kubernetes中存在的不同标签自动标记度量标准,并且监控系统能够了解Kubernetes元数据,那么只需要在每种情况下按照需要聚合和分割度量标准就可以实现多维度度量。

考虑到这些问题,让我们创建一组对Kubernetes环境至关重要的报警策略。我们的Kubernetes报警策略教程将涵盖:

  • 应用程序层度量标准的报警
  • Kubernetes上运行的服务的报警
  • Kubernetes基础设施的报警
  • 在主机/节点层上的报警

最后我们还将通过检测系统调用问题来了解如何通过报警加速故障排除。

应用程序层度量标准的报警

工作指标(working metrics)可以确认应用程序是否按预期执行,这些度量标准通常来自用户或消费者对服务的期望操作,或者是由应用程序通过statsd或JMX在内部生成。如果监控系统本身提供网络数据,就可以使用它来创建基于响应时间的报警策略。

以下示例是一个公共REST API端点监控警报,监控prod命令空间中的名为javaapp的deployment,在10分钟内如果延迟超过1秒则报警。

未分类

所有这些报警高度依赖于应用程序、工作负载模式等等,但真正的问题是如何在所有服务中一致性地获取数据。在理想环境中,除了核心监控工具之外,无需再购买综合监控服务即可获得这一套关键指标。

这个类别中经常出现的一些指标和报警是:

  • 服务相应时间
  • 服务可用性
  • SLA合规性
  • 每秒请求的成功/失败数

Kubernetes上运行的服务的报警

对于服务级别,和使用Kubernetes之前对于服务集群需要做的事情应该没什么不同。试想下,当在MySQL/MariaDB或者MongoDB等数据库服务中查看副本状态与延迟时,需要考虑些什么?

没错,如果想知道服务的全局运行和执行情况,就需要利用监视工具功能根据容器元数据将度量标准进行聚合和分段。

如第一篇文章所述,Kubernetes将容器标记为一个deployment或者通过一个service进行暴露,在定义报警时就需要考虑这些因素,例如针对生产环境的报警(可能通过命名空间进行定义)。

以下是Cassandra集群的示例:

未分类

如果我们在Kubernetes、AWS EC2或OpenStack中运行Cassandra,则衡量指标cassandra.compactions.pending存在每个实例上,但是现在我们要查看在prod命名空间内的cassandra复制控制器中聚合的指标。这两个标签都来自Kubernetes,但我们也可以更改范围,包括来自AWS或其他云提供商的标签,例如可用区域。

这个类别中经常出现的一些指标和警报是:

  • HTTP请求数
  • 数据库连接数、副本数
  • 线程数、文件描述符数、连接数
  • 中间件特定指标:Python uwsgi worker数量,JVM堆大小等

另外,如果正在使用外部托管服务,则很可能需要从这些提供程序导入度量标准,因为它们可能还有需要做出反应的事件。

Kubernetes基础设施的报警

在容器编排层面的监控和报警有两个层面。一方面,我们需要监控Kubernetes所处理的服务是否符合所定义的要求。另一方面,我们需要确保Kubernetes的所有组件都正常运行。
由Kubernetes处理的服务

1.1 是否有足够的Pod/Container给每个应用程序运行?

Kubernetes可以通过Deployments、Replica Sets和Replication Controllers来处理具有多个Pod的应用程序。它们之间区别比较小,可以使用它们来维护运行相同应用程序的多个实例。运行实例的数量可以动态地进行伸缩,甚至可以通过自动缩放来实现自动化。

运行容器数量可能发生变化的原因有多种:因为节点失败或资源不足将容器重新调度到不同主机或者进行新版本的滚动部署等。如果在延长的时间段内运行的副本数或实例的数量低于我们所需的副本数,则表示某些组件工作不正常(缺少足够的节点或资源、Kubernetes或Docker Engine故障、Docker镜像损坏等等)。

在Kubernetes部署服务中,跨多个服务进行如下报警策略对比是非常有必要的:

timeAvg(kubernetes.replicaSet.replicas.running) < timeAvg(kubernetes.replicaSet.replicas.desired)

正如之前所说,在重新调度与迁移过程运行中的实例副本数少于预期是可接受的,所以对每个容器需要关注配置项 .spec.minReadySeconds (表示容器从启动到可用状态的时间)。你可能也需要检查 .spec.strategy.rollingUpdate.maxUnavailable 配置项,它定义了在滚动部署期间有多少容器可以处于不可用状态。

下面是上述报警策略的一个示例,在 wordpress 命名空间中集群 kubernetes-dev 的一个deployment wordpress-wordpress

未分类

1.2 是否有给定应用程序的任何Pod/Container?

与之前的警报类似,但具有更高的优先级(例如,这个应用程序是半夜获取页面的备选对象),将提醒是否没有为给定应用程序运行容器。

以下示例,在相同的deployment中增加警报,但不同的是1分钟内运行Pod <1时触发:

未分类

1.3 重启循环中是否有任何Pod/Container?

在部署新版本时,如果没有足够的可用资源,或者只是某些需求或依赖关系不存在,容器或Pod最终可能会在循环中持续重新启动。这种情况称为“CrashLoopBackOff”。发生这种情况时,Pod永远不会进入就绪状态,从而被视为不可用,并且不会处于运行状态,因此,这种场景已被报警覆盖。尽管如此,笔者仍希望设置一个警报,以便在整个基础架构中捕捉到这种行为,立即了解具体问题。这不是那种中断睡眠的报警,但会有不少有用的信息。

这是在整个基础架构中应用的一个示例,在过去的2分钟内检测到4次以上的重新启动:

未分类

监控Kubernetes系统服务

除了确保Kubernetes能正常完成其工作之外,我们还希望监测Kubernetes的内部健康状况。这将取决于Kubernetes集群安装的不同组件,因为这些可能会根据不同部署选择而改变,但有一些基本组件是相同的。

2.1 etcd是否正常运行?

etcd是Kubernetes的分布式服务发现、通信、命令通道。监控etcd可以像监控任何分布式键值数据库一样深入,但会使事情变得简单。

未分类

我们可以进一步去监控设置命令失败或者节点数,但是我们将会把它留给未来的etcd监控文章来描述。

2.2 集群中有足够的节点吗?

节点故障在Kubernetes中不是问题,调度程序将在其他可用节点中生成容器。但是,如果我们耗尽节点呢?或者已部署的应用程序的资源需求超过了现有节点?或者我们达到配额限制?

在这种情况下发出警报并不容易,因为这取决于你想要在待机状态下有多少个节点,或者你想要在现有节点上推送多少超额配置。可以使用监控度量值kube_node_status_ready和kube_node_spec_unschedulable进行节点状态警报。

如果要进行容量级别报警,则必须将每个已调度Pod请求的CPU和memory进行累加,然后检查是否会覆盖每个节点,使用监控度量值kube_node_status_capacity_cpu_cores和kube_node_status_capacity_memory_bytes。

在主机/节点层上的报警

主机层的警报与监视虚拟机或物理机区别不大,主要是关于主机是启动还是关闭或不可访问状态,以及资源可用性(CPU、内存、磁盘等)。

主要区别是现在警报的严重程度。在此之前,系统服务宕机可能意味着你的应用程序停止运行并且要紧急处理事故(禁止有效的高可用性)。而对于Kubernetes来说当服务发生异常,服务可以在主机之间移动,主机警报不应该不受重视。

让我们看看我们应该考虑的几个方面:

1. 主机宕机

如果主机停机或无法访问,我们希望收到通知。我们将在整个基础架构中应用此单一警报。我们打算给它一个5分钟的等待时间,因为我们不想看到网络连接问题上的嘈杂警报。你可能希望将其降低到1或2分钟,具体取决于你希望接收通知的速度。

未分类

2. 磁盘利用率

这是稍微复杂一些的警报。我们在整个基础架构的所有文件系统中应用此警报。我们将范围设置为 everywhere,并针对每个 fs.mountDir 设置单独的评估/警报。

以下是超过80%使用率的通用警报,但你可能需要不同的策略,如具有更高阈值(95%)或不同阈值的更高优先级警报(具体取决于文件系统)。

未分类

如果要为不同的服务或主机创建不同的阈值,只需更改要应用特定阈值的范围。

3. 一些其他资源

此类别中的常见资源是有关负载、CPU使用率、内存和交换空间使用情况的警报。如果在一定的时间内这些数据中的任何一个显著提升,可能就需要警报提醒您。我们需要在阈值、等待时间以及如何判定噪声警报之间进行折中。

如果您想为这些资源设置指标,请参考以下指标:

  • 负载:load.average.1m, load.average.5m 和 load.average.15m
  • CPU:cpu.used.percent
  • memory:memory.used.percent 或 memory.bytes.used
  • swap:memory.swap.used.percent 或 memory.swap.bytes.used

一些人同样使用这个类别监控他们部分基础架构所在云服务提供商的资源。

Sysdig额外监控:监控系统调用

从警报触发并收到通知的那一刻起,真正的工作就开始为DevOps值班成员开展工作。有时运行手册就像检查工作负载中的轻微异常一样简单。这可能是云提供商的一个事件,但希望Kubernetes正在处理这个问题,并且只需要花费一些时间来应对负载。

但是如果一些更棘手的问题出现在我们面前,我们可以看到我们拥有的所有武器:提供者状态页面、日志(希望来自中心位置)、任何形式的APM或分布式追踪(如果开发人员安装了代码或者外部综合监测)。然后,我们祈祷这一事件在这些地方留下了一些线索,以便我们可以追溯到问题出现的多种来源原因。

我们怎么能够在警报被解除时自动dump所有系统调用?系统调用是所发生事情的真实来源,并将包含我们可以获得的所有信息。通过系统调用有可能发现触发警报的根本问题所在。Sysdig Monitor允许在警报触发时自动开始捕获所有系统调用。

例如,可以配置CrashLoopBackOff场景:

未分类

只有当你安装代码后,Sysdig Monitor代理才能从系统调用拦截中计算出来相应的指标。考虑HTTP响应时间或SQL响应时间。Sysdig Monitor代理程序在套接字上捕获read()和write()系统调用,解码应用程序协议并计算转发到我们时间序列数据库中的指标。这样做的好处是可以在不触及开发人员代码的情况下获得仪表盘数据和警报:

未分类

结论

我们已经看到如何使用容器编排平台来增加系统中移动部分的数量。拥有容器本地监控是建立可靠基础架构的关键要素。监控不能仅仅关注基础架构,而是需要从底层的主机到应用程序指标所在的顶端来了解整个技术栈。

能够利用Kubernetes和云服务提供商的元数据信息来汇总和细分监控指标和警报将成为多层次有效监控的必要条件。我们已经看到如何使用标签来更新我们已有的警报或创建Kubernetes上所需的新警报。

接下来,让我们看看在Kubernetes编排环境中的典型服务发现故障排除的挑战。

视频:https://v.qq.com/x/page/s06032nah0m.html

Kubernetes监控系列(一):Kubernetes监控开源工具基本介绍以及如何使用Sysdig进行监控

【编者的话】Kubernetes监控系列一共有四篇文章,此篇是该系列的首篇文章,主要介绍了一些Kubernetes监控的开源工具,并着重介绍了Sysdig这个监控工具。

在GitHub上有超过21000颗星,超过一千个提交者,以及一个包括Google、RedHat、Intel在内的生态系统,Kubernetes已经让容器生态系统风靡一时。Kubernetes这么火是因为它充当了分布式容器部署的大脑,它旨在使用分布在宿主机集群上的容器来管理面向服务的应用程序。Kubernetes为应用程序部署、服务发现、调度、更新、运维以及扩容提供了相关机制,但对于Kubernetes监控呢?

虽然Kubernetes能够显著简化在容器中以及在云上部署应用程序的过程,但同时它也增加了日常管理应用程序性能、获取服务可见性以及监控->报警->故障排除流程的复杂性。

基础设施层次的新型复杂性出现在期望简化应用程序部署的过程中:通过IaaS层动态配置;使用配置管理工具自动配置;以及使用像Kubernetes一样的编排工具,这些编排工具介于裸机或者虚机基础设施和支持应用程序的服务之间。

未分类

除了增加基础设施复杂性之外,应用程序正在被设计成微服务,在微服务结构中,更多组件之间需要互相通信。每个服务都可以分布在多个实例上,Docker容器根据需求跨越基础设施。在我们知道每个服务组件有多少实例以及它们的部署位置之前,情况就不再是这样了。这会怎么影响Kubernetes监控的方法和工具呢?正如 Site Reliability Engineering – How Google Runs Production Systems中所述,“我们需要监控系统,让我对高层次服务目标保持警惕,但也要根据需求保持对单个组件粒度的监控”。考虑到这一点,这篇博客是一个系列中的第一篇,帮助您更好地理解在生产中使用Kubernetes的行为。 这四部分组成的系列包括:

  • 第一部分(本篇):Kubernetes监控开源工具的基本介绍以及使用Sysdig进行监控。

  • 第二部分:Kubernetes报警的最佳实践

  • 第三部分:通过系统捕获对Kubernetes服务发现进行故障排除
  • 第四部分:WayBlazer的Kubernetes监控(一个示例)

了解Kubernetes及其复杂性

从物理/基础设施的角度来看,Kubernetes集群由一组master节点监控的nodes组成。master节点的任务包括跨node节点的容器编排、状态追踪以及通过REST API和UI界面暴露集群控制。

另一方面,从逻辑/应用的角度来看,Kubernetes集群按照图中所示的层级方式排列:

未分类

所有容器运行在Pods中,一个Pod是一组容器集合。这些容器总是部署在一起并且一起进行调度,它们运行在共享的环境中并且拥有共享的存储。Pod中的容器被保证部署在相同的机器上,可以共享资源。

  • Pods位于services之后,service主要负责负载均衡,并且将一组Pod作为一个可发现的IP地址/端口进行暴露。
  • Services通过replica sets(之前成为副本控制器)进行水平伸缩,它根据需求为每个服务创建/销毁Pod。
  • ReplicaSets由deployments进行控制,它允许对运行中的replicasets以及Pods进行状态申明。
  • Namespaces代表虚拟集群,可以包含一个或者多个services。
  • Metadata允许使用labels和tags基于容器的部署特性进行标记。

所以需要搞清楚的是,多个services甚至多个namespaces可以分散在同一个物理基础设施中。每个service都由多个Pod构建,而每个Pod都有多个container构成,这就为监控增加了一定程度的复杂性,即使是适度的Kubernetes部署也是如此。

让我们来看看Kubernetes本身提供的解决方案。

如何收集Kubernetes监控数据:开源工具

和大多数平台一样,Kubernetes有一套基本的工具,可以让你监控你的服务器,在这种情况下,Kubernetes可以直接部署在物理基础设施之上。“内置”一词可能有点言过其实,更公正地说,考虑到Kubernetes的可扩展性,你可以添加额外的组件来获取Kubernetes的可见性,让我们来看看其中的几个选择:

  • Probes
  • cAdvisor
  • Heapster
  • Kubernetes Dashboard
  • Kebu-state-metrics

之后我们将对比一下这些选择和使用Sysdig进行监控。

Liveness和Readiness Probes

Kubernetes Probes具有定期监测集装箱或服务的健康状况的重要功能,并在发生不健康的事件时采取行动。Kubernetes监控探针允许你通过设定一个特定的命令来定义“Liveness”状态,这个命令应该在Pod中成功执行。你还可以设定Liveness探针执行的频率。以下是一个简单的示例,基于cat命令。

#Example Liveness probe for the Sysdig Blog on "Monitoring Kubernetes"

apiVersion: v1
kind: Pod

metadata:
labels:
test: liveness
name: liveness-exec
spec:
containers:

-name: liveness
args:
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600

image: gcr.io/google_containers/busybox

livenessProbe:
  exec:
    command:
    - cat
    - /tmp/healthy
  initialDelaySeconds: 5
  periodSeconds: 5

  #source https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/

Readiness Probe某种程度上说是Liveness Probe的修正版,同样它是执行一条命令来检测Pod在启动/重启后是否准备好进行工作。

cAdvisor和Heapster

cAdvisor是一个开源的容器资源使用收集器。它是专门为容器构建的,原生支持Docker容器。与在Pod级别运行的Kubernetes中的大多数元素不同,cAdvisor在每个node节点上运行。它会自动发现给定node节点中的所有容器,并收集CPU,内存,文件系统和网络使用统计信息。cAdvisor还通过分析机器上的“root”容器来提供整体机器使用情况。不过,cAdvisor两个方面有限制性:

  1. 它只能收集一些基本的资源利用信息——cAdvisor不能够告诉你应用程序的真实性能,它只能告诉你一个容器使用了X% CPU信息。
  2. cAdvisor自身没有提供任何长期存储、趋势或者分析功能。

为了进一步处理这些数据,我们需要添加Heapster。Heapster会聚集Kubernetes集群中所有节点上的数据。Heapster作为集群中的一个Pod运行,就像其他应用程序一样。Heapster Pod通过节点上Kubelets(机器上的Kubernetes agent)查询使用信息,而Kubelets又查询来自cAdvisor的数据。最终,Heapster将来自相关标签的Pod信息进行分组。

未分类

在Kubernetes中使用Heapster和cAdvisor进行监控,来源:blog.kubernetes.io

不过,Heapster也不能够进行存储数据、趋势分析以及报警。它只是让你更容易在整个集群中收集cAdvisor数据。所以,你还需要将数据推送到可配置的后端进行存储和可视化,目前支持的后端包括InfluxDB,Google Cloud Monitoring等。此外,你还必须添加一个和Grafana一样的可视化图层来查看数据。

Kubernetes Dashboard

最近比较流行使用Kubernets插件提供一致性方式来查看一些基础数据以及管理集群环境。Kubernetes Dashboard提供了一种依据Kubernetes元数据来查看您的环境的简单方式。它有两个优势:基本的CPU/内存数据是可用的,以及可以在Dashboard上采取行动。

未分类

简单通过kubectl安装Dashboard,Kubernetes命令:

$ kubectl create -f https://rawgit.com/kubernetes/dashboard/master/src/deploy/kubernetes-dashboard.yaml

然后在安装了Dashboard的机器上通过localhost进行访问:http://localhost:8001/ui。

kube-state-metrics:作为监控设置的补充

除了配置cAdvisor/Heapster/Influx/Grafana之外,还可以考虑部署kube-state-metrics。这是一个附加服务,与Heapster一起运行,它会轮询Kubernetes API并将有关你的Kubernetes结构化特征信息转换为metrics信息。以下是kube-state-metrics会回答的一些问题:

  • 我调度了多少个replicas?现在可用的有几个?
  • 多少个Pod是running/stopped/terminated状态?
  • Pod重启了多少次?

……等等。一般来说,该模型将采集Kubernetes事件并将其转换为metrics信息。需要Kubernetes 1.2+版本,不过,需要提醒的是metrics名称和标签是不稳定的,可能会改变。

通过以上介绍,至少可以让你了解如何为你的Kubernetes环境构建合理的监控配置。虽然我们仍然没有详细的应用程序监控(例如:“我的数据库服务的响应时间是多少?”),但我们至少可以看到我们的宿主机,Kubernetes节点以及我们的Kubernetes抽象状态的一些监控。

让我们来看看Sysdig进行Kubernetes监控的方式。

使用Sysdig进行Kubernetes监控

在与数百名Kubernetes用户交谈之后,似乎典型的集群管理员往往有兴趣从物理角度来看事物,而负责构建服务的应用程序开发人员往往更倾向于从逻辑角度来看事物。无论你从什么角度进行观察,所有团队都希望减少他们为了测试系统或管理数据收集而需要完成的工作量。

考虑到这两个用例,Sysdig Monitor对Kubernetes的支持现在可以做到:

  1. 通过连接到Kubernetes的集群API服务器并查询API(常规API和观察API)我们现在可以推断出您的微服务应用程序物理和逻辑结构。
  2. 另外,我们透传获取到的重要元数据信息,例如labels。
  3. 这些信息与我们的ContainerVision技术相结合,这使得我们可以检查在容器内运行的应用程序,而不需要对任何容器或应用程序进行检测。检测在每个节点上进行,而不是针对每个Pod。
  4. 因此,使用单一的检测点,你就可以监控你的主机、网络、容器和应用程序——所有这些都使用Kubernetes元数据进行标记。您可以通过Kubernetes中的DaemonSet部署Sysdig。
  5. 然后,你可以根据你的要求,在Sysdig Monitor的云服务或我们的内部部署软件中对此数据进行可视化和报警。

基于此,Sysdig Monitor现在可以从以基础架构为中心和以应用为中心的角度提供丰富的可视性和上下文。 两全其美!

让我们来看看实际是什么样子。

按Kubernetes元数据分组

Sysdig Monitor的核心功能之一就是分组。你可以根据物理层次(例如,AWS region > Host > pod > container),或者基于逻辑微服务层次(例如,namespace > replicaset > pod > container)对容器进行分组和检索

未分类

未分类

如果你对利用你的基础物理资源感兴趣——例如识别noisy neighbors——那么物理层次是较好的选择。但是如果你正在研究应用程序和微服务的性能,那么逻辑层次往往是较好的。

一般来说,与典型Dashboard相比,动态重新组合基础架构的能力是解决环境故障更强大的方法。

分析Kubernetes服务的响应时间

例如:通过单击逻辑表中的Prod -> WordPress,我们会自动获得一个dashboard,不管容器中运行的是主机或数据中心服务,该dashboard可以分析所有容器上聚合的HTTP服务性能。其中一个强大的概况是服务响应时间:跨所有相关容器自动聚合服务延迟,然后将其与资源利用率相关联:

未分类

这使我们能够快速分析服务是否按预期执行,是否与底层资源利用率有关。 现在我们可以更深入服务。

分析Kubernetes HTTP服务的最慢端点

接下来,让我们深入应用程序本身的特定指标——在本演示中使用WordPress应用程序充当我们的HTTP服务,让我们来看看HTTP概况。

未分类

从上图你可以看到我们现在可以深入了解:

  • 最常用的HTTP端点
  • 最慢的HTTP端点
  • 平均连接时间
  • 错误
  • 状态码

实现此服务的Pod可能分散在多台计算机上,但我们仍然可以将此服务的请求计数、响应时间和URL统计信息汇总在一起。不要忘记:这不需要任何配置或检测WordPress、Apache或底层容器。

同样,如果这是RabbitMQ、MySQL、Redis、Consul、Nginx或其他50多个组件服务,我们将以相同的方式执行(尽管每个应用程序的指标会有所不同)。

关联Kubernetes事件

最后,根据你正在检测的任何metrics信息,查看Kubernetes汇报的上下文事件,可能会提供一些有用的线索用以查看你的应用程序的运行过程。Sysdig会自动收集Kubernetes事件,所以你可以这么做。

未分类

从这个视图(或任何其他方面),你可以轻松地为Kubernetes service聚合的metrics信息创建报警策略,而不是容器或node节点。此外,你仍然可以深入到任何单独的容器中进行深度检查直至进程级别。

可视化你的Kubernetes Services

上面的例子让你感受了如何监控一个Kubernetes service的性能。但是如果我们想看到我们所有的service,以及他们如何相互沟通呢?我们通过拓扑视图来完成这个任务。

下面的两张图片展示的是完全相同的基础设施和服务。第一张图描述了物理层次,一个master节点和三个node节点:

未分类

而第二张图将容器分组到Namespace、Service和Pod中,同时抽象容器的物理位置。

未分类

第二张(面向服务的)视图对于监视Kubernetes应用程序是多么自然和直观。应用程序的结构和各种依赖性非常清晰。各种微服务之间的交互变得明显,尽管这些微服务混合在我们的机器群集中!

视频:https://v.qq.com/x/page/c0602kmsnm8.html

总结

主要结论是:如果你有一个非常庞大的部署,你必须开始考虑用一种适合你的方式来监视Kubernetes集群环境。

Skaffold-简化本地开发kubernetes应用的神器

在我们开发kubernetes应用的过程中,一般情况下是我们在本地开发调试测试完成以后,再通过CI/CD的方式部署到kubernetes的集群中,这个过程首先是非常繁琐的,而且效率非常低下,因为你想验证你的每次代码修改,就得提交代码重新走一遍CI/CD的流程,我们知道编译打包成镜像这些过程就是很耗时的,即使我们在自己本地搭建一套开发kubernetes集群,也同样的效率很低。在实践中,若不在本地运行那些服务,调试将变得颇具挑战。就在几天前,我遇到了Skaffold,它是一款命令行工具,旨在促进kubernetes应用的持续开发,Skaffold可以将构建、推送及向kubernetes集群部署应用程序的过程自动化,听上去是不是很舒服呀~~~

介绍

Skaffold是一款命令行工具,旨在促进Kubernetes应用的持续开发。你可以在本地迭代应用源码,然后将其部署到本地或者远程Kubernetes集群中。Skaffold会处理构建、上传和应用部署方面的工作流。它通用可以在自动化环境中使用,例如CI/CD流水线,以实施同样的工作流,并作为将应用迁移到生产环境时的工具 —— Skaffold官方文档

Skaffold的特点:

  • 没有服务器端组件,所以不会增加你的集群开销
  • 自动检测源代码中的更改并自动构建/推送/部署
  • 自动更新镜像TAG,不要担心手动去更改kubernetes的 manifest 文件
  • 一次性构建/部署/上传不同的应用,因此它对于微服务同样完美适配
  • 支持开发环境和生产环境,通过仅一次运行manifest,或者持续观察变更

另外Skaffold是一个可插拔的架构,允许开发人员选择自己最合适的工作流工具

未分类

我们可以通过下面的 gif 图片来了解Skaffold的使用

未分类

使用

要使用Skaffold最好是提前在我们本地安装一套单节点的kubernetes集群,比如minikube或者Docker for MAC/Windows的Edge版

安装

您将需要安装以下组件后才能开始使用Skaffold:

1. skaffold

下载最新的Linux版本,请运行如下命令:

$ curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-amd64 && chmod +x skaffold && sudo mv skaffold /usr/local/bin

下载最新的OSX版本,请运行:

$ curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-darwin-amd64 && chmod +x skaffold && sudo mv skaffold /usr/local/bin

当然如果由于某些原因你不能访问上面的链接的话,则可以前往Skaffold的github release页面下载相应的安装包。

2. Kubernetes集群

其中Minikube, GKE, Docker for Mac(Edge)和Docker for Windows(Edge) 已经过测试,但任何kubernetes群集都是可以使用,为了简单起见,我这里使用的是Docker for Mac(Edge)

3. kubectl

要使用kubernetes那么肯定kubectl也是少不了的,在本地配置上你的目标群集的当前上下文进行开发

4. Docker

这个应该不用多说了吧?

5. Docker 镜像仓库

如果你有私有的镜像仓库,则要先配置上相关的登录认证之类的。我这里为了方便,就直接使用Docker Hub,当然要往上面推送镜像的话,你得提前去docker hub注册一个帐号

开发

我们可以在本地开发一个非常简单的应用程序,然后通过Skaffold来进行迭代开发,这里我们直接使用Skaffold的官方示例,首先clone代码:

$ git clone https://github.com/GoogleCloudPlatform/skaffold

然后我们定位到examples/getting-started目录下去:

$ cd examples/getting-started
$ tree .
.
├── Dockerfile
├── k8s-pod.yaml
├── main.go
├── skaffold-gcb.yaml
└── skaffold.yaml

0 directories, 5 files

该目录下面有一个非常简单的golang程序:(main.go)

package main
import (
    "fmt"
    "time"
)

func main() {
    for {
        fmt.Println("Hello Skaffold!")
        time.Sleep(time.Second * 2)
    }
}

其中skaffold-gcb.yaml文件我们可以暂时忽略,这个文件是和google cloud结合使用的,我们可以看下skaffold.yaml文件内容,这里我已经将镜像名称改成了我自己的了(cnych/skaffold-example),如下:

apiVersion: skaffold/v1alpha1
kind: Config
build:
  artifacts:
  - imageName: cnych/skaffold-example
    workspace: .
  local: {}
deploy:
  kubectl:
    manifests:
    - paths:
      - k8s-*
      parameters:
        IMAGE_NAME: cnych/skaffold-example

然后我们可以看到k8s-pod.yaml文件,其中的镜像名称是一个IMAGE_NAME的参数,这个地方Skaffold会自动帮我们替换成正在的镜像地址的,所以不用我们做任何更改了,如下:

apiVersion: v1
kind: Pod
metadata:
  name: getting-started
spec:
  containers:
  - name: getting-started
    image: IMAGE_NAME

然后我们就可以在getting-started目录下面执行skaffold dev命令了:

$ skaffold dev
Starting build...
Found minikube or Docker for Desktop context, using local docker daemon.
Sending build context to Docker daemon  6.144kB
Step 1/5 : FROM golang:1.9.4-alpine3.7
 ---> fb6e10bf973b
Step 2/5 : WORKDIR /go/src/github.com/GoogleCloudPlatform/skaffold/examples/getting-started
 ---> Using cache
 ---> e6ae5322ee52
Step 3/5 : CMD ["./app"]
 ---> Using cache
 ---> bac5f3fd392e
Step 4/5 : COPY main.go .
 ---> Using cache
 ---> 47fa1e536263
Step 5/5 : RUN go build -o app main.go
 ---> Using cache
 ---> f1470fe9f398
Successfully built f1470fe9f398
Successfully tagged a250d03203f9a5df267d8ad63bae8dba:latest
Successfully tagged cnych/skaffold-example:f1470fe9f3984775f5dea87b5f720d67b6c2eeaaf2ca5efd1ca3c3ec7c4d4cce
Build complete.
Starting deploy...
Deploying k8s-pod.yaml...
Deploy complete.
Dependencies may be incomplete.
[getting-started getting-started] Hello Skaffold!
[getting-started getting-started] Hello Skaffold!

Skaffold已经帮我们做了很多事情了:

  • 用本地源代码构建 Docker 镜像
  • 用它的sha256值作为镜像的标签
  • 设置skaffold.yaml文件中定义的 kubernetes manifests 的镜像地址
  • 用kubectl apply -f命令来部署 kubernetes 应用

部署完成后,我们可以看到 pod 打印出了如下的信息:

[getting-started getting-started] Hello Skaffold!
[getting-started getting-started] Hello Skaffold!
[getting-started getting-started] Hello Skaffold!

同样的,我们可以通过kubectl工具查看当前部署的 POD:

$ kubectl get pods
NAME              READY     STATUS    RESTARTS   AGE
getting-started   1/1       Running   3          1h

然后我们可以打印出上面的 POD 的详细信息:

$ kubectl get pod getting-started  -o yaml
...
spec:
  containers:
  - image: cnych/skaffold-example:f1470fe9f3984775f5dea87b5f720d67b6c2eeaaf2ca5efd1ca3c3ec7c4d4cce
    imagePullPolicy: IfNotPresent
    name: getting-started
...

我们可以看到我们部署的 POD 的镜像地址,和我们已有的 docker 镜像地址和标签是一样的:

$ docker images |grep skaffold
cnych/skaffold-example                                    f1470fe9f3984775f5dea87b5f720d67b6c2eeaaf2ca5efd1ca3c3ec7c4d4cce   f1470fe9f398        8 minutes ago       271MB

现在,我们来更改下我们的main.go文件:

package main
import (
    "fmt"
    "time"
)

func main() {
    for {
        fmt.Println("Hello blog.qikqiak.com!")
        time.Sleep(time.Second * 2)
    }
}

当我们保存该文件后,观察 POD 的输出信息:

[getting-started getting-started] Hello Skaffold!
[getting-started getting-started] Hello Skaffold!
[getting-started getting-started] Hello blog.qikqiak.com!
[getting-started getting-started] Hello blog.qikqiak.com!
[getting-started getting-started] Hello blog.qikqiak.com!
[getting-started getting-started] Hello blog.qikqiak.com!
[getting-started getting-started] Hello blog.qikqiak.com!

是不是立刻就变成了我们修改的结果了啊,同样我们可以用上面的样式去查看下 POD 里面的镜像标签是已经更改过了。

总结

我这里为了说明Skaffold的使用,可能描述显得有点拖沓,但是当你自己去使用的时候,就完全能够感受到Skaffold为开发kubernetes应用带来的方便高效,大大的提高了我们的生产力。 另外在kubernetes开发自动化工具领域,还有一些其他的选择,比如Azure的Draft、Datawire 的 Forge 以及 Weavework 的 Flux,大家都可以去使用一下,其他微软的Draft是和Helm结合得非常好,不过Skaffold当然也是支持的,工具始终是工具,能为我们提升效率的就是好工具,不过从开源的角度来说,信 Google 准没错。

参考资料

  • https://github.com/GoogleCloudPlatform/skaffold
  • https://draft.sh/