k8s

Rook用还是不用,这就是Kubernetes

就在今年的五月份,Rook官方宣告Rook 1.0.0主版本发布了,“可用于Kubernetes,已经生产就绪的云原生存储”。大约在一年前,这个解决方案首次出现在我们的视野里,但是距离我们实际使用它又过去了一段时间。最后,我们很高兴在这里分享我们学到的一些经验教训。

0_MrxXNO55N-rSYLEb.png

简而言之,Rook就是一组Kubernetes的Operator,它可以完全控制多种数据存储解决方案(例如Ceph、EdgeFS、Minio、Cassandra)的部署,管理以及自动恢复。

到目前为止,rook-ceph-operator仍然是最先进(也是唯一一个稳定的)解决方案。

注意:Rook 1.0.0这次发布了一些和Ceph有关的显著特性,包括对Ceph Nautilus的支持,以及启动NFS守护进程用于导出CephFS卷或者RGW桶的CephNFS CRD。此外,新版本还加入了期待已久的对EdgeFS的beta级支持。

在本文中,我们将会:

  • 解答在Kubernetes集群里使用Rook来部署Ceph有什么好处的问题;
  • 分享我们在生产环境使用Rook的一些经验和见解;
  • 解释我们为什么对Rook说”Yes”的原因并分享我们的未来规划。

但是现在,让我们先从一些通俗的概念和理论讲起吧。

“Rook是我的强项!”(来自一位不知名的棋手)

0_DGLCeT9iunOYK6Oh.png

使用Rook的其中一个主要好处在于它是通过原生的Kubernetes机制和数据存储交互。这就意味着你不再需要通过命令行手动配置Ceph。

  • 你想要在一个集群里部署CephFS吗?只需要创建一个YAML文件就行了!
  • 什么?你还想要部署一个支持S3 API的对象存储?行,另外再建一个YAML文件就行!

Rook具备了一个典型的Kubernetes Operator的所有功能。与它的交互依赖于自定义资源定义(CRD)来描述我们需要的Ceph实例的一些属性(除非另有明确说明,否则在本文的其余部分中,我们将会隐去Ceph的字眼,因为它是Rook目前唯一稳定的存储解决方案)。通过使用给定的参数,一个Operator将会自动执行设置所需的命令。

我们不妨看一个创建对象存储的具体示例,以CephObjectStoreUser为例:

apiVersion: ceph.rook.io/v1
kind: CephObjectStore
metadata:
name: {{ .Values.s3.storeName }}
namespace: kube-rook
spec:
metadataPool:
failureDomain: host
replicated:
  size: 3
dataPool:
failureDomain: host
erasureCoded:
  dataChunks: 2
  codingChunks: 1
gateway:
type: s3
sslCertificateRef:
port: 80
securePort:
instances: 1
allNodes: false
---
apiVersion: ceph.rook.io/v1
kind: CephObjectStoreUser
metadata:
name: {{ .Values.s3.username }}
namespace: kube-rook
spec:
store: {{ .Values.s3.storeName }}
displayName: {{ .Values.s3.username }} 

上述清单里列出的参数都是一些常见配置,无需进一步说明。不过这里要重点关注一下参数值指定为模板变量的部分。

一般流程的结构如下:我们会通过一个YAML文件请求资源,然后一个operator会去执行所有必要的命令并返回一个“不那么真实”的secret,我们可以据此进行接下来的工作(参见下文)。然后,基于上述提供的变量,Rook会帮助用户生成要执行的命令以及secret的名称。

什么命令呢?当Rook Operator为对象存储创建一个用户时,它会在Pod里执行这条命令:

radosgw-admin user create --uid={{ .Values.s3.username }} --display-name={{ .Values.s3.username }} --rgw-realm={{ .Values.s3.storeName }} --rgw-zonegroup={{ .Values.s3.storeName }} 

它会生成如下结构的JSON数据:

{
"user_id": "{{ .Values.s3.username }}",
"display_name": "{{ .Values.s3.username }}",
"keys": [
    {
       "user": "{{ .Values.s3.username }}",
       "access_key": "NRWGT19TWMYOB1YDBV1Y",
       "secret_key": "gr1VEGIV7rxcP3xvXDFCo4UDwwl2YoNrmtRlIAty"
    }
],
...
} 

这里的keys在将来用于通过S3 API为应用程序提供访问对象存储的权限。Rook Operator将它们照单全收,然后以rook-ceph-object-user-{{ .Values.s3.crdName }}-{{ .Values.s3.username }}的格式作为secret存储到它的namespace里。

为了使用保存到secret里的数据,所有你要做的就是通过环境变量把它们传入到容器里。这里有一个Job模板的示例,它会自动给每个用户环境创建对应的bucket:

{{- range $bucket := $.Values.s3.bucketNames }}
apiVersion: batch/v1
kind: Job
metadata:
name: create-{{ $bucket }}-bucket-job
namespace: kube-rook
annotations:
"helm.sh/hook": post-install, post-upgrade
"helm.sh/hook-weight": "2"
spec:
template:
metadata:
  name: create-{{ $bucket }}-bucket-job
spec:
  restartPolicy: Never
  initContainers:
  - name: waitdns
    image: alpine:3.6
    command: ["/bin/sh", "-c"]
    args:
    - "while ! getent ahostsv4 rook-ceph-rgw-{{ $.Values.s3.storeName }}; do sleep 1; done"
  - name: config
    image: rook/toolbox:v0.7.1
    command: ["/bin/sh", "-c"]
    args:
    - >-
      s3cmd --configure --access_key=$(ACCESS-KEY) --secret_key=$(SECRET-KEY)
      --no-ssl --dump-config
      --host=rook-ceph-rgw-{{ $.Values.s3.storeName }}
      --host-bucket=rook-ceph-rgw-{{ $.Values.s3.storeName }}
      | tee /config/.s3cfg
    volumeMounts:
      - name: config
        mountPath: /config
    env:
    - name: ACCESS-KEY
      valueFrom:
        secretKeyRef:
          name: rook-ceph-object-user-{{ $.Values.s3.storeName }}-{{ $.Values.s3.username }}
          key: AccessKey
    - name: SECRET-KEY
      valueFrom:
        secretKeyRef:
          name: rook-ceph-object-user-{{ $.Values.s3.storeName }}-{{ $.Values.s3.username }}
          key: SecretKey
  containers:
  - name: create-bucket
    image: rook/toolbox:v0.7.1
    command: ["s3cmd", "mb", "s3://{{ $bucket }}"]
    ports:
    - name: s3-no-sll
      containerPort: 80
    volumeMounts:
    - name: config
      mountPath: /root
  volumes:
  - name: config
    emptyDir: {}
---
{{- end }} 

这个Job的所有操作仅在Kubernetes内部执行。YAML文件里描述的数据结构已经被添加到了Git仓库里以便复用。对于DevOps工程师和整体的CI/CD流程而言,这个功能真是棒极了。目前,Rook团队已经计划使用Bucket置备库,这将会使你和S3 bucket的交互更加方便。

通过Rook处理RADOS

传统的Ceph + RBD组合在为Pod挂载卷时施加了特定的限制。

换句话说,namespace必须包含访问Ceph的secret这样有状态应用才可以正常工作。如果你有,比方说,namespace里有2-3个环境的话,这很容易做到:你可以手动复制secret。但是如果针对每个功能都创建一个具有自己的namespace的单独环境的话该怎么办呢?我们已经通过shell-operator解决了这个问题,它会自动将secret复制到新的namespace里(在这篇文章中你可以找到一个这样的hook示例)。

#!/bin/bash
if [[ $1 == "--config" ]]; then
cat <<EOF
{"onKubernetesEvent":[
{"name": "OnNewNamespace",
"kind": "namespace",
"event": ["add"]
}
]}
EOF
else
NAMESPACE=$(kubectl get namespace -o json | jq '.items | max_by( .metadata.creationTimestamp ) | .metadata.name')
kubectl -n ${CEPH_SECRET_NAMESPACE} get secret ${CEPH_SECRET_NAME} -o json | jq ".metadata.namespace=\"${NAMESPACE}\"" | kubectl apply -f -
fi

但是,如果使用Rook的话就不存在这个问题了。卷的挂载流程是基于Flexvolume或者CSI(目前还是beta阶段)驱动,不需要任何secret。

Rook通过一些手段自动化地自行解决了这些问题,因此我们倾向于将它用于我们的新项目。

上手Rook

让我们通过安装Rook和Ceph来结束实战部分,这样你便可以实际上手体验。为了简化安装过程,开发人员提供了一个Helm包。让我们下载它:

$ helm fetch rook-master/rook-ceph --untar --version 1.0.0

你可以在rook-ceph/values.yaml文件里找到许多不同的设置。这里最重要的是为discover和agent指定toleration。这里我们不打算过多涉及Kubernetes里的taints和tolerations的细节,你只需要知道我们不想让应用的Pod调度到带有数据存储盘的节点上即可。理由也是显而易见的:这样做的话,我们可以避免Rook的Agent影响应用程序本身。

如今是时候在你喜欢的文本编辑器里打开rook-ceph/values.yaml然后将下列部分追加到文件的末尾:

discover:
toleration: NoExecute
tolerationKey: node-role/storage
agent:
toleration: NoExecute
tolerationKey: node-role/storage
mountSecurityMode: Any

我们还为每个保留用于数据存储的节点打上了相应的污点(taint):

$ kubectl taint node ${NODE_NAME}  node-role/storage="":NoExecute

然后通过下列命令安装Helm chart:

$ helm install --namespace ${ROOK_NAMESPACE} ./rook-ceph

现在我们可以去创建集群,然后指定OSD的路径:

apiVersion: ceph.rook.io/v1
kind: CephCluster
metadata:
clusterName: "ceph"
finalizers:
- cephcluster.ceph.rook.io
generation: 1
name: rook-ceph
spec:
cephVersion:
image: ceph/ceph:v13
dashboard:
enabled: true
dataDirHostPath: /var/lib/rook/osd
mon:
allowMultiplePerNode: false
count: 3
network:
hostNetwork: true
rbdMirroring:
workers: 1
placement:
all:
  tolerations:
  - key: node-role/storage
    operator: Exists
storage:
useAllNodes: false
useAllDevices: false
config:
  osdsPerDevice: "1"
  storeType: bluestore
nodes:
- name: ceph01
  deviceFilter: ^sd[b-i]
- name: ceph02
  deviceFilter: ^sd[b-i]
- name: ceph03
  deviceFilter: ^sd[b-i]

Ceph的状态应当是HEALTH_OK

$ kubectl -n ${ROOK_NAMESPACE} exec $(kubectl -n ${ROOK_NAMESPACE} get pod -l app=rook-ceph-operator -o name -o jsonpath='{.items[0].metadata.name}') -- ceph -s

我们还要确保应用程序的Pod不会调度到那些为Ceph保留的节点上去:

$ kubectl -n ${APPLICATION_NAMESPACE} get pods -o custom-columns=NAME:.metadata.name,NODE:.spec.nodeName

现在你可以定制其他额外的组件了。相关的信息可以在文档里找到。为了能够让我们在集群里的后续操作变得更加顺滑,我们强烈推荐开启Dashboard并且部署toolbox

人无完人:Rook的不足之处是什么?

如你所见,Rook的开发进展顺利。但是存在一些问题,使得一些Ceph的手动配置仍然无法避免。

  • 目前没有Rook驱动可以导出展示已挂载的区块的使用情况的指标数据,因此我们无法监控它们的状态(Rook v1.0.3在Flexvolume卷上加入了这项功能)。
  • Flexvolume和CSI(与RBD不同)无法调整卷的大小,因此Rook缺乏这项有用的(有时甚至非常关键)工具。(在写完这篇文章后不久,业内已经出现了一些实现方案但是也仅仅只是针对Flexvolume;不过也有讨论到CSI)
  • Rook仍然不如Ceph灵活。举个例子,要将CephFS元数据存储到SSD上,然后将相关数据存储到HDD时,你必须得在CRUSH映射里手动定义每组设备。
  • 尽管rook-ceph-operator被视为已经稳定了,但是从Ceph 13升级到14版本的过程中仍然会遇到一些问题。

 

小结

“今天Rook是通过小兵们来抵御外面的世界,但是终有一天它会在游戏里扮演一个至关重要的角色!”(这句话简直像是为本文量身定制的。)

我们无疑非常喜欢Rook项目,尽管有许多利弊,但是我们相信它绝对值得你的关注。

我们未来的计划归结为将rook-ceph变成我们addon-operator的一个模块。这样一来,我们可以很轻松方便地在我们维护的大量Kubernetes集群里使用它。

ConfigMap使用方法

ConfigMap概览

  • ConfigMap API资源用来保存key-value pair配置数据,这个数据可以在pods里使用,或者被用来为像controller一样的系统组件存储配置数据。虽然ConfigMap跟Secrets类似,但是ConfigMap更方便的处理不含敏感信息的字符串。 注意:ConfigMaps不是属性配置文件的替代品。ConfigMaps只是作为多个properties文件的引用。你可以把它理解为Linux系统中的/etc目录,专门用来存储配置文件的目录。
  • 下面举个例子,使用ConfigMap配置来创建Kuberntes Volumes,ConfigMap中的每个data项都会成为一个新文件。

kind: ConfigMap
apiVersion: v1
metadata:
  creationTimestamp: 2016-02-18T19:14:38Z
  name: example-config
  namespace: default
data:
  example.property.1: hello
  example.property.2: world
  example.property.file: |-
    property.1=value-1
    property.2=value-2
    property.3=value-3
  • data一栏包括了配置数据,ConfigMap可以被用来保存单个属性,也可以用来保存一个配置文件。
  • 配置数据可以通过很多种方式在Pods里被使用。
  • ConfigMaps可以被用来:

设置环境变量的值
在容器里设置命令行参数
在数据卷里面创建config文件
用户和系统组件两者都可以在ConfigMap里面存储配置数据。
  • 其实不用看下面的文章,直接从kubectl create configmap -h的帮助信息中就可以对ConfigMap究竟如何创建略知一二了。

Examples:
  # Create a new configmap named my-config based on folder bar
  kubectl create configmap my-config --from-file=path/to/bar

  # Create a new configmap named my-config with specified keys instead of file basenames on disk
  kubectl create configmap my-config --from-file=key1=/path/to/bar/file1.txt --from-file=key2=/path/to/bar/file2.txt

  # Create a new configmap named my-config with key1=config1 and key2=config2
  kubectl create configmap my-config --from-literal=key1=config1 --from-literal=key2=config2

创建ConfigMaps

  • 可以使用该命令,用给定值、文件或目录来创建ConfigMap。

使用目录创建

  • 比如我们已经有个了包含一些配置文件,其中包含了我们想要设置的ConfigMap的值:

$ ls docs/user-guide/configmap/kubectl/
game.properties
ui.properties

$ cat docs/user-guide/configmap/kubectl/game.properties
enemies=aliens
lives=3
enemies.cheat=true
enemies.cheat.level=noGoodRotten
secret.code.passphrase=UUDDLRLRBABAS
secret.code.allowed=true
secret.code.lives=30

$ cat docs/user-guide/configmap/kubectl/ui.properties
color.good=purple
color.bad=yellow
allow.textmode=true
how.nice.to.look=fairlyNice
  • 使用下面的命令可以创建一个包含目录中所有文件的ConfigMap。

$ kubectl create configmap game-config --from-file=docs/user-guide/configmap/kubectl
  • —from-file指定在目录下的所有文件都会被用在ConfigMap里面创建一个键值对,键的名字就是文件名,值就是文件的内容。
  • 让我们来看一下这个命令创建的ConfigMap:

$ kubectl describe configmaps game-config
Name:           game-config
Namespace:      default
Labels:         <none>
Annotations:    <none>

Data
====
game.properties:        158 bytes
ui.properties:          83 bytes
  • 我们可以看到那两个key是从kubectl指定的目录中的文件名。这些key的内容可能会很大,所以在kubectl describe的输出中,只能够看到键的名字和他们的大小。
  • 如果想要看到键的值的话,可以使用kubectl get:

$ kubectl get configmaps game-config -o yaml
  • 我们以yaml格式输出配置。

apiVersion: v1
data:
  game.properties: |
    enemies=aliens
    lives=3
    enemies.cheat=true
    enemies.cheat.level=noGoodRotten
    secret.code.passphrase=UUDDLRLRBABAS
    secret.code.allowed=true
    secret.code.lives=30
  ui.properties: |
    color.good=purple
    color.bad=yellow
    allow.textmode=true
    how.nice.to.look=fairlyNice
kind: ConfigMap
metadata:
  creationTimestamp: 2016-02-18T18:34:05Z
  name: game-config
  namespace: default
  resourceVersion: "407"
  selfLink: /api/v1/namespaces/default/configmaps/game-config
  uid: 30944725-d66e-11e5-8cd0-68f728db1985

使用文件创建

  • 刚才使用目录创建的时候我们—from-file指定的是一个目录,只要指定为一个文件就可以从单个文件中创建ConfigMap。

$ kubectl create configmap game-config-2 --from-file=docs/user-guide/configmap/kubectl/game.properties 

$ kubectl get configmaps game-config-2 -o yaml
apiVersion: v1
data:
  game-special-key: |
    enemies=aliens
    lives=3
    enemies.cheat=true
    enemies.cheat.level=noGoodRotten
    secret.code.passphrase=UUDDLRLRBABAS
    secret.code.allowed=true
    secret.code.lives=30
kind: ConfigMap
metadata:
  creationTimestamp: 2016-02-18T18:54:22Z
  name: game-config-3
  namespace: default
  resourceVersion: "530"
  selfLink: /api/v1/namespaces/default/configmaps/game-config-3
  uid: 05f8da22-d671-11e5-8cd0-68f728db1985
  • —from-file这个参数可以使用多次,你可以使用两次分别指定上个实例中的那两个配置文件,效果就跟指定整个目录是一样的。

使用字面值创建

  • 使用文字值创建,利用—from-literal参数传递配置信息,该参数可以使用多次,格式如下;

$ kubectl create configmap special-config --from-literal=special.how=very --from-literal=special.type=charm

$ kubectl get configmaps special-config -o yaml
apiVersion: v1
data:
  special.how: very
  special.type: charm
kind: ConfigMap
metadata:
  creationTimestamp: 2016-02-18T19:14:38Z
  name: special-config
  namespace: default
  resourceVersion: "651"
  selfLink: /api/v1/namespaces/default/configmaps/special-config
  uid: dadce046-d673-11e5-8cd0-68f728db1985

Pod中使用ConfigMap

  • 使用ConfigMap来替代环境变量
  • ConfigMap可以被用来填入环境变量。看下下面的ConfigMap。

apiVersion: v1
kind: ConfigMap
metadata:
  name: special-config
  namespace: default
data:
  special.how: very
  special.type: charm
apiVersion: v1
kind: ConfigMap
metadata:
  name: env-config
  namespace: default
data:
  log_level: INFO
  • 我们可以在Pod中这样使用ConfigMap:

apiVersion: v1
kind: Pod
metadata:
  name: dapi-test-pod
spec:
  containers:
    - name: test-container
      image: gcr.io/google_containers/busybox
      command: [ "/bin/sh", "-c", "env" ]
      env:
        - name: SPECIAL_LEVEL_KEY
          valueFrom:
            configMapKeyRef:
              name: special-config
              key: special.how
        - name: SPECIAL_TYPE_KEY
          valueFrom:
            configMapKeyRef:
              name: special-config
              key: special.type
      envFrom:
        - configMapRef:
            name: env-config
  restartPolicy: Never
  • 这个Pod运行后会输出如下几行:

SPECIAL_LEVEL_KEY=very
SPECIAL_TYPE_KEY=charm
log_level=INFO

用ConfigMap设置命令行参数

  • ConfigMap也可以被使用来设置容器中的命令或者参数值。
  • 它使用的是Kubernetes的$(VAR_NAME)替换语法。
  • 我们看下下面这个ConfigMap。

apiVersion: v1
kind: ConfigMap
metadata:
  name: special-config
  namespace: default
data:
  special.how: very
  special.type: charm
  • 为了将ConfigMap中的值注入到命令行的参数里面,我们还要像前面那个例子一样使用环境变量替换语法${VAR_NAME)。
  • (其实这个东西就是给Docker容器设置环境变量,以前我创建镜像的时候经常这么玩,通过docker run的时候指定-e参数修改镜像里的环境变量,然后docker的CMD命令再利用该$(VAR_NAME)通过sed来修改配置文件或者作为命令行启动参数。)

apiVersion: v1
kind: Pod
metadata:
  name: dapi-test-pod
spec:
  containers:
    - name: test-container
      image: gcr.io/google_containers/busybox
      command: [ "/bin/sh", "-c", "echo $(SPECIAL_LEVEL_KEY) $(SPECIAL_TYPE_KEY)" ]
      env:
        - name: SPECIAL_LEVEL_KEY
          valueFrom:
            configMapKeyRef:
              name: special-config
              key: special.how
        - name: SPECIAL_TYPE_KEY
          valueFrom:
            configMapKeyRef:
              name: special-config
              key: special.type
  restartPolicy: Never
  • 运行这个Pod后会输出:

very charm

通过数据卷插件使用ConfigMap

  • ConfigMap也可以在数据卷里面被使用。还是这个ConfigMap。

apiVersion: v1
kind: ConfigMap
metadata:
  name: special-config
  namespace: default
data:
  special.how: very
  special.type: charm
  • 在数据卷里面使用这个ConfigMap,有不同的选项。最基本的就是将文件填入数据卷,在这个文件中,键就是文件名,键值就是文件内容:

apiVersion: v1
kind: Pod
metadata:
  name: dapi-test-pod
spec:
  containers:
    - name: test-container
      image: gcr.io/google_containers/busybox
      command: [ "/bin/sh", "-c", "cat /etc/config/special.how" ]
      volumeMounts:
      - name: config-volume
        mountPath: /etc/config
  volumes:
    - name: config-volume
      configMap:
        name: special-config
  restartPolicy: Never

*运行这个Pod的输出是very。

  • 我们也可以在ConfigMap值被映射的数据卷里控制路径。

apiVersion: v1
kind: Pod
metadata:
  name: dapi-test-pod
spec:
  containers:
    - name: test-container
      image: gcr.io/google_containers/busybox
      command: [ "/bin/sh","-c","cat /etc/config/path/to/special-key" ]
      volumeMounts:
      - name: config-volume
        mountPath: /etc/config
  volumes:
    - name: config-volume
      configMap:
        name: special-config
        items:
        - key: special.how
          path: path/to/special-key
  restartPolicy: Never
  • 运行这个Pod后的结果是very。

这里有几点需要注意。

挂载目录下的文件名称,即为cm定义里的key值。
挂载目录下的文件的内容,即为cm定义里的value值。value可以多行定义,这在一些稍微复杂的场景下特别有用,比如 my.cnf。
如果挂载目录下原来有文件,挂载后将不可见(AUFS)。
有的时候,我们希望将文件挂载到某个目录,但希望只是挂载该文件,不要影响挂载目录下的其他文件。有办法吗?

可以用subPath: Path within the volume from which the container’s volume should be mounted。Volume中待挂载的子目录。

subPath 的目的是为了在单一Pod中多次使用同一个volume而设计的。

例如,像下面的LAMP,可以将同一个volume下的 mysql 和 html目录,挂载到不同的挂载点上,这样就不需要为 mysql 和 html 单独创建volume了。

apiVersion: v1
kind: Pod
metadata:
  name: my-lamp-site
spec:
    containers:
    - name: mysql
      image: mysql
      env:
      - name: MYSQL_ROOT_PASSWORD
        value: "rootpasswd" 
      volumeMounts:
      - mountPath: /var/lib/mysql
        name: site-data
        subPath: mysql
    - name: php
      image: php:7.0-apache
      volumeMounts:
      - mountPath: /var/www/html
        name: site-data
        subPath: html
    volumes:
    - name: site-data
      persistentVolumeClaim:
        claimName: my-lamp-site-data

那怎么解决我们的问题呢?很简单,如果subPath不是目录,而是一个文件,那么就可以达到单独挂载的目的了。

containers:
- volumeMounts:
  - name: demo-config
    mountPath: /etc/special.type
    subPath: special.type
volumes:
- name: demo-config
  configMap:
    name: special-config

作者:Rick_Ji
链接:https://www.jianshu.com/p/9d55b4a4eeab
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Kubernetes工作负载监控:Sidecar模式

Kubernetes彻底改变了构建基础架构的方式,加快了部署速度,并使我们能够复制和扩展微服务架构。但是,Kubernetes在应用可见性和监控功能方面,却面临一系列新的挑战。

在这篇文章中,我通过回顾Sensu首席执行官Caleb Hailey在CNCF的讨论,如:一些现有的和流行的Kubernetes监控模式(例如Prometheus),以及为什么传统方法在云原生世界中的不足。除此之外,我还将介绍用于监控Kubernetes上的工作负载的当前最佳实践–sidecar模式。

容器:现状

长期以来,我们一直依靠微服务,实现在云中更快,更可靠地交付软件。容器化是微服务发展过程中自然而然的下一步,它与Kubernetes一起进行容器编排,迫使我们重新考虑如何部署和监控应用程序。

尽管Kubernetes是一个功能强大的平台,可用于在任何地方运行应用程序,但这种功能带来了更多的复杂性。

Kubernetes监控:挑战和数据来源

采用Kubernetes意味着要争夺各种各样的数据源。这些数据源都是抽象的,因此要了解系统的运行状况和性能,你需要访问堆栈中的监控数据。

Sensu首席技术官Sean Porter 之前在一篇文章中详细概述了数据源。简而言之,你需要能够从四个主要来源收集数据:

  1. 运行Kubelet的Kubernetes主机,从这些主机中获取数据的最常见方法是使用 Prometheus node exporter ,该程序从Kubernetes主机上抓取数据并在通过HTTP方式暴露。
  2. Kubernetes process即Kubelet metrics,这些提供了有关Kubernetes节点及其运行 jobs 的详细信息。
  3. Kubelet的内置cAdvisor,跟踪正在运行的容器的资源使用情况。
  4. Kube-state-metrics, Kubernetes集群级别的全局视图

Kubernetes的动态特性,要求一种动态的监控方法–并可以应对大量的分布式应用程序和临时基础设施。

Kubernetes监控策略

监控策略概括如下:

  • 远程轮询 -轮询设备并报告其健康状况的传统服务检查方法。
  • 基于节点(每个主机的代理) -监控代理位于Kubernetes主机上,或者在Kubernetes集群内部以DaemonSet进行部署,以获得对系统资源信息的访问。
  • Sidecar(每个Pod的代理) -用于监控Kubernetes系统及其正在运行的工作负载。
  • 日志和APM-日志数据和应用程序性能的管理。

考虑到这些可观察性方法,让我们仔细研究监控Kubernetes的一种更传统的方法:使用Prometheus。

Kubernetes:Prometheus监控

有篇很不错的文章—Prometheus成为Kubernetes出色的基于遥测的监控伴侣的方法

使用Prometheus监控有个好处,就是开发人员不用修改他们的deployment的yaml文件。

但是,如果你需要服务的功能运行状况检查,Prometheus则无法反映“是否健康?”。

更全面的监控方法:Sidecar模式

Kubernetes监控的 sidecar 模式是一种更具动态性的方法。Sidecar作为一种模式并不是Kubernetes的正式约定,但是随着Kubernetes的发展,Sidecar也越来越普及。

Kubernetes的Sidecar示例包括:

  • 服务网格
  • 使用作为sidecar运行的代理,来记录平台的日志
  • 监控解决方案(例如Sensu),他们以sidecar方式运行代理,可以为每个服务一对一地配对监控代理。

当你使用sidecar模式时,你的Kubernetes pod将运行应用程序的容器与运行Sensu代理的容器保持在一起。这些容器共享相同的网络空间,因此你的应用程序可以与Sensu通信,它们就像在同一容器或主机中运行一样。

Sidecar具有和微服务在云原生环境中相同的优势,如:

  • 模块化的
  • 可组合
  • 可重用

演示:Sidecar方式监控应用程序

在本教程中,我们将使用Kubectl在Kubernetes中部署应用程序(NGINX网站)。在演示的第二部分中,我们将展示如何监控已部署的应用程序。

如果你想再本地使用演示,则需要安装Kubernetes(可以通过Minikube工具在本地运行Kubernetes)并将Sensu部署到Kubernetes中

完整示例: https://github.com/calebhailey/monitoring-k8s-workloads

在kubernetes中部署Sensu,请参考 https://github.com/sensu/sensu-kube-demo

首先,使用kubectl apply命令部署NGINX部署:

nginx-deployment.yaml

---
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  selector:
    app: nginx
  ports:
    - name: http
      protocol: TCP
      port: 8000
      targetPort: 80
  type: LoadBalancer

---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  minReadySeconds: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      name: nginx
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - protocol: TCP
          containerPort: 80

部署

$ kubectl create namespace webinar
$ kubectl --namespace webinar apply -f kubernetes/nginx-deployment.yaml
$ kubectl --namespace webinar get services
NAME    TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)          AGE
nginx   LoadBalancer   10.27.251.183   35.230.122.31   8000:30161/TCP   33d

NGINX容器默认在80端口上运行。我通过8000端口暴露了它。

现在,在浏览器中输入http://35.230.122.31:8000访问你的NGINX应用程序。

你已经在Kubernetes中部署了应用程序或服务,但是如何找出监控它的最佳方法。

让我们看看如何将Sensu Go代理作为 sidecar 添加到你的应用程序中。(这适用于所有Kubernetes控制器: deployments ,Statefulsets,DaemonSet等)。

nginx-deployment-with-sensu-sidecar.yaml

---
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  selector:
    app: nginx
  ports:
    - name: http
      protocol: TCP
      port: 8000
      targetPort: 80
  type: LoadBalancer

---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  minReadySeconds: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      name: nginx
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - protocol: TCP
          containerPort: 80
      - name: sensu-agent
        image: sensu/sensu:5.11.1
        command: ["/opt/sensu/bin/sensu-agent", "start", "--log-level=debug", "--insecure-skip-tls-verify", "--keepalive-interval=5", "--keepalive-timeout=10"]
        env:
        - name: SENSU_BACKEND_URL
          value: wss://sensu-backend-0.sensu.sensu-system.svc.cluster.local:8081 wss://sensu-backend-1.sensu.sensu-system.svc.cluster.local:8081 wss://sensu-backend-2.sensu.sensu-system.svc.cluster.local:8081
        - name: SENSU_NAMESPACE
          value: webinar
        - name: SENSU_SUBSCRIPTIONS
          value: linux kubernetes nginx
        - name: SENSU_DEREGISTER
          value: "true"
        - name: SENSU_STATSD_EVENT_HANDLERS
          value: statsd

使用kubectl apply以下命令更新你的deployment:

$ kubectl --namespace webinar apply -f 
kubernetes/nginx-deployment-with-sensu-sidecar.yaml

访问Sensu面板,将看到Sensu自动注册Sidecar。(Sensu还会自动注销节点,因此你可以分辨出发生故障的节点与已取消配置的节点之间的区别。)

Kubernetes中deployment 的.spec.template.spec.containers:

- name: sensu-agent
  image: sensu/sensu:5.11.1
  command: ["/opt/sensu/bin/sensu-agent", "start", "--log-level=debug", "--insecure-skip-tls-verify", "--keepalive-interval=5", "--keepalive-timeout=10"]
  env:
  - name: SENSU_BACKEND_URL
    value: wss://sensu-backend-0.sensu.sensu-system.svc.cluster.local:8081 wss://sensu-backend-1.sensu.sensu-system.svc.cluster.local:8081 wss://sensu-backend-2.sensu.sensu-system.svc.cluster.local:8081
  - name: SENSU_NAMESPACE
    value: webinar
  - name: SENSU_SUBSCRIPTIONS
    value: linux kubernetes nginx
  - name: SENSU_DEREGISTER
    value: "true"
  - name: SENSU_STATSD_EVENT_HANDLERS
    value: statsd

现在已经有了此配置,你可以轻松扩展资源。

$ kubectl --namespace webinar scale deployment nginx --replicas 10

在配置监控检查之前,我们有必要了解下有关Sensu的体系结构以及服务如何通信的背景知识。

Sensu代理以pub-sub模型与Sensu后端进行通信,并且该代理仅要求出站网络访问,因此我们不必打开代理中或Pod上的端口即可访问它们。

代理在Sensu Go中具有TLS加密的Web套接字,它们与Sensu后端保持此连接,并内置支持高可用性。

当代理连接时,它会自我描述其角色。如果是 sidecar ,它说:“我在Kubernetes上,我是Docker容器,我的角色是nginx服务。” 代理通过订阅收集数据。Sensu支持各种插件,如:Nagios插件,本地StatsD套接字Prometheus endpoints等等。

在本教程中,我们将使用Nagios check_http插件配置一个简单的检查。

首先,你需要提供Sensu配置,其中包括四个主要属性: resource type, API version, metadata, 和spec 。这些将在控制平面中注册,并与监控检查配置关联。

例如,这是Sensu中的监控检查,其中包含注册该检查所需的插件:

输出的格式为nagios_perfdata,并将其写入InfluxDB。

让我们配置监控检查:

$ sensuctl --namespace webinar create -f sensu/checks/check-nginx.yaml

你会看到该配置出现在Sensu仪表板中,可以在需要的地方对其进行编辑,并且该配置将在所有三个Kubernetes Pod上自动执行。

这是你的服务检查结果:

通过Sensu的监控事件管道,最终我们可以再面板上看到这些数据。

你可以在Sensu中配置InfluxDB处理程序,以将数据从其Nagios格式转换为InfluxDB格式,以便可以将其写入InfluxDB,并且可以开始在Granfana仪表板上查看指标。

以下是配置遥测管道(InfluxDB处理程序)的方法:

sensu/handlers/influxdb.yaml

---
type: Asset
api_version: core/v2
metadata:
  name: monitoring-plugins-alpine
spec:
  url: https://github.com/calebhailey/sensu-assets-monitoring-plugins/releases/download/2.2.0/monitoring-plugins-alpine_2.2.0_linux_amd64.tar.gz
  sha512: 81f7d411bd4ed4234dc54cd9218e808130a0a7db93d8ea998aa6ce74dc847bbd87ffa232c70bf4b066eb96ff3e6cb3ea0ddab1f593050b5c0bdb83f4eb36a6b2
  filters:
  - entity.system.os == 'linux'
  - entity.system.arch == 'amd64'
  - entity.system.platform == 'alpine'

---
type: Asset
api_version: core/v2
metadata:
  name: monitoring-plugins-centos
spec:
  url: https://github.com/calebhailey/sensu-assets-monitoring-plugins/releases/download/2.2.0/monitoring-plugins-centos_2.2.0_linux_amd64.tar.gz
  sha512: 0131ffea75099b2140b2d11da576c5b601d99eb7661a8105cadd884e5d953f215d23386a84ed871aabfe6a5751ce11443278be6ab510c8feab478b0365b329f0
  filters:
  - entity.system.os == 'linux'
  - entity.system.arch == 'amd64'
  - entity.system.platform == 'centos'

---
type: Asset
api_version: core/v2
metadata:
  name: monitoring-plugins-debian
spec:
  url: https://github.com/calebhailey/sensu-assets-monitoring-plugins/releases/download/2.2.0/monitoring-plugins-debian_2.2.0_linux_amd64.tar.gz
  sha512: 632366d0d55bd79762a71ef4e3209bcf6a499b6449ef6cb9471712e7799d3f6834ae7b3bafa1dfb00d4f358afed27971e7ed8b34d0424cdb31ec7a18aaf74b4f
  filters:
  - entity.system.os == 'linux'
  - entity.system.arch == 'amd64'
  - entity.system.platform == 'debian'

---
type: CheckConfig
api_version: core/v2
metadata:
  name: check-nginx
spec:
  command: check_http -H 127.0.0.1 -N
  publish: true
  interval: 10
  subscriptions:
  - nginx
  runtime_assets:
  - monitoring-plugins-alpine
  - monitoring-plugins-centos
  - monitoring-plugins-debian
  timeout: 10
  output_metric_format: nagios_perfdata
  output_metric_handlers:
  - influxdb

部署

$ sensuctl --namespace webinar create -f sensu/handlers/influxdb.yaml

你可以在GitHub中重新查看这些配置。

最后

现在,你已经建立了一个简单的管道,该管道可以通过Sidecar模式从Kubernetes收集指标,并且能够将数据发送到Grafana进行可视化。

如果你想了解有关InfluxDB处理程序(或可与Sensu Go一起使用的任何其他处理程序,包括PagerDuty,ElasticSearch,Splunk等的处理程序)的更多信息,请访问Sensai社区的开源目录Bonsai

DaemonSet

DaemonSet 确保全部(或者某些)节点上运行一个 Pod 的副本。 当有节点加入集群时, 也会为他们新增一个 Pod 。 当有节点从集群移除时,这些 Pod 也会被回收。删除 DaemonSet 将会删除它创建的所有 Pod。

DaemonSet 的一些典型用法:

  • 在每个节点上运行集群守护进程
  • 在每个节点上运行日志收集守护进程
  • 在每个节点上运行监控守护进程

一种简单的用法是为每种类型的守护进程在所有的节点上都启动一个 DaemonSet。 一个稍微复杂的用法是为同一种守护进程部署多个 DaemonSet;每个具有不同的标志, 并且对不同硬件类型具有不同的内存、CPU 要求。

k8s 去除master节点污点NoSchedule,添加master节点 尽量不调度

k8s 去除master节点(k8s-master02)污点NoSchedule,添加master节点 尽量不调度 PreferNoSchedule标签

#添加 尽量不调度 PreferNoSchedule 
kubectl taint nodes k8s-master02 node-role.kubernetes.io/master:PreferNoSchedule
#去除污点NoSchedule,最后一个"-"代表删除
kubectl taint nodes k8s-master02 node-role.kubernetes.io/master:NoSchedule-

 

k8s pod的生命周期

1.pod资源-spec.containers

– name:镜像运行起来之后叫容器,该字段为容器名

image:镜像名字

imagePullPolicy:表示从哪拉取镜像,

Always:不管本地有没有镜像,都要从仓库中下载镜像,也就是说,即使本地有镜像了,也不使用本地镜像,而是从仓库下载;

Never:从来不从仓库下载镜像,也就是说本地有镜像就用,没有就算了;

IfNotPresent:如果本地存在就直接使用,不存在才从仓库下载,默认的策略是:当镜像标签版本是latest,则策略是Always;其余都是IfNotPresent.

指定策略为ifNotPresent,即使image指定的版本是latest,每次启动容器,也不会从仓库重新下载镜像.

ports:指定暴露容器端口号,可以指定多个端口,如下:

1
2
3
4
5
6
7
8
9
spec:
  containers:
  - name: myapp
    image: ikubernetes/myapp:v1
    ports:
    - name: http
      containerPort: 80
    - name: https
      containerPort: 443

在yaml中,docker field name和k8s field name的对应关系:

docker field    k8s field

ENTRYPOINT     command

CMD         args

1
2
3
4
5
6
7
args:相当于dockerfile里面的cmd
command:相当于docker里面的entrypoint
执行命令的优先级:
如果没有提供command和args,则用docker中的默认启动命令;
如果提供了command,则镜像中的CMD和ENTRYPOINT都不生效;
如果没有提供command,提供了args,则CMD没用了,将args当成参数传给ENTRYPOINT;
如果提供了command和args,则镜像中的CMD和ENTRYPOINT都不生效;

2.标签

一个标签可以对应多个资源,一个资源也可以有多个标签,它们是多对多的关系;一个资源拥有多个标签,可以实现不同维度的管理;标签是key=value格式的,key最大63个字符,只能是字母、数字、_、-、.五种类型的组合,只能以字母或数字开头结尾.

1
2
3
4
5
6
7
8
9
kubectl get pods --show-labels
# 用-l过滤标签中有app的pod
kubectl get pods -l app --show-labels
#  用-L显示pod中的标签哪些有app,哪些有run
kubectl get pods -L app,run
# 多加一个标签
kubectl label pods pod-demo release=haha
# 修改标签
kubectl label pods pod-demo release=stable --overwrite

3.标签选择器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
等值关系:=、==、!=
kubectl get pods -l release=stable,app=myapp --show-labels
集合关系:KEY in (VALUE1,VALUE2….)、KEY notin (VALUE1,VALUE2….)、KEY、!KEY
kubectl get pods -l "release notin (stable,haha)"
许多资源支持内嵌字段定义其使用的标签选择器:
matchLabels:直接给定键值;
matchExpressions:基于给定的表达式来定义使用标签选择器,
{key:"KEY",operator:"OPERATOR",values:[VAL1,VAL2,...]}
常见操作符(operator):In、NotIn:values字段的值必须为非空列表;
Exists、NotExists:values字段的值必须为空列表.
# nodes对象也有标签
kubectl get nodes --show-labels
# 给node1节点打个disktype=ssd的标签
kubectl label nodes k8s-node1 disktype=ssd
nodeSelector:节点选择器,可以指定pod运行在哪个节点上
nodeName:可以直接指定运行节点
在maniteste/pod-demo.yaml文件spec字段中添加这两行,即可改变pod的运行节点
spec:
  ...
  nodeSelector:
    disktype: ssd

4.annotations:资源注释

1
2
3
4
5
6
7
8
9
与label不同的地方在于,它不能用于挑选资源对象,仅用于为对象提供"元数据"
metadata:
  name: pod-demo
  namespace: default
  labels:
    app: myapp
    tier: frontend
  annotations:
    mowang.com/create_by"cluster admin"

5.Pod生命周期

在一个pod中,可以运行多个容器,但通常在一个pod里面运行一个容器,容器在创建之前,有多个初始化容器(init container)用来进行初始化环境,init container执行完,它就退出了,接下来是主容器(main container)开始启动,主容器启动时也要初始化主容器里面的环境,在主容器刚启动时,用户可以手动嵌入一个操作叫post start;在主容器结束前,也可以做一个收尾操作pre stop,用来在主容器结束前做一个清理.

Pod生命周期中的重要行为:初始化容器、容器探测

liveness probe–存活性探测:用于判定主容器是否处于存活状态;

readiness probe–就绪性探测:用于判定容器中的主进程是否准备就绪以及能否对外提供服务.

在post start后,先做存活性探测,再做就绪性探测,Pod的状态:Pending(挂起,没有匹配到可运行节点),Running,Failed,Success,Unknown.

创建pod的大致流程:

apiserver会将创建请求的目标状态保存到etcd,接着请求scheduler进行调度,将调度结果保存到etcd中,目标节点上的kubelet通过apiserver拿到用户提交的创建清单,根据清单在当前节点上创建并运行这个Pod,并将结果返回给apiserver,再把结果存到etcd中.

健康检查分三个层次:1.直接执行命令;2.向tcp连接请求;3.向http发get请求.

6.livenessProbe–存活状态探测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# 探针类型
kubectl explain pod.spec.containers.livenessProbe
exec    <Object>
httpGet <Object>
tcpSocket   <Object>
failureThreshold:表示探测失败次数,默认是3,探测3次失败,才认为是真失败了;
periodSeconds:周期间隔时长,默认10s探测一次;
timeoutSeconds:超时时间,表示发出探测,对方始终没有响应,需要等多久,默认等1s;
initialDelaySeconds:默认是容器一启动就开始探测,但是此时容器可能还没启动完,此时探测肯定是失败的,
所以initialDelaySeconds表示容器启动多长时间后才开始探测.
ExecAction举例:
vim liveness-exec.yaml
apiVersion: v1
kind: Pod
metadata:
  name: liveness-exec-pod
  namespace: default
spec:
  containers:
  - name: liveness-exec-container
    image: busybox:latest
    imagePullPolicy: IfNotPresent
    command: ["/bin/sh","-c","touch /tmp/healthy; sleep 30; rm -f /tmp/healthy; sleep 3600"]
    livenessProbe:
      exec:
        command: ["test","-e","/tmp/healthy"]
      initialDelaySeconds: 2
      periodSeconds: 3
initialDelaySeconds:延迟几秒探测
periodSeconds:探测周期,多长时间探测一次
kubectl create -f liveness-exec.yaml
可以看到restart次数会随着时间增长
liveness-HTTPGetAction举例
vim liveness-httpGet.yaml
apiVersion: v1
kind: Pod
metadata:
  name: liveness-httpget-pod
  namespace: default
spec:
  containers:
  - name: liveness-httpget-container
    image: ikubernetes/myapp:v1
    imagePullPolicy: IfNotPresent
    ports:
    - name: http
      containerPort: 80
    livenessProbe:
      httpGet:
         port: http
         path: /index.html
      initialDelaySeconds: 1
      periodSeconds: 3
kubectl create -f liveness-httpGet.yaml
kubectl exec -it liveness-httpget-pod  -- /bin/sh
rm -rf  /usr/share/nginx/html/index.html

当删除pod里面的index.html之后,liveness监测到文件被删除了,容器就会重启,容器会重新初始化,里面就会又生成index.html文件,所以只重启一次,restarts次数为1.

7.readlinessProbe–准备就绪型探针

readiness-HTTPGetAction举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
vim readiness-httget.yaml
apiVersion: v1
kind: Pod
metadata:
  name: readiness-httpget-pod
  namespace: default
spec:
  containers:
kind: Pod
metadata:
  name: readiness-httpget-pod
  namespace: default
spec:
  containers:
  - name: readiness-httpget-container
    image: ikubernetes/myapp:v1
    imagePullPolicy: IfNotPresent
    ports:
    - name: http
      containerPort: 80
    readinessProbe:
      httpGet:
         port: http
         path: /index.html
      initialDelaySeconds: 1
      periodSeconds: 3

进入容器删除index.html,ready变成0/1,但是status是runing的,说明nginx进程正常,但index.html丢失,则判定nginx没有就绪.

poststart示例

postStart:如果执行操作失败了,容器将被终止并且重启,而重启与否是由重启策略决定;

preStop:容器在终止前要立即执行的命令,等这些命令执行完了,容器才能终止.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
vim poststart-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: poststart-pod
  namespace: default
spec:
  containers:
  - name: busybox-httpd
    image: busybox:latest
    imagePullPolicy: IfNotPresent
    lifecycle:
       postStart:
         exec:
           command: ["/bin/sh","-c","echo Home_Page >> /tmp/index.html"]
    #command: ['/bin/sh','-c','sleep 3600']
    command: ["/bin/httpd"]
    args: ["-f","-h /tmp"]  # -f是前台,-h是家目录
容器启动后默认执行的命令,但容器启动不能依赖于postStart执行的结果
kubectl create -f  poststart-pod.yaml

restartPolicy–容器的重启策略

一旦pod中的容器挂了,重启容器,有如下策略:

Always:表示容器挂了总是重启,这是默认策略,很耗费资源,所以Always是这么做的:

第一次挂了立即重启,如果再挂了就延时10s重启,第三次挂了就等20s重启…以此类推

OnFailures:状态是Failure时才重启,正常退出则不重启;

Never:表示容器挂了不予重启;

容器的终止策略

1
2
3
4
5
6
7
8
9
10
11
k8s会给容器30s的时间进行终止,如果30s后还没终止,就会强制终止.
kill -l
kill -15 pid  SIGTERM
系统会发送一个SIGTERM的信号给对应的程序.当程序接收到该signal后,将会发生以下的事情:
a.程序立刻停止;
b.当程序释放相应资源后再停止;
c.程序可能仍然继续运行.
大部分程序接收到SIGTERM信号后,会先释放自己的资源,然后在停止.但是也有程序可以在接受到信号量后,
做一些其他的事情,并且这些事情是可以配置的.
如果程序正在等待IO,可能就不会立马做出相应,也就是说:SIGTERM多半是会被阻塞的、忽略的.
kill -9 pid 强行终止 SIGKILL

 

Kubernetes Liveness 和 Readiness Probes的最佳实践

Kubernetes Liveness 和 Readiness探针可用于通过减少运行问题和提高服务质量来使服务更健壮和更具弹性。但是,如果不仔细设置这些探针,则它们可能会严重降低服务的整体运行性能。

在本文中,我将探讨在实现KubernetesLiveness 和 Readiness探针时如何避免使服务可靠性变差。虽然本文的重点是Kubernetes,但我将重点介绍的概念适用于用于推断服务的运行状况并采取自动补救措施的任何应用程序或基础设施机制。

Kubernetes Liveness 和 Readiness Probes

Kubernetes使用Liveness探针来决定何时重新启动容器。如果容器没有响应(可能是由于多线程缺陷而导致应用程序死锁),则尽管程序代码存在缺陷,重新启动容器仍可以使应用程序可用。无疑,它比在半夜安排运维来重新启动容器要好。

Kubernetes使用Readiness探针来确定容器何时可用于接受流量。Readiness探针用于控制将哪些Pod用作服务的后端。当Pod的所有容器都准备就绪时,将其视为就绪。如果未准备就绪,则将其从服务负载平衡器中删除。例如,如果某个容器在启动时加载了较大的缓存,并且花了几分钟启动,那么您不希望在该容器就绪之前将请求发送到该容器,否则请求将失败,您希望将请求路由到其他容器。能够处理请求。

在撰写本文时,Kubernetes支持三种用于实现Liveness 和 Readiness探针的机制:1)在容器内运行命令,2)针对容器发出HTTP请求,或3)针对容器打开TCP套接字。

探针具有许多配置参数来控制其行为,例如执行探针的频率。启动容器后启动探针要等待多长时间;探测失败之前经过的秒数;以及在放弃之前探针可以失败多少次。对于Liveness探针,放弃意味着将重新启动Pod。对于Readiness探针,放弃意味着不将流量路由到Pod,但是Pod不会重新启动。Liveness 和 Readiness探针可以结合使用。

Readiness Probes最佳实践

Kubernetes文档以及许多博客文章和示例在某种程度上误导了人们在启动容器时强调了Readiness探针的使用。通常这是最常见的考虑因素-我们希望避免在将Pod准备好接受流量之前将请求路由到该Pod。但是,在容器的整个生命周期中将周期性(periodSeconds)调用Readiness探针,以便当容器中的某个依赖项不可用时,或者在运行大型批处理作业,执行维护等操作时,容器可以使其自身暂时不可用。

如果您没有意识到启动容器后将继续调用Readiness探针,这些探针可能在运行时导致严重问题。即使您了解这种行为,但如果准备Readiness探针未考虑异常的系统动态,您仍然可能会遇到严重的问题。我将通过一个例子来说明这一点。

下面的应用程序是在Scala中使用Akka HTTP实现的,它会在启动之前将大缓存加载到内存中,然后才能处理请求。加载缓存后,将加载的原子变量设置为true。如果缓存加载失败,该容器将退出并由Kubernetes重新启动,并具有指数退避延迟。

object CacheServer extends App with CacheServerRoutes with CacheServerProbeRoutes {
  implicit val system = ActorSystem()
  implicit val materializer = ActorMaterializer()
  implicit val executionContext = ExecutionContext.Implicits.global

  val routes: Route = cacheRoutes ~ probeRoutes

  Http().bindAndHandle(routes, "0.0.0.0", 8888)

  val loaded = new AtomicBoolean(false)

  val cache = Cache()
  cache.load().onComplete {
    case Success(_) => loaded.set(true)
    case Failure(ex) =>
      system.terminate().onComplete {
        sys.error(s"Failed to load cache : $ex")
      }
  }
}

该应用程序将以下 /readiness HTTP路由用于Kubernetes Readiness探针。如果加载了缓存,则 /readiness 路由将始终成功返回。

trait CacheServerProbeRoutes {
  def loaded: AtomicBoolean

  val probeRoutes: Route = path("readiness") {
    get {
      if (loaded.get) complete(StatusCodes.OK)
      else complete(StatusCodes.ServiceUnavailable)
    }
  }
}

HTTP Readiness 探针的配置如下:

spec:
  containers:
  - name: cache-server
    image: cache-server/latest
    readinessProbe:
      httpGet:
        path: /readiness
        port: 8888
      initialDelaySeconds: 300
      periodSeconds: 30

这种Readiness探针实现极为可靠。在加载缓存之前,请求不会路由到应用程序。加载缓存后,/readiness 路由将永久返回HTTP 200,并且将始终将pod视为就绪。

将此实现与以下应用程序进行对比,该应用程序向其依赖服务发出HTTP请求,这是其准备情况检查的一部分。像这样的Readiness探针对于在部署时捕获配置问题很有用,例如使用错误的证书进行双向TLS或错误的凭据进行数据库身份验证,以确保服务在准备就绪之前可以与其所有依赖项进行通信。

trait ServerWithDependenciesProbeRoutes {
  implicit def ec: ExecutionContext

  def httpClient: HttpRequest => Future[HttpResponse]

  private def httpReadinessRequest(
    uri: Uri,
    f: HttpRequest => Future[HttpResponse] = httpClient): Future[HttpResponse] = {
    f(HttpRequest(method = HttpMethods.HEAD, uri = uri))
  }

  private def checkStatusCode(response: Try[HttpResponse]): Try[Unit] = {
    response match {
      case Success(x) if x.status == StatusCodes.OK => Success(())
      case Success(x) if x.status != StatusCodes.OK => Failure(HttpStatusCodeException(x.status))
      case Failure(ex) => Failure(HttpClientException(ex))
    }
  }

  private def readinessProbe() = {
    val authorizationCheck = httpReadinessRequest("https://authorization.service").transform(checkStatusCode)
    val inventoryCheck = httpReadinessRequest("https://inventory.service").transform(checkStatusCode)
    val telemetryCheck = httpReadinessRequest("https://telemetry.service").transform(checkStatusCode)

    val result = for {
      authorizationResult <- authorizationCheck
      inventoryResult <- inventoryCheck
      telemetryResult <- telemetryCheck
    } yield (authorizationResult, inventoryResult, telemetryResult)

    result
  }

  val probeRoutes: Route = path("readiness") {
    get {
      onComplete(readinessProbe()) {
        case Success(_) => complete(StatusCodes.OK)
        case Failure(_) => complete(StatusCodes.ServiceUnavailable)
      }
    }
  }
}

这些并发的HTTP请求通常以毫秒为单位非常快速地返回。Readiness探针的默认超时为一秒。由于这些请求在绝大多数时间都成功,因此大多时候默认值就可以满足。

但是请考虑一下,如果某个临时服务的延迟稍有暂时的增加,该怎么办?可能是由于网络拥塞,垃圾收集暂停或临时增加了相关服务的负载。如果对依赖项的等待时间增加到甚至稍大于一秒,则准备就绪探测将失败并且Kubernetes将不再将流量路由到Pod。由于所有Pod都共享相同的依赖关系,因此支持该服务的所有Pod很可能会同时使“就绪性”探测失败。这将导致所有Pod从服务路由中删除。没有Pod支持该服务,Kubernetes将针对所有对该服务的请求返回HTTP 404(默认后端)。尽管我们已尽最大努力提高可用性,但我们已经创建了单点故障,使服务完全不可用。在这种情况下,我们将通过使客户端请求成功(尽管延迟稍有增加)来提供更好的最终用户体验,而不是一次或几秒钟地使整个服务不可用。

如果Readiness探针正在验证容器专有的依赖关系(私有缓存或数据库),则可以假设容器依赖项是独立的,那么您可以更加积极地使Readiness探针失败。但是,如果Readiness探针正在验证共享依赖关系(例如用于身份验证,授权,指标,日志记录或元数据的通用服务),则在使Readiness探针失败时应该非常保守。

所以建议如下:

  • 如果容器在Readiness探针中包含了共享的依赖关系,则将就绪探针超时设置为大于该依赖关系的最大响应时间。
  • 默认的failThreshold计数为3,即Readiness探针在不再将Pod视为就绪之前探测失败的次数。Readiness探针的频率(由periodSeconds参数确定),您可能需要增加failureThreshold计数。这样做的目的是避免在临时系统故障已经过去并且响应等待时间恢复正常之前,过早地使Readiness探针失败。

Liveness Probes 最佳实践

回想一下,Liveness探针故障将导致容器重新启动。与Readiness探针不同,Liveness探针检测依赖项是非常危险的。应使用Liveness探针检查容器本身是否没有响应。

Liveness探针的一个问题是,探针可能实际上无法验证服务的响应能力。例如,如果服务托管两台Web服务器-一台用于服务路由,一台用于状态路由(如readiness和liveness或指标收集),则该服务可能会变慢或无响应,而Liveness探针路由会返回正常。为了有效,Liveness探针必须以与依赖服务类似的方式设置。

与Readiness 探针类似,考虑随时间变化的动态变化也很重要。如果Liveness探针超时太短,则响应时间的少量增加(可能是负载的暂时增加所引起的)可能会导致容器重新启动。重新启动可能会为支持该服务的其他Pod带来更多负载,从而导致Liveness探针故障进一步级联,从而使服务的整体可用性变得更糟。按客户端超时的顺序配置Liveness探针超时,并使用failureThreshold计数,可以防止这些级联失败。

Liveness探针的一个细微问题来自容器启动延迟随时间的变化。这可能是由于网络拓扑更改,资源分配更改或随服务扩展而增加负载的结果。如果由于Kubernetes节点故障或Liveness探针故障而重新启动了容器,并且initialDelaySeconds参数的时间不够长,则可能会导致永远无法启动该应用程序,因为该应用程序会在完全启动之前反复被杀死并重新启动。 initialDelaySeconds参数应大于容器的最大初始化时间。为避免这些动态变化随时间变化带来的意外,在一定程度上定期重启Pod很有好处-单个Pod一次支持服务运行数周或数月不一定是目标。重要的是,定期运行和评估部署,重新启动和故障是运行可靠服务的一部分。

所以建议如下:

  • 避免在Liveness探针中检查服务的依赖项。Liveness探针应该简单并且具有最小的响应时间。
  • 保守地设置Liveness探针超时,以便系统临时或永久更改,而不会导致Liveness探针故障过多。考虑将活动探针超时设置为与客户端超时相同的幅度。
  • 保守地设置initialDelaySeconds参数,以便即使启动动态随时间变化,也可以可靠地重新启动容器。

结论

KubernetesLiveness 和 Readiness 探针可以极大地提高服务的健壮性和弹性,并提供出色的最终用户体验。但是,如果您不仔细考虑如何使用这些探针,特别是如果您不考虑异常的系统动态(无论多么罕见),则有可能使服务的可用性变差,而不是变好。

k8s污点Taints与容忍详解Toleration

污点状态:

  • NoSchedule:如果 Node 上带有污点 effect 为 NoSchedule,而 Node 上不带相应容忍,Kubernetes 就不会调度 Pod 到这台 Node 上。
  • PreferNoShedule:如果 Node 上带有污点 effect 为 PreferNoShedule,这时候 Kubernetes 会努力不要调度这个 Pod 到这个 Node 上。
  • NoExecute:如果 Node 上带有污点 effect 为 NoExecute,这个已经在 Node 上运行的 Pod 会从 Node 上驱逐掉。没有运行在 Node 的 Pod 不能被调度到这个 Node 上。

污点值:

  • 污点 value 的值可以为 NoSchedule、PreferNoSchedule 或 NoExecute

污点属性:

  • 污点是k8s集群的pod中的一种属性
  • 污点属性分为以上三种

污点组成:

  • key、value 及一个 effect 三个元素
<key>=<value>:<effect>

1、设置单污点及单容忍度

kubectl taint nodes master1 node-role.kubernetes.io/master=:NoSchedule     

kubectl taint node node1 key1=value1:NoSchedule       # 设置value值

kubectl taint node master1 key2=:PreferNoSchedule     # 不设置value值

2、设置多污点及多容忍度

kubectl taint nodes node1 key1=value1:NoSchedule  

kubectl taint nodes node1 key1=value1:NoExecute  

kubectl taint nodes node1 key2=value2:NoSchedule

3、查看pod中的污点状态

[root@master1 ~]# kubectl describe nodes master1
Name:               master1
Roles:              master
Labels:             beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/os=linux
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=master1
                    kubernetes.io/os=linux
                    node-role.kubernetes.io/master=
Annotations:        flannel.alpha.coreos.com/backend-data: {"VNI":1,"VtepMAC":"36:51:e1:31:e5:9e"}
                    flannel.alpha.coreos.com/backend-type: vxlan
                    flannel.alpha.coreos.com/kube-subnet-manager: true
                    flannel.alpha.coreos.com/public-ip: 192.168.200.3
                    kubeadm.alpha.kubernetes.io/cri-socket: /var/run/dockershim.sock
                    node.alpha.kubernetes.io/ttl: 0
                    volumes.kubernetes.io/controller-managed-attach-detach: true
CreationTimestamp:  Wed, 13 Jan 2021 06:04:10 -0500
Taints:             node-role.kubernetes.io/master:NoSchedule              # 污点状态及容忍度
Unschedulable:      false
Lease:
  HolderIdentity:  master1
  AcquireTime:     <unset>
  RenewTime:       Thu, 14 Jan 2021 01:14:07 -0500
Conditions:
  Type                 Status  LastHeartbeatTime                 LastTransitionTime                Reason                       Message
  ----                 ------  -----------------                 ------------------                ------                       -------
  NetworkUnavailable   False   Wed, 13 Jan 2021 06:12:43 -0500   Wed, 13 Jan 2021 06:12:43 -0500   FlannelIsUp                  Flannel is running on this node
  MemoryPressure       False   Thu, 14 Jan 2021 01:11:17 -0500   Wed, 13 Jan 2021 06:50:32 -0500   KubeletHasSufficientMemory   kubelet has sufficient memory available
  DiskPressure         False   Thu, 14 Jan 2021 01:11:17 -0500   Wed, 13 Jan 2021 06:50:32 -0500   KubeletHasNoDiskPressure     kubelet has no disk pressure
  PIDPressure          False   Thu, 14 Jan 2021 01:11:17 -0500   Wed, 13 Jan 2021 06:50:32 -0500   KubeletHasSufficientPID      kubelet has sufficient PID available
  Ready                True    Thu, 14 Jan 2021 01:11:17 -0500   Wed, 13 Jan 2021 06:50:32 -0500   KubeletReady                 kubelet is posting ready status
Addresses:
  InternalIP:  192.168.200.3
  Hostname:    master1
Capacity:
  cpu:                4
  ephemeral-storage:  17394Mi
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             2897500Ki
  pods:               110
Allocatable:
  cpu:                4
  ephemeral-storage:  16415037823
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             2795100Ki
  pods:               110
System Info:
  Machine ID:                 feb4edfea2404d3c8ad028ca4593bb32
  System UUID:                C6F44D56-0F24-6114-23E7-8DF6CD4E4CFE
  Boot ID:                    afcc0ef6-d767-4b97-9a7b-9b2500757f2e
  Kernel Version:             3.10.0-862.el7.x86_64
  OS Image:                   CentOS Linux 7 (Core)
  Operating System:           linux
  Architecture:               amd64
  Container Runtime Version:  docker://19.3.0
  Kubelet Version:            v1.18.2
  Kube-Proxy Version:         v1.18.2
PodCIDR:                      10.244.0.0/24
PodCIDRs:                     10.244.0.0/24
Non-terminated Pods:          (6 in total)
  Namespace                   Name                               CPU Requests  CPU Limits  Memory Requests  Memory Limits  AGE
  ---------                   ----                               ------------  ----------  ---------------  -------------  ---
  kube-system                 etcd-master1                       0 (0%)        0 (0%)      0 (0%)           0 (0%)         19h
  kube-system                 kube-apiserver-master1             250m (6%)     0 (0%)      0 (0%)           0 (0%)         19h
  kube-system                 kube-controller-manager-master1    200m (5%)     0 (0%)      0 (0%)           0 (0%)         19h
  kube-system                 kube-flannel-ds-wzf7w              100m (2%)     100m (2%)   50Mi (1%)        50Mi (1%)      19h
  kube-system                 kube-proxy-7h5sb                   0 (0%)        0 (0%)      0 (0%)           0 (0%)         19h
  kube-system                 kube-scheduler-master1             100m (2%)     0 (0%)      0 (0%)           0 (0%)         19h
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests    Limits
  --------           --------    ------
  cpu                650m (16%)  100m (2%)
  memory             50Mi (1%)   50Mi (1%)
  ephemeral-storage  0 (0%)      0 (0%)
  hugepages-1Gi      0 (0%)      0 (0%)
  hugepages-2Mi      0 (0%)      0 (0%)
Events:              <none>

4、过滤出有几台节点存在污和容忍度是什么

[root@master1 ~]# kubectl describe node master1 | grep Taints
Taints:             node-role.kubernetes.io/master:NoSchedule

[root@master1 ~]# kubectl describe node master2 | grep Taints
Taints:             node-role.kubernetes.io/master:NoSchedule

[root@master1 ~]# kubectl describe node master3 | grep Taints
Taints:             node-role.kubernetes.io/master:NoSchedule

5、有无污点返回的结果

Taints:             node-role.kubernetes.io/master:NoSchedule     # 有污点

Taints:             <none>                                        # 没污点

6、删除污点使其pod能够调度和使用

kubectl taint node master1 node-role.kubernetes.io/master:NoSchedule-

kubectl taint nodes master1 key:NoSchedule-

kubectl create 一直处于 ContainerCreating 状态

使用命令查看 pods 状态,发现过去很久还是没有启动成功。

guoqingsongmbp:k8s guo$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx 0/1 ContainerCreating 0 15s

1
2
3

继续查看详情

guoqingsongmbp:k8s guo$ kubectl describe pod nginx
Name: nginx
Namespace: default
Node: minikube/192.168.99.105
Start Time: Tue, 25 Dec 2018 17:45:28 +0800
Labels: app=nginx
Annotations: <none>
Status: Pending
IP:
Containers:
nginx:
Container ID:
Image: nginx
Image ID:
Port: 80/TCP
State: Waiting
Reason: ContainerCreating
Ready: False
Restart Count: 0
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-5bz7m (ro)
Conditions:
Type Status
Initialized True
Ready False
PodScheduled True
Volumes:
default-token-5bz7m:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-5bz7m
Optional: false
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: <none>
Events:
Type Reason Age From Message
—- —— —- —- ——-
Normal Scheduled 14s default-scheduler Successfully assigned nginx to minikube
Normal SuccessfulMountVolume 14s kubelet, minikube MountVolume.SetUp succeeded for volume “default-token-5bz7m”
Warning FailedCreatePodSandBox 0s kubelet, minikube Failed create pod sandbox.

 

发现最后出错了,Warning FailedCreatePodSandBox 0s kubelet, minikube Failed create pod sandbox.。

进入到 minikube 节点里面进行排查问题。

guoqingsongmbp:k8s guo$ minikube ssh
_ _
_ _ ( ) ( )
___ ___ (_) ___ (_)| |/’) _ _ | |_ __
/’ _ ` _ `\| |/’ _ `\| || , < ( ) ( )| ‘_`\ /’__`\
| ( ) ( ) || || ( ) || || |\`\ | (_) || |_) )( ___/
(_) (_) (_)(_)(_) (_)(_)(_) (_)`\___/'(_,__/’`\____)

# 查看日志
$ journalctl -xe

1
2
3
4
5
6
7
8
9
10

发现有这么一个错误,如下:

Dec 25 09:40:03 minikube dockerd[2468]: time=”2018-12-25T09:40:03.283646463Z” level=info msg=”Attempting next endpoint for pull after error: Get https://gcr.io/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)”
Dec 25 09:40:03 minikube dockerd[2468]: time=”2018-12-25T09:40:03.283664032Z” level=error msg=”Handler for POST /v1.31/images/create returned error: Get https://gcr.io/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)”
Dec 25 09:40:03 minikube localkube[3258]: E1225 09:40:03.284457 3258 remote_runtime.go:92] RunPodSandbox from runtime service failed: rpc error: code = Unknown desc = failed pulling image “gcr.io/google_containers/pause-amd64:3.0”: Error response from daemon: Get https://gcr.io/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)

1
2
3

这里是因为会使用地址 gcr.io/google_containers/pause-amd64:3.0 进行拉取镜像,但是这个地址被墙了,所以不通,这里的解决办事是去 docker hub 上拉取完毕之后然后再进行更改 tag。如下:

$ docker pull docker.io/kubernetes/pause
Using default tag: latest
latest: Pulling from kubernetes/pause
a3ed95caeb02: Pull complete
f72a00a23f01: Pull complete
Digest: sha256:2088df8eb02f10aae012e6d4bc212cabb0ada93cb05f09e504af0c9811e0ca14
Status: Downloaded newer image for kubernetes/pause:latest

$ docker tag kubernetes/pause:latest gcr.io/google_containers/pause-amd64:3.0

1
2
3
4
5
6
7
8
9

最后把原来的 pod 删除掉,再重新启动即可。

guoqingsongmbp:k8s guo$ kubectl delete -f pod_nginx.yml
pod “nginx” deleted

guoqingsongmbp:k8s guo$ kubectl create -f pod_nginx.yml
pod “nginx” created

guoqingsongmbp:k8s guo$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 14m

guoqingsongmbp:k8s guo$ kubectl describe pod nginx
Name: nginx
Namespace: default
Node: minikube/192.168.99.105
Start Time: Tue, 25 Dec 2018 18:00:56 +0800
Labels: app=nginx
Annotations: <none>
Status: Running
IP: 172.17.0.4
Containers:
nginx:
Container ID: docker://cf22052ba5626cf6d99fbdb3867fa545a20c16d6f02c7eb9d9ad25b6ce6500ad
Image: nginx
Image ID: docker-pullable://nginx@sha256:5d32f60db294b5deb55d078cd4feb410ad88e6fe77500c87d3970eca97f54dba
Port: 80/TCP
State: Running
Started: Tue, 25 Dec 2018 18:01:26 +0800
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-5bz7m (ro)
Conditions:
Type Status
Initialized True
Ready True
PodScheduled True
Volumes:
default-token-5bz7m:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-5bz7m
Optional: false
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: <none>
Events:
Type Reason Age From Message
—- —— —- —- ——-
Normal Scheduled 14m default-scheduler Successfully assigned nginx to minikube
Normal SuccessfulMountVolume 14m kubelet, minikube MountVolume.SetUp succeeded for volume “default-token-5bz7m”
Normal Pulling 14m kubelet, minikube pulling image “nginx”
Normal Pulled 14m kubelet, minikube Successfully pulled image “nginx”
Normal Created 14m kubelet, minikube Created container
Normal Started 14m kubelet, minikube Started container