第1章 安装

1.1 基础配置

[root@k8s_master ~]# cat /etc/redhat-release

CentOS Linux release 7.5.1804 (Core)

[root@k8s_master ~]# systemctl status firewalld

● firewalld.service - firewalld - dynamic firewall daemon

   Loaded: loaded (/usr/lib/systemd/system/firewalld.service; disabled; vendor preset: enabled)

   Active: inactive (dead)

     Docs: man:firewalld(1)

[root@k8s_master ~]# setenforce 0

setenforce: SELinux is disabled

[root@k8s_master ~]# cat /etc/hosts

127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4

::1         localhost localhost.localdomain localhost6 localhost6.localdomain6

192.168.138.150 k8s_master

192.168.138.151 k8s_client1

192.168.138.152  k8s_client2

配置阿里云的yum源或系统默认的源;

[root@k8s_master ~]# yum -y install ntp docker

[root@k8s_master ~]# systemctl start ntpd

[root@k8s_master ~]# systemctl enable ntpd

Created symlink from /etc/systemd/system/multi-user.target.wants/ntpd.service to /usr/lib/systemd/system/ntpd.service.

Master节点操作

安装etcd

[root@k8s_master ~]# yum -y install etcd

[root@k8s_master ~]# cat /etc/etcd/etcd.conf | grep -v "^#"

ETCD_DATA_DIR="/var/lib/etcd/default.etcd"

ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379,http://0.0.0.0:4001"

ETCD_NAME="master"

ETCD_ADVERTISE_CLIENT_URLS=http://k8s_master:2379,http://k8s_master:4001

[root@k8s_master ~]# systemctl enable etcd

[root@k8s_master ~]# systemctl start etcd

[root@k8s_master ~]# etcdctl -C http://k8s_master:2379 cluster-health

member 8e9e05c52164694d is healthy: got healthy result from http://k8s_master:2379

cluster is healthy

[root@k8s_master ~]# etcdctl -C http://k8s_master:4001 cluster-health

member 8e9e05c52164694d is healthy: got healthy result from http://k8s_master:2379

cluster is healthy

安装docker(所有节点)

[root@k8s_master ~]# yum -y install docker

[root@k8s_master ~]# docker version

Client:

 Version:         1.13.1

 API version:     1.26

 Package version: docker-1.13.1-74.git6e3bb8e.el7.centos.x86_64

 Go version:      go1.9.4

 Git commit:      6e3bb8e/1.13.1

 Built:           Tue Aug 21 15:23:37 2018

 OS/Arch:         linux/amd64

 

Server:

 Version:         1.13.1

 API version:     1.26 (minimum version 1.12)

 Package version: docker-1.13.1-74.git6e3bb8e.el7.centos.x86_64

 Go version:      go1.9.4

 Git commit:      6e3bb8e/1.13.1

 Built:           Tue Aug 21 15:23:37 2018

 OS/Arch:         linux/amd64

 Experimental:    false

[root@k8s_master ~]# yum install kubernetes

[root@k8s_master ~]# cat /etc/kubernetes/apiserver | grep -Ev "^#|^$"

KUBE_API_ADDRESS="--insecure-bind-address=0.0.0.0"

KUBE_API_PORT="--port=8080"

KUBE_ETCD_SERVERS="--etcd-servers=http://192.168.138.150:2379"

KUBE_SERVICE_ADDRESSES="--service-cluster-ip-range=10.254.0.0/16"

KUBE_ADMISSION_CONTROL="--admission-control=NamespaceLifecycle,NamespaceExists,LimitRanger,ResourceQuota"

KUBE_API_ARGS=""

[root@k8s_master ~]# cat /etc/kubernetes/config |grep -Ev "^#|^$"

KUBE_LOGTOSTDERR="--logtostderr=true"

KUBE_LOG_LEVEL="--v=0"

KUBE_ALLOW_PRIV="--allow-privileged=false"

KUBE_MASTER="--master=http://192.168.138.150:8080"

 

[root@k8s_master ~]# systemctl enable kube-apiserver kube-controller-manager kube-scheduler

[root@k8s_master ~]# systemctl start kube-apiserver kube-controller-manager kube-scheduler

[root@k8s_master ~]# netstat -lntup

Active Internet connections (only servers)

Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name   

tcp        0      0 127.0.0.1:2380          0.0.0.0:*               LISTEN      936/etcd           

tcp        0      0 0.0.0.0:111             0.0.0.0:*               LISTEN      639/rpcbind        

tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      932/sshd           

tcp        0      0 127.0.0.1:25            0.0.0.0:*               LISTEN      1026/master        

tcp6       0      0 :::4001                 :::*                    LISTEN      936/etcd           

tcp6       0      0 :::6443                 :::*                    LISTEN      1047/kube-apiserver

tcp6       0      0 :::2379                 :::*                    LISTEN      936/etcd           

tcp6       0      0 :::10251                :::*                    LISTEN      651/kube-scheduler 

tcp6       0      0 :::10252                :::*                    LISTEN      634/kube-controller

tcp6       0      0 :::111                  :::*                    LISTEN      639/rpcbind        

tcp6       0      0 :::8080                 :::*                    LISTEN      1047/kube-apiserver        

tcp6       0      0 ::1:25                  :::*                    LISTEN      1026/master        

udp        0      0 0.0.0.0:803             0.0.0.0:*                           639/rpcbind        

udp        0      0 0.0.0.0:68              0.0.0.0:*                           2964/dhclient      

udp        0      0 192.168.138.150:8285    0.0.0.0:*                           1048/flanneld      

udp        0      0 0.0.0.0:111             0.0.0.0:*                           639/rpcbind        

udp        0      0 192.168.138.150:123     0.0.0.0:*                           2555/ntpd 

node节点安装

[root@k8s_client1 ~]# yum install kubernetes

[root@k8s_client1 ~]#  cat /etc/kubernetes/config | egrep -v "^#|^$"

KUBE_LOGTOSTDERR="--logtostderr=true"

KUBE_LOG_LEVEL="--v=0"

KUBE_ALLOW_PRIV="--allow-privileged=false"

KUBE_MASTER="--master=http://192.168.138.150:8080"

[root@k8s_client1 ~]# cat /etc/kubernetes/kubelet | egrep -v "^#|^$"

KUBELET_ADDRESS="--address=0.0.0.0"

KUBELET_PORT="--port=10250"

KUBELET_HOSTNAME="--hostname-override=192.168.138.151"

KUBELET_API_SERVER="--api-servers=http://192.168.138.150:8080"

KUBELET_POD_INFRA_CONTAINER="--pod-infra-container-image=registry.access.redhat.com/rhel7/pod-infrastructure:latest"

KUBELET_ARGS=""

[root@k8s_client1 ~]# systemctl enable kubelet kube-proxy

[root@k8s_client1 ~]# systemctl start kubelet kube-proxy   

[root@k8s_client1 ~]# netstat -lntup

Active Internet connections (only servers)

Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name   

tcp        0      0 127.0.0.1:10248         0.0.0.0:*               LISTEN      1376/kubelet       

tcp        0      0 127.0.0.1:10249         0.0.0.0:*               LISTEN      925/kube-proxy     

tcp        0      0 0.0.0.0:111             0.0.0.0:*               LISTEN      635/rpcbind        

tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      927/sshd           

tcp        0      0 127.0.0.1:25            0.0.0.0:*               LISTEN      1014/master        

tcp6       0      0 :::4194                 :::*                    LISTEN      1376/kubelet       

tcp6       0      0 :::30372                :::*                    LISTEN      925/kube-proxy     

tcp6       0      0 :::10250                :::*                    LISTEN      1376/kubelet       

tcp6       0      0 :::9100                 :::*                    LISTEN      1947/node_exporter 

tcp6       0      0 :::10255                :::*                    LISTEN      1376/kubelet       

tcp6       0      0 :::111                  :::*                    LISTEN      635/rpcbind        

tcp6       0      0 :::30000                :::*                    LISTEN      925/kube-proxy     

tcp6       0      0 :::22                   :::*                    LISTEN      927/sshd           

tcp6       0      0 ::1:25                  :::*                    LISTEN      1014/master         

udp        0      0 0.0.0.0:807             0.0.0.0:*                           635/rpcbind        

udp        0      0 0.0.0.0:68              0.0.0.0:*                           744/dhclient       

udp        0      0 192.168.138.151:8285    0.0.0.0:*                           930/flanneld 

[root@k8s_master ~]# kubectl get nodes

NAME              STATUS    AGE

192.168.138.151   Ready     15d

192.168.138.152   Ready     15d

安装flannel(所有节点)

yum install flannel

systemctl status firewalld

systemctl stop firewalld

systemctl disable firewalld

 

Master节点

[root@k8s_master ~]# cat /etc/sysconfig/flanneld | egrep -v "^#|^$"

FLANNEL_ETCD_ENDPOINTS="http://192.168.138.150:2379"

FLANNEL_ETCD_PREFIX="/atomic.io/network"

[root@k8s_master ~]# etcdctl mk //atomic.io/network/config '{"Network":"172.8.0.0/16"}'

[root@k8s_master ~]# systemctl enable flanneld

[root@k8s_master ~]# systemctl start flanneld  

[root@k8s_master ~]# for SERVICES in docker kube-apiserver kube-controller-manager kube-scheduler; do systemctl restart $SERVICES ; done

Node节点

[root@k8s_client1 ~]# cat /etc/sysconfig/flanneld | egrep -v "^#|^$"

FLANNEL_ETCD_ENDPOINTS="http://192.168.138.150:2379"

FLANNEL_ETCD_PREFIX="/atomic.io/network"

[root@k8s_client1 ~]# etcdctl mk //atomic.io/network/config '{"Network":"172.8.0.0/16"}'

[root@k8s_client1 ~]# systemctl enable flanneld

[root@k8s_client1 ~]# systemctl start flanneld 

[root@k8s_client1 ~]# systemctl restart kube-proxy kubelet docker     

[root@k8s_client1 ~]# ip a

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000

    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

    inet 127.0.0.1/8 scope host lo

       valid_lft forever preferred_lft forever

    inet6 ::1/128 scope host

       valid_lft forever preferred_lft forever

2: eno16777728: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000

    link/ether 00:0c:29:f2:c5:36 brd ff:ff:ff:ff:ff:ff

    inet 192.168.138.151/24 brd 192.168.138.255 scope global noprefixroute dynamic eno16777728

       valid_lft 1126sec preferred_lft 1126sec

    inet6 fe80::20c:29ff:fef2:c536/64 scope link

       valid_lft forever preferred_lft forever

3: flannel0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1472 qdisc pfifo_fast state UNKNOWN group default qlen 500

    link/none

    inet 172.8.77.0/16 scope global flannel0

       valid_lft forever preferred_lft forever

    inet6 fe80::4ac1:c034:5096:3b04/64 scope link flags 800

       valid_lft forever preferred_lft forever

4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1472 qdisc noqueue state UP group default

    link/ether 02:42:27:e3:50:d7 brd ff:ff:ff:ff:ff:ff

    inet 172.8.77.1/24 scope global docker0

       valid_lft forever preferred_lft forever

    inet6 fe80::42:27ff:fee3:50d7/64 scope link

       valid_lft forever preferred_lft forever

所有节点

yum install *rhsm* -y

docker pull registry.access.redhat.com/rhel7/pod-infrastructure:latest  (k8s运行docker的基础镜像)

在阿里云上拿下来的

部署dashboard

[root@k8s_master ~]# docker pull docker.io/siriuszg/kubernetes-dashboard-amd64:v1.5.1

[root@k8s_master ~]# cat kubernetes-dashboard.yaml

kind: Deployment

apiVersion: extensions/v1beta1

metadata:

  labels:

    app: kubernetes-dashboard

  name: kubernetes-dashboard

  namespace: kube-system

spec:

  replicas: 1

  revisionHistoryLimit: 10

  selector:

    matchLabels:

      app: kubernetes-dashboard

  template:

    metadata:

      labels:

        app: kubernetes-dashboard

# Comment the following annotation if Dashboard must not be deployed on master

      annotations:

        scheduler.alpha.kubernetes.io/tolerations: |

          [

            {

              "key": "dedicated",

              "operator": "Equal",

              "value": "master",

              "effect": "NoSchedule"

            }

          ]

    spec:

      containers:

      - name: kubernetes-dashboard

        image: docker.io/siriuszg/kubernetes-dashboard-amd64:v1.5.1

        imagePullPolicy: IfNotPresent

        ports:

        - containerPort: 9090

          protocol: TCP

        args:

 # Uncomment the following line to manually specify Kubernetes API server Host

 # If not specified, Dashboard will attempt to auto discover the API server and connect

 # to it. Uncomment only if the default does not work.

        - --apiserver-host=http://192.168.138.150:8080

        livenessProbe:

          httpGet:

            path: /

            port: 9090

          initialDelaySeconds: 30

          timeoutSeconds: 30

[root@k8s_master ~]# cat dashboard-service.yaml

kind: Service

apiVersion: v1

metadata:

  labels:

    app: kubernetes-dashboard

  name: kubernetes-dashboard

  namespace: kube-system

spec:

  type: NodePort

  ports:

  - port: 80

    targetPort: 9090

  selector:

app: kubernetes-dashboard

[root@k8s_master ~]# kubectl create -f kubernetes-dashboard.yaml
deployment "kubernetes-dashboard" created
[root@k8s_master ~]# kubectl create -f dashboard-service.yaml 
service "kubernetes-dashboard" created

[root@k8s_master ~]# kubectl get deployment --all-namespaces

NAMESPACE     NAME                   DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE

default       httpd                  3         3         3            3           14d

default       nginx-deployment       3         3         3            3           14d

kube-system   kubernetes-dashboard   1         1         1            1           15d

kube-system   tiller-deploy          1         1         1            0           13d

weave         weave-scope-app        1         1         1            1           13d

[root@k8s_master ~]# kubectl get svc --all-namespaces

NAMESPACE     NAME                   CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE

default       httpd-svc              10.254.146.62    <nodes>       8082:30000/TCP   14d

default       kubernetes             10.254.0.1       <none>        443/TCP          15d

kube-system   kubernetes-dashboard   10.254.172.95    <nodes>       80:30372/TCP     15d

kube-system   tiller-deploy          10.254.112.169   <none>        44134/TCP        13d

weave         weave-scope-app        10.254.187.130   <none>        80/TCP           13d

访问:192.168.138.150:8080/ui

 

 

 

 

参考文档:http://blog.51cto.com/jonauil/2084986

https://www.cnblogs.com/clsn/p/8410321.html

 

第2章 概念

Kubernetes 具备完善的集群管理能力,包括多层次的安全防护和准入机制、多租户应用支撑能力、透明的服务注册和服务发现机制、内建负载均衡器、故障发现和自我修复能力、服务滚动升级和在线扩容、可扩展的资源自动         调度机制、多粒度的资源配额管理能力。

Kubernetes 还提供完善的管理工具,涵盖开发、部署测试、运维监控等各个环节。

Kubernetes主要由以下几个核心组件组成:

etcd保存了整个集群的状态;

apiserver提供了资源操作的唯一入口,并提供认证、授权、访问控制、API注册和发现等机制;

controller manager负责维护集群的状态,比如故障检测、自动扩展、滚动更新等;

scheduler负责资源的调度,按照预定的调度策略将Pod调度到相应的机器上;

kubelet负责维护容器的生命周期,同时也负责Volume(CVI)和网络(CNI)的管理;

Container runtime负责镜像管理以及Pod和容器的真正运行(CRI);

kube-proxy负责为Service提供cluster内部的服务发现和负载均衡;

 

kube-dns负责为整个集群提供DNS服务

Ingress Controller为服务提供外网入口

Heapster提供资源监控

Dashboard提供GUI

Federation提供跨可用区的集群

Fluentd-elasticsearch提供集群日志采集、存储与查询

 

Kubernetes设计理念和功能其实就是一个类似Linux的分层架构,如下图所示:

 

 

 

Cluster 
Cluster 是计算、存储和网络资源的集合,Kubernetes 利用这些资源运行各种基于容器的应用。

Master 
Master 是 Cluster 的大脑,它的主要职责是调度,即决定将应用放在哪里运行。Master 运行 Linux 操作系统,可以是物理机或者虚拟机。为了实现高可用,可以运行多个 Master。

Node 
Node 的职责是运行容器应用。Node 由 Master 管理,Node 负责监控并汇报容器的状态,并根据 Master 的要求管理容器的生命周期。Node 运行在 Linux 操作系统,可以是物理机或者是虚拟机。

Pod 
Pod 是 Kubernetes 的最小工作单元。每个 Pod 包含一个或多个容器。Pod 中的容器会作为一个整体被 Master 调度到一个 Node 上运行。

Kubernetes 引入 Pod 主要基于下面两个目的:

可管理性。
有些容器天生就是需要紧密联系,一起工作。Pod 提供了比容器更高层次的抽象,将它们封装到一个部署单元中。Kubernetes 以 Pod 为最小单位进行调度、扩展、共享资源、管理生命周期。

通信和资源共享。
Pod 中的所有容器使用同一个网络 namespace,即相同的 IP 地址和 Port 空间。它们可以直接用 localhost 通信。同样的,这些容器可以共享存储,当 Kubernetes 挂载 volume 到 Pod,本质上是将 volume 挂载到 Pod 中的每一个容器。

Pods 有两种使用方式:

运行单一容器。
one-container-per-Pod 是 Kubernetes 最常见的模型,这种情况下,只是将单个容器简单封装成 Pod。即便是只有一个容器,Kubernetes 管理的也是 Pod 而不是直接管理容器。

运行多个容器。
但问题在于:哪些容器应该放到一个 Pod 中? 
答案是:这些容器联系必须 非常紧密,而且需要 直接共享资源。

Controller 
Kubernetes 通常不会直接创建 Pod,而是通过 Controller 来管理 Pod 的。Controller 中定义了 Pod 的部署特性,比如有几个副本,在什么样的 Node 上运行等。为了满足不同的业务场景,Kubernetes 提供了多种 Controller,包括 Deployment、ReplicaSet、DaemonSet、StatefuleSet、Job 等,我们逐一讨论。

Deployment 是最常用的 Controller,比如前面在线教程中就是通过创建 Deployment 来部署应用的。Deployment 可以管理 Pod 的多个副本,并确保 Pod 按照期望的状态运行。

ReplicaSet 实现了 Pod 的多副本管理。使用 Deployment 时会自动创建 ReplicaSet,也就是说 Deployment 是通过 ReplicaSet 来管理 Pod 的多个副本,我们通常不需要直接使用 ReplicaSet。

DaemonSet 用于每个 Node 最多只运行一个 Pod 副本的场景。正如其名称所揭示的,DaemonSet 通常用于运行 daemon。

StatefuleSet 能够保证 Pod 的每个副本在整个生命周期中名称是不变的。而其他 Controller 不提供这个功能,当某个 Pod 发生故障需要删除并重新启动时,Pod 的名称会发生变化。同时 StatefuleSet 会保证副本按照固定的顺序启动、更新或者删除。

Job 用于运行结束就删除的应用。而其他 Controller 中的 Pod 通常是长期持续运行。

Service 
Deployment 可以部署多个副本,每个 Pod 都有自己的 IP,外界如何访问这些副本呢?

通过 Pod 的 IP 吗?
要知道 Pod 很可能会被频繁地销毁和重启,它们的 IP 会发生变化,用 IP 来访问不太现实。

答案是 Service。
Kubernetes Service 定义了外界访问一组特定 Pod 的方式。Service 有自己的 IP 和端口,Service 为 Pod 提供了负载均衡。

Namespace

如果有多个用户或项目组使用同一个 Kubernetes Cluster,如何将他们创建的 Controller、Pod 等资源分开呢?

答案就是 Namespace。
Namespace 可以将一个物理的 Cluster 逻辑上划分成多个虚拟 Cluster,每个 Cluster 就是一个 Namespace。不同 Namespace 里的资源是完全隔离的。

Kubernetes 默认创建了两个 Namespace。

default -- 创建资源时如果不指定,将被放到这个 Namespace 中。

kube-system -- Kubernetes 自己创建的系统资源将放到这个 Namespace 中

第3章 k8s架构

Master是Kubernetes Cluster的大脑,运行着如下Daemon服务:kube-apiserver、kube-scheduler、kube-controller-manager、etcd和Pod网络(例如 flannel)。

API Server(kube-apiserver)

API Server提供HTTP/HTTPS RESTful API,即Kubernetes API。API Server是Kubernetes Cluster的前端接口,各种客户端工具(CLI 或 UI)以及 Kubernetes其他组件可以通过它管理 Cluster 的各种资源。

 

Scheduler(kube-scheduler)

Scheduler负责决定将Pod 放在哪个 Node上运行。Scheduler在调度时会充分考虑Cluster 的拓扑结构,当前各个节点的负载,以及应用对高可用、性能、数据亲和性的需求。

 

Controller Manager(kube-controller-manager)

Controller Manager 负责管理 Cluster 各种资源,保证资源处于预期的状态。Controller Manager由多种 controller组成,包括 replication controller、endpoints controller、namespace controller、serviceaccounts controller 等。

不同的 controller 管理不同的资源。例如 replication controller 管理 Deployment、StatefulSet、DaemonSet 的生命周期,namespace controller 管理 Namespace 资源。

 

etcd

etcd 负责保存 Kubernetes Cluster 的配置信息和各种资源的状态信息。当数据发生变化时,etcd 会快速地通知 Kubernetes 相关组件。

 

Pod 网络

Pod 要能够相互通信,Kubernetes Cluster 必须部署 Pod 网络,flannel 是其中一个可选方案。

 

Node是Pod运行的地方,Kubernetes支持 Docker、rkt 等容器 Runtime。Node上运行的 Kubernetes 组件有kubelet、kube-proxy 和Pod网络(例如 flannel)。

kubelet

kubelet 是 Node 的 agent,当 Scheduler 确定在某个 Node 上运行 Pod 后,会将 Pod 的具体配置信息(image、volume 等)发送给该节点的 kubelet,kubelet 根据这些信息创建和运行容器,并向 Master 报告运行状态。

kube-proxy

service 在逻辑上代表了后端的多个 Pod,外界通过 service 访问 Pod。service接收到的请求是如何转发到 Pod 的呢?这就是 kube-proxy 要完成的工作。

每个 Node 都会运行 kube-proxy 服务,它负责将访问 service 的 TCP/UPD 数据流转发到后端的容器。如果有多个副本,kube-proxy 会实现负载均衡。

Pod 网络

Pod 要能够相互通信,Kubernetes Cluster必须部署Pod网络,flannel是其中一个可选方案。

[root@k8s_master ~]# kubectl get pod --all-namespaces -o wide

NAMESPACE     NAME                                    READY     STATUS    RESTARTS   AGE       IP           NODE

default       redis                                   1/1       Running   3          1h        172.8.95.2   192.168.138.130

kube-system   kubernetes-dashboard-2318246707-6vmhg   1/1       Running   4          1h        172.8.57.2   192.168.138.162

提前pull好镜像;

[root@k8s_master ~]# kubectl run httpd-app --image=httpd --replicas=2

deployment "httpd-app" created

[root@k8s_master ~]# kubectl get deployment

NAME        DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE

httpd-app   2         2         2            2           16s

[root@k8s_master ~]# kubectl get pod -o wide

NAME                         READY     STATUS    RESTARTS   AGE       IP           NODE

httpd-app-3361905436-4p8vk   1/1       Running   0          30s       172.8.18.2   192.168.138.161

httpd-app-3361905436-cq6xd   1/1       Running   0          30s       172.8.95.3   192.168.138.130

redis                        1/1       Running   3          1h        172.8.95.2   192.168.138.130

 

kubectl 发送部署请求到 API Server。

API Server 通知 Controller Manager 创建一个 deployment 资源。

Scheduler 执行调度任务,将两个副本 Pod 分发到 k8s-node1 和 k8s-node2。

k8s-node1 和 k8s-node2 上的 kubelet 在各自的节点上创建并运行 Pod。

补充两点:

应用的配置和当前状态信息保存在etcd 中,执行kubectl get pod 时,API Server 会从 etcd 中读取这些数据。

flannel会为每个Pod都分配IP。因为没有创建service,目前kube-proxy 还没参与进来。

第4章 运行应用

前面我们已经了解到,Kubernetes通过各种Controller来管理Pod的生命周期。为了满足不同业务场景,Kubernetes开发了Deployment、ReplicaSet、DaemonSet、StatefuleSet、Job 等多种Controller。

4.1 Deployment

[root@k8s_master ~]# kubectl get deployment httpd-app

NAME        DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE

httpd-app   2         2         2            2           24m

[root@k8s_master ~]# kubectl describe deployment httpd-app

Name:           httpd-app

Namespace:      default

CreationTimestamp:  Thu, 30 Aug 2018 19:57:36 +0800

Labels:         run=httpd-app

Selector:       run=httpd-app

Replicas:       2 updated | 2 total | 2 available | 0 unavailable

StrategyType:      RollingUpdate

MinReadySeconds:    0

RollingUpdateStrategy:  1 max unavailable, 1 max surge

Conditions:

  Type      Status  Reason

  ----      ------  ------

  Available     True    MinimumReplicasAvailable

OldReplicaSets: <none>

NewReplicaSet:  httpd-app-3361905436 (2/2 replicas created)

Events:

  FirstSeen LastSeen    Count   From               SubObjectPath   Type        Reason          Message

  --------- --------    -----   ----               -------------   --------    ------          -------

  25m       25m     1   {deployment-controller }            Normal      ScalingReplicaSet   Scaled up replica set httpd-app-3361905436 to 2

大部分内容都是自解释的,我们重点看最下面部分。这里告诉我们创建了一个 ReplicaSet nginx-deployment-1260880958,Events 是 Deployment 的日志,记录了ReplicaSet 的启动过程。

通过上面的分析,也验证了Deployment通过ReplicaSet来管理Pod的事实接着我们将注意力切换到 nginx-deployment-1260880958,执行 kubectl describe replicaset:

 

Controlled By指明此ReplicaSet是由 Deployment nginx-deployment创建。Events记录了两个副本Pod的创建。接着我们来看Pod执行 kubectl get pod:

[root@k8s_master ~]# kubectl get pod

NAME                         READY     STATUS    RESTARTS   AGE

httpd-app-3361905436-4p8vk   1/1       Running   0          2h

httpd-app-3361905436-cq6xd   1/1       Running   0          2h

redis                        1/1       Running   3          3h

 

 

Controlled By 指明此Pod是由ReplicaSet nginx-deployment-1260880958 创建。Events记录了Pod的启动过程。如果操作失败(比如 image 不存在)也能在这里查看到原因。

总结一下这个过程:

用户通过 kubectl 创建 Deployment。

Deployment 创建 ReplicaSet。

ReplicaSet 创建 Pod。

 

从上图也可以看出,对象的命名方式是:子对象的名字=父对象名字 + 随机字符串或数字。

4.2 K8s资源创建方式

Kubernetes 支持两种方式创建资源:

1.用 kubectl 命令直接创建,比如:

kubectl run nginx-deployment --image=nginx:1.7.9 --replicas=2

在命令行中通过参数指定资源的属性。

2. 通过配置文件和 kubectl apply 创建,要完成前面同样的工作,可执行命令:

kubectl apply -f nginx.yml

nginx.yml

 

 

资源的属性写在配置文件中,文件格式为 YAML。

下面对这两种方式进行比较。

基于命令的方式:

简单直观快捷,上手快。

适合临时测试或实验。

 

基于配置文件的方式:

配置文件描述了 What,即应用最终要达到的状态。

配置文件提供了创建资源的模板,能够重复部署。

可以像管理代码一样管理部署。

适合正式的、跨环境的、规模化部署。

这种方式要求熟悉配置文件的语法,有一定难度。

后面我们都将采用配置文件的方式,大家需要尽快熟悉和掌握。

 

kubectl apply 不但能够创建 Kubernetes 资源,也能对资源进行更新,非常方便。不过 Kubernets 还提供了几个类似的命令,例如 kubectl create、kubectl replace、kubectl edit 和 kubectl patch。

为避免造成不必要的困扰,我们会尽量只使用 kubectl apply

[root@k8s_master ~]# kubectl apply -f nginx.yml

deployment "nginx-deployment" created

[root@k8s_master ~]# cat nginx.yml

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

  name: nginx-deployment

spec:

  replicas: 2

  template:

    metadata:

      labels:

        app: web_server

    spec:

      containers:

      - name: nginx

        image: nginx:1.7.9

[root@k8s_master ~]#  kubectl get pod --all-namespaces

NAMESPACE     NAME                                    READY     STATUS    RESTARTS   AGE

default       httpd-app-3361905436-4p8vk              1/1       Running   0          2h

default       httpd-app-3361905436-cq6xd              1/1       Running   0          2h

default       nginx-deployment-2472058307-1lrwc       1/1       Running   0          56s

default       nginx-deployment-2472058307-n786m       1/1       Running   0          56s

default       redis                                   1/1       Running   3          4h

kube-system   kubernetes-dashboard-2318246707-6vmhg   1/1       Running   4          4h

 

yaml配置文件解释

 

apiVersion 是当前配置格式的版本。

kind 是要创建的资源类型,这里是 Deployment。

metadata 是该资源的元数据,name 是必需的元数据项。

spec 部分是该 Deployment 的规格说明。

replicas 指明副本数量,默认为 1。

template 定义 Pod 的模板,这是配置文件的重要部分。

metadata 定义 Pod 的元数据,至少要定义一个 label。label 的 key 和 value 可以任意指定。

spec 描述 Pod 的规格,此部分定义 Pod 中每一个容器的属性,name 和 image 是必需的。

此nginx.yml是一个最简单的Deployment配置文件,后面我们学习Kubernetes各项功能时会逐步丰富这个文件。

执行 kubectl apply -f nginx.yml:

[root@k8s_master ~]# kubectl get deployment

NAME               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE

httpd-app          2         2         2            2           2h

nginx-deployment   2         2         2            2           7m

[root@k8s_master ~]# kubectl get replicaset

NAME                          DESIRED   CURRENT   READY     AGE

httpd-app-3361905436          2         2         2         2h

nginx-deployment-2472058307   2         2         2         7m

[root@k8s_master ~]# kubectl get pod -o wide

NAME                                READY     STATUS    RESTARTS   AGE       IP           NODE

httpd-app-3361905436-4p8vk          1/1       Running   0          2h        172.8.18.2   192.168.138.161

httpd-app-3361905436-cq6xd          1/1       Running   0          2h        172.8.95.3   192.168.138.130

nginx-deployment-2472058307-1lrwc   1/1       Running   0          7m        172.8.57.3   192.168.138.162

nginx-deployment-2472058307-n786m   1/1       Running   0          7m        172.8.18.3   192.168.138.161

redis                               1/1       Running   3          4h        172.8.95.2   192.168.138.130

Deployment、ReplicaSet、Pod 都已经就绪。如果要删除这些资源,执行kubectl delete deployment nginx-deployment或者kubectl delete -f nginx.yml。

 

 

4.3 Scale Up/Down

伸缩(Scale Up/Down)是指在线增加或减少 Pod 的副本数。

Deployment nginx-deployment 初始是两个副本

修改nginx.yml

[root@k8s_master ~]# cat nginx.yml

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

  name: nginx-deployment

spec:

  replicas: 5

  template:

    metadata:

      labels:

        app: web_server

    spec:

      containers:

      - name: nginx

        image: nginx:1.7.9

[root@k8s_master ~]# kubectl apply -f nginx.yml

deployment "nginx-deployment" configured

[root@k8s_master ~]# kubectl get pod

NAME                                READY     STATUS    RESTARTS   AGE

nginx-deployment-2472058307-1lrwc   1/1       Running   0          19m

nginx-deployment-2472058307-6mj1s   1/1       Running   0          20s

nginx-deployment-2472058307-b5bp7   1/1       Running   0          20s

nginx-deployment-2472058307-n786m   1/1       Running   0          19m

nginx-deployment-2472058307-sthxc   1/1       Running   0          20s

redis                               1/1       Running   3          4h

[root@k8s_master ~]# cat nginx.yml

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

  name: nginx-deployment

spec:

  replicas: 3

  template:

    metadata:

      labels:

        app: web_server

    spec:

      containers:

      - name: nginx

        image: nginx:1.7.9

[root@k8s_master ~]# kubectl apply -f nginx.yml

deployment "nginx-deployment" configured

[root@k8s_master ~]# kubectl get pod

NAME                                READY     STATUS    RESTARTS   AGE

nginx-deployment-2472058307-1lrwc   1/1       Running   0          21m

nginx-deployment-2472058307-b5bp7   1/1       Running   0          2m

nginx-deployment-2472058307-n786m   1/1       Running   0          21m

redis                               1/1       Running   3          4h

4.4 Failover故障迁移

关闭一个节点

等待一段时间,Kubernetes会检查到k8s-node2不可用,将k8s-node2上的Pod标记为Unknown状态,并在 k8s-node1上新创建两个 Pod,维持总副本数为 3

当 k8s-node2 恢复后,Unknown 的 Pod 会被删除,不过已经运行的 Pod 不会重新调度回 k8s-node2。

4.5 用label控制Pod的位置

[root@k8s_master ~]# kubectl get nodes

NAME              STATUS    AGE

192.168.138.130   Ready     5h

192.168.138.161   Ready     5h

192.168.138.162   Ready     5h

[root@k8s_master ~]# kubectl get node --show-labels

NAME              STATUS    AGE       LABELS

192.168.138.130   Ready     5h        beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=192.168.138.130

192.168.138.161   Ready     5h        beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=192.168.138.161

192.168.138.162   Ready     5h        beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=192.168.138.162

[root@k8s_master ~]# kubectl label node 192.168.138.162 disktype=ssd

node "192.168.138.162" labeled

[root@k8s_master ~]# kubectl get node --show-labels

NAME              STATUS    AGE       LABELS

192.168.138.130   Ready     5h        beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=192.168.138.130

192.168.138.161   Ready     5h        beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=192.168.138.161

192.168.138.162   Ready     5h        beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,disktype=ssd,kubernetes.io/hostname=192.168.138.162

修改nginx.yml

[root@k8s_master ~]# cat nginx.yml

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

  name: nginx-deployment

spec:

  replicas: 3

  template:

    metadata:

      labels:

        app: web_server

    spec:

      containers:

      - name: nginx

        image: nginx:1.7.9

      nodeSelector:

        disktype: ssd

[root@k8s_master ~]# kubectl delete -f nginx.yml

deployment "nginx-deployment" deleted

[root@k8s_master ~]# kubectl get pod

NAME      READY     STATUS    RESTARTS   AGE

redis     1/1       Running   3          4h

[root@k8s_master ~]# vim nginx.yml

[root@k8s_master ~]# kubectl get pod -o wide

NAME      READY     STATUS    RESTARTS   AGE       IP           NODE

redis     1/1       Running   3          4h        172.8.95.2   192.168.138.130

[root@k8s_master ~]# kubectl apply -f nginx.yml

deployment "nginx-deployment" created

[root@k8s_master ~]# kubectl get pod -o wide

NAME                                READY     STATUS    RESTARTS   AGE       IP           NODE

nginx-deployment-1851826925-f0gpc   1/1       Running   0          10s       172.8.57.4   192.168.138.162

nginx-deployment-1851826925-phw8g   1/1       Running   0          10s       172.8.57.5   192.168.138.162

nginx-deployment-1851826925-ttsqm   1/1       Running   0          10s       172.8.57.3   192.168.138.162

redis                               1/1       Running   3          4h        172.8.95.2   192.168.138.130

要删除 label disktype,执行如下命令:

[root@k8s_master ~]# kubectl label node 192.168.138.162 disktype-

node "192.168.138.162" labeled

[root@k8s_master ~]# kubectl get node --show-labels

NAME              STATUS    AGE       LABELS

192.168.138.130   Ready     6h        beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=192.168.138.130

192.168.138.161   Ready     6h        beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=192.168.138.161

192.168.138.162   Ready     6h        beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=192.168.138.162

不过此时 Pod 并不会重新部署,依然在 192.168.138.162上运行

4.6 DaemonSet典型应用

因为部署原因,待补充;

Deployment 部署的副本 Pod 会分布在各个 Node 上,每个 Node 都可能运行好几个副本。DaemonSet 的不同之处在于:每个 Node 上最多只能运行一个副本。

DaemonSet 的典型应用场景有:

在集群的每个节点上运行存储 Daemon,比如 glusterd 或 ceph。

在每个节点上运行日志收集 Daemon,比如 flunentd 或 logstash。

在每个节点上运行监控 Daemon,比如 Prometheus Node Exporter 或 collectd。

其实 Kubernetes 自己就在用 DaemonSet 运行系统组件。执行如下命令:

[root@k8s_master ~]# kubectl get daemonset --namespace=kube-system

No resources found.

Kubernetes集群中每个当前运行的资源都可以通过kubectl edit查看其配置和运行状态,比如 kubectl edit deployment nginx-deployment。

[root@k8s_master ~]# kubectl edit deployment nginx-deployment

 

# Please edit the object below. Lines beginning with a '#' will be ignored,

# and an empty file will abort the edit. If an error occurs while saving this file will be

# reopened with the relevant failures.

#

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

  annotations:

    deployment.kubernetes.io/revision: "1"

    kubectl.kubernetes.io/last-applied-configuration: '{"kind":"Deployment","apiVersion":"extensions/v1beta1","metadata":{"name":"nginx-deployment","creationTimestamp":null},"spec":{"replicas":3,"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"web_server"}},"spec":{"containers":[{"name":"nginx","image":"nginx:1.7.9","resources":{}}],"nodeSelector":{"disktype":"ssd"}}},"strategy":{}},"status":{}}'

  creationTimestamp: 2018-08-30T15:11:23Z

  generation: 1

  labels:

    app: web_server

  name: nginx-deployment

  namespace: default

  resourceVersion: "36897"

  selfLink: /apis/extensions/v1beta1/namespaces/default/deployments/nginx-deployment

  uid: f55b248b-ac66-11e8-97ec-000c29cc726b

spec:

  replicas: 3

  selector:

    matchLabels:

      app: web_server

  strategy:

    rollingUpdate:

      maxSurge: 1

      maxUnavailable: 1

    type: RollingUpdate

  template:

    metadata:

      creationTimestamp: null

      labels:

        app: web_server

    spec:

      containers:

      - image: nginx:1.7.9

        imagePullPolicy: IfNotPresent

        name: nginx

        resources: {}

        terminationMessagePath: /dev/termination-log

      dnsPolicy: ClusterFirst

      nodeSelector:

        disktype: ssd

      restartPolicy: Always

      securityContext: {}

      terminationGracePeriodSeconds: 30

status:

  availableReplicas: 3

  conditions:

  - lastTransitionTime: 2018-08-30T16:34:59Z

    lastUpdateTime: 2018-08-30T16:34:59Z

    message: Deployment has minimum availability.

    reason: MinimumReplicasAvailable

    status: "True"

    type: Available

  observedGeneration: 1

  replicas: 3

  updatedReplicas: 3

kind: deployment类型的资源。

containers 定义了nginx的容器。

status是当前deployment的运行时状态,这个部分是 kubectl edit特有的。

[root@k8s_master ~]# cat prom.yml

apiVersion: extensions/v1beta1

kind: DaemonSet

metadata:

  name: node-exporter-daemonset

spec:

  template:

    metadata:

      labels:

        app: prometheus

    spec:

      hostNetwork: true   #直接使用 Host 的网络。

      containers:

      - name: node-exporter

        image: prom/node-exporter

        imagePullPolicy: IfNotPresent

        command:  #设置容器启动命令。

        - /bin/node_exporter

        - --path.procfs

        - /host/proc

        - --path.sysfs

        - /host/sys

        - --collector.filesystem.ignored-mount-points

        - ^/(sys|proc|dev|host|etc)($|/)

        volumeMounts: #通过 Volume 将 Host 路径 /proc、/sys 和 / 映射到容器中。

        - name: proc

          mountPath: /host/proc

        - name: sys

          mountPath: /host/sys

        - name: root

          mountPath: /rootfs

      volumes:

      - name: proc

        hostPath:

          path: /proc

      - name: sys

        hostPath :

          path: /sys

      - name: root

        hostPath:

          path: /

[root@k8s_master ~]# kubectl apply -f prom.yml

daemonset "node-exporter-daemonset" created

[root@k8s_master ~]# kubectl get pod -o wide

NAME                                READY     STATUS    RESTARTS   AGE       IP                NODE

nginx-deployment-1851826925-f0gpc   1/1       Running   0          4h        172.8.57.4        192.168.138.162

nginx-deployment-1851826925-phw8g   1/1       Running   0          4h        172.8.57.5        192.168.138.162

nginx-deployment-1851826925-ttsqm   1/1       Running   0          4h        172.8.57.3        192.168.138.162

node-exporter-daemonset-0b3zt       1/1       Running   0          4m        192.168.138.130   192.168.138.130

node-exporter-daemonset-mr3qp       1/1       Running   0          4m        192.168.138.162   192.168.138.162

node-exporter-daemonset-xl4gb       1/1       Running   0          4m        192.168.138.161   192.168.138.161

redis                               1/1       Running   3          9h        172.8.95.2        192.168.138.130

4.7 用k8s运行一次性任务

容器按照持续运行的时间可分为两类:服务类容器和工作类容器。

服务类容器通常持续提供服务,需要一直运行,比如 http server,daemon 等。工作类容器则是一次性任务,比如批处理程序,完成后容器就退出。

Kubernetes的Deployment、ReplicaSet和DaemonSet都用于管理服务类容器;对于工作类容器,我们用 Job。

4.7.1 一次性job

[root@k8s_master ~]# cat myjob.yml

apiVersion: batch/v1 #batch/v1 是当前 Job 的 apiVersion。

kind: Job  #指明当前资源的类型为 Job

metadata:

  name: myjob

spec:

  template:

    metadata:

      name: myjob

    spec:

      containers:

      - name: hello

        image: busybox

        command: ["echo", "hello k8s job! "]

      restartPolicy: Never

#restartPolicy 指定什么情况下需要重启容器。对于Job,只能设置为Never或者OnFailure。对于其他controller(比如 Deployment)可以设置为 Always 。

[root@k8s_master ~]# kubectl apply -f myjob.yml

job "myjob" created

[root@k8s_master ~]# kubectl get pod

NAME                                READY     STATUS    RESTARTS   AGE

nginx-deployment-1851826925-f0gpc   1/1       Running   0          7h

nginx-deployment-1851826925-phw8g   1/1       Running   0          7h

nginx-deployment-1851826925-ttsqm   1/1       Running   0          7h

node-exporter-daemonset-0b3zt       1/1       Running   0          2h

node-exporter-daemonset-mr3qp       1/1       Running   0          2h

node-exporter-daemonset-xl4gb       1/1       Running   0          2h

redis                               1/1       Running   3          11h

[root@k8s_master ~]# kubectl get job

NAME      DESIRED   SUCCESSFUL   AGE

myjob     1         1            57s

DESIRED和SUCCESSFUL都为1,表示按照预期启动了一个Pod,并且已经成功执行。kubectl get pod查看Pod的状态

[root@k8s_master ~]# kubectl get pod --show-all

NAME                                READY     STATUS      RESTARTS   AGE

myjob-24bwp                         0/1       Completed   0          3m

nginx-deployment-1851826925-f0gpc   1/1       Running     0          7h

nginx-deployment-1851826925-phw8g   1/1       Running     0          7h

nginx-deployment-1851826925-ttsqm   1/1       Running     0          7h

node-exporter-daemonset-0b3zt       1/1       Running     0          2h

node-exporter-daemonset-mr3qp       1/1       Running     0          2h

node-exporter-daemonset-xl4gb       1/1       Running     0          2h

redis                               1/1       Running     3          11h

因为Pod执行完毕后容器已经退出,需要用--show-all才能查看Completed状态的Pod。

[root@k8s_master ~]# kubectl logs myjob-24bwp

hello k8s job!

[root@k8s_master ~]# kubectl delete -f myjob.yml

job "myjob" deleted

修改myjob.yml,故意引入一个错误:

[root@k8s_master ~]# cat myjob.yml

apiVersion: batch/v1

kind: Job

metadata:

  name: myjob

spec:

  template:

    metadata:

      name: myjob

    spec:

      containers:

      - name: hello

        image: busybox

        command: ["invalidecho", "hello k8s job! "]

      restartPolicy: Never

[root@k8s_master ~]# kubectl get job

NAME      DESIRED   SUCCESSFUL   AGE

myjob     1         0            28s

[root@k8s_master ~]# kubectl get pod --show-all

NAME                                READY     STATUS               RESTARTS   AGE

myjob-0p538                         0/1       ContainerCreating    0          2s

myjob-76vb7                         0/1       ContainerCannotRun   0          14s

myjob-8blcd                         0/1       ContainerCannotRun   0          26s

myjob-8r379                         0/1       ContainerCannotRun   0          32s

myjob-hd55m                         0/1       ContainerCannotRun   0          39s

myjob-pwvk2                         0/1       ContainerCannotRun   0          20s

myjob-qznph                         0/1       ContainerCannotRun   0          8s

nginx-deployment-1851826925-f0gpc   1/1       Running              0          7h

nginx-deployment-1851826925-phw8g   1/1       Running              0          7h

nginx-deployment-1851826925-ttsqm   1/1       Running              0          7h

node-exporter-daemonset-0b3zt       1/1       Running              0          2h

node-exporter-daemonset-mr3qp       1/1       Running              0          2h

node-exporter-daemonset-xl4gb       1/1       Running              0          2h

redis                               1/1       Running              3          12h

原因是:当第一个Pod启动时,容器失败退出,根据 restartPolicy: Never,此失败容器不会被重启,但Job DESIRED的Pod是1,目前SUCCESSFUL为0,不满足,所以 Job controller会启动新的 Pod,直到 SUCCESSFUL 为 1。对于我们这个例子,SUCCESSFUL 永远也到不了1,所以 Job controller会一直创建新的Pod。为了终止这个行为,只能删除Job。

[root@k8s_master ~]# kubectl delete -f myjob.yml

job "myjob" deleted

[root@k8s_master ~]# vim myjob.yml

[root@k8s_master ~]# cat myjob.yml

apiVersion: batch/v1

kind: Job

metadata:

  name: myjob

spec:

  template:

    metadata:

      name: myjob

    spec:

      containers:

      - name: hello

        image: busybox

        command: ["invalidecho", "hello k8s job! "]

      restartPolicy: OnFailure

 

[root@k8s_master ~]# kubectl get pod --show-all

NAME                                READY     STATUS             RESTARTS   AGE

myjob-sccpv                         0/1       CrashLoopBackOff   3          2m

nginx-deployment-1851826925-f0gpc   1/1       Running            0          7h

nginx-deployment-1851826925-phw8g   1/1       Running            0          7h

nginx-deployment-1851826925-ttsqm   1/1       Running            0          7h

node-exporter-daemonset-0b3zt       1/1       Running            0          3h

node-exporter-daemonset-mr3qp       1/1       Running            0          3h

node-exporter-daemonset-xl4gb       1/1       Running            0          3h

redis                               1/1       Running            3          12h

这里只有一个Pod,不过RESTARTS为3,而且不断增加,说明OnFailure生效,容器失败后会自动重启。

[root@k8s_master ~]# kubectl delete job myjob

job "myjob" deleted

[root@k8s_master ~]# kubectl get job

No resources found.

4.7.2 并行job

有时,我们希望能同时运行多个Pod,提高Job的执行效率。这个可以通过parallelism设置。这里我们将并行的Pod数量设置为2,实践一下:

[root@k8s_master ~]# kubectl apply -f myjob.yml

job "myjob" created

[root@k8s_master ~]# cat myjob.yml

apiVersion: batch/v1

kind: Job

metadata:

  name: myjob

spec:

  parallelism: 2

  template:

    metadata:

      name: myjob

    spec:

      containers:

      - name: hello

        image: busybox

        command: ["echo", "hello k8s job! "]

      restartPolicy: OnFailure

[root@k8s_master ~]# kubectl get job

NAME      DESIRED   SUCCESSFUL   AGE

myjob     <none>    2            1m

[root@k8s_master ~]# kubectl get pod --show-all

NAME                                READY     STATUS      RESTARTS   AGE

myjob-0ll5n                         0/1       Completed   0          1m

myjob-r8fjz                         0/1       Completed   0          1m

nginx-deployment-1851826925-f0gpc   1/1       Running     0          7h

nginx-deployment-1851826925-phw8g   1/1       Running     0          7h

nginx-deployment-1851826925-ttsqm   1/1       Running     0          7h

node-exporter-daemonset-0b3zt       1/1       Running     0          3h

node-exporter-daemonset-mr3qp       1/1       Running     0          3h

node-exporter-daemonset-xl4gb       1/1       Running     0          3h

redis                               1/1       Running     3          12h

 

我们还可以通过completions设置Job成功完成Pod的总数

上面配置的含义是:每次运行两个Pod,直到总共有6个Pod成功完成。实践一下

[root@k8s_master ~]# cat myjob.yml

apiVersion: batch/v1

kind: Job

metadata:

  name: myjob

spec:

  completions: 6

  parallelism: 2

  template:

    metadata:

      name: myjob

    spec:

      containers:

      - name: hello

        image: busybox

        command: ["echo", "hello k8s job! "]

      restartPolicy: OnFailure

[root@k8s_master ~]# kubectl apply -f myjob.yml

The Job "myjob" is invalid: spec.completions: Invalid value: 6: field is immutable

[root@k8s_master ~]# kubectl delete job myjob

job "myjob" deleted

[root@k8s_master ~]# kubectl apply -f myjob.yml

job "myjob" created

[root@k8s_master ~]# kubectl get pod --show-all

NAME                                READY     STATUS      RESTARTS   AGE

myjob-0kll9                         0/1       Completed   0          1m

myjob-35hgj                         0/1       Completed   0          46s

myjob-cchqx                         0/1       Completed   0          58s

myjob-jbmc1                         0/1       Completed   0          48s

myjob-rjktd                         0/1       Completed   0          1m

myjob-tpd9b                         0/1       Completed   0          54s

nginx-deployment-1851826925-f0gpc   1/1       Running     0          7h

nginx-deployment-1851826925-phw8g   1/1       Running     0          7h

nginx-deployment-1851826925-ttsqm   1/1       Running     0          7h

node-exporter-daemonset-0b3zt       1/1       Running     0          3h

node-exporter-daemonset-mr3qp       1/1       Running     0          3h

node-exporter-daemonset-xl4gb       1/1       Running     0          3h

redis                               1/1       Running     3          12h

[root@k8s_master ~]# kubectl get job

NAME      DESIRED   SUCCESSFUL   AGE

myjob     6         6            1m

DESIRED和SUCCESSFUL均为6,符合预期。如果不指定completions和parallelism,默认值均为1。

4.7.3 定时job

Linux中有cron程序定时执行任务,Kubernetes的CronJob提供了类似的功能,可以定时执行Job。

[root@k8s_master ~]# cat cronjob.yml

apiVersion: batch/v2alpha1   #batch/v2alpha1 是当前 CronJob的apiVersion

kind: CronJob  #指明当前资源的类型为 CronJob

metadata:

  name: hello

spec:

  schedule: "*/1 * * * *"  #schedule 指定什么时候运行 Job,其格式与 Linux cron一致。

  jobTemplate:   #jobTemplate定义Job的模板,格式与前面Job一致。

    spec:

      template:

        spec:

          containers:

          - name: hello

            image: busybox

            command: ["echo", "hello k8s job! "]

          restartPolicy: OnFailure

[root@k8s_master ~]# kubectl apply -f cronjob.yml

error: error validating "cronjob.yml": error validating data: couldn't find type: v2alpha1.CronJob; if you choose to ignore these errors, turn validation off with --validate=false

[root@k8s_master ~]# kubectl api-versions

apps/v1beta1

authentication.k8s.io/v1beta1

authorization.k8s.io/v1beta1

autoscaling/v1

batch/v1

certificates.k8s.io/v1alpha1

extensions/v1beta1

policy/v1beta1

rbac.authorization.k8s.io/v1alpha1

storage.k8s.io/v1beta1

v1

没有batch/v2alpha1

[root@k8s_master ~]# kubectl version

Client Version: version.Info{Major:"1", Minor:"5", GitVersion:"v1.5.2", GitCommit:"269f928217957e7126dc87e6adfa82242bfe5b1e", GitTreeState:"clean", BuildDate:"2017-07-03T15:31:10Z", GoVersion:"go1.7.4", Compiler:"gc", Platform:"linux/amd64"}

Server Version: version.Info{Major:"1", Minor:"5", GitVersion:"v1.5.2", GitCommit:"269f928217957e7126dc87e6adfa82242bfe5b1e", GitTreeState:"clean", BuildDate:"2017-07-03T15:31:10Z", GoVersion:"go1.7.4", Compiler:"gc", Platform:"linux/amd64"}

[root@k8s_master ~]# cd /etc/kubernetes/

 [root@k8s_master kubernetes]# cat apiserver

###

# kubernetes system config

#

# The following values are used to configure the kube-apiserver

#

 

# The address on the local server to listen to.

KUBE_API_ADDRESS="--insecure-bind-address=0.0.0.0"

 

# The port on the local server to listen on.

KUBE_API_PORT="--port=8080"

 

# Port minions listen on

# KUBELET_PORT="--kubelet-port=10250"

 

# Comma separated list of nodes in the etcd cluster

KUBE_ETCD_SERVERS="--etcd-servers=http://192.168.138.130:2379"

 

# Address range to use for services

KUBE_SERVICE_ADDRESSES="--service-cluster-ip-range=10.254.0.0/16"

 

# default admission control policies

KUBE_ADMISSION_CONTROL="--admission-control=NamespaceLifecycle,NamespaceExists,LimitRanger,ResourceQuota"

 

# Add your own!

KUBE_API_ARGS="--runtime-config=batch/v2alpha1=true"

[root@k8s_master ~]# for SERVICES in docker kube-apiserver kube-controller-manager kube-scheduler; do systemctl restart $SERVICES ; done

[root@k8s_master ~]# kubectl api-versions

apps/v1beta1

authentication.k8s.io/v1beta1

authorization.k8s.io/v1beta1

autoscaling/v1

batch/v1

batch/v2alpha1

certificates.k8s.io/v1alpha1

extensions/v1beta1

policy/v1beta1

rbac.authorization.k8s.io/v1alpha1

storage.k8s.io/v1beta1

v1

[root@k8s_master ~]# kubectl apply -f cronjob.yml

cronjob "hello" created

[root@k8s_master ~]# kubectl get cronjob

NAME      SCHEDULE      SUSPEND   ACTIVE    LAST-SCHEDULE

hello     */1 * * * *   False     0         Fri, 31 Aug 2018 07:38:00 +0800

[root@k8s_master ~]# kubectl get job

NAME               DESIRED   SUCCESSFUL   AGE

hello-1535671980   1         1            6m

hello-1535672040   1         1            5m

hello-1535672100   1         1            4m

hello-1535672160   1         1            3m

hello-1535672220   1         1            2m

hello-1535672280   1         1            1m

hello-1535672340   1         1            18s

[root@k8s_master ~]# kubectl get pod --show-all

NAME                                READY     STATUS      RESTARTS   AGE

hello-1535671980-t65n2              0/1       Completed   0          7m

hello-1535672040-grvbw              0/1       Completed   0          6m

hello-1535672100-0s9zw              0/1       Completed   0          5m

hello-1535672160-dbmkj              0/1       Completed   0          4m

hello-1535672220-v0lfr              0/1       Completed   0          3m

hello-1535672280-kq0cz              0/1       Completed   0          2m

hello-1535672340-9b2wz              0/1       Completed   0          1m

nginx-deployment-1851826925-f0gpc   1/1       Running     0          8h

nginx-deployment-1851826925-phw8g   1/1       Running     0          8h

nginx-deployment-1851826925-ttsqm   1/1       Running     0          8h

 

运行容器化应用是 Kubernetes 最重要的核心功能。为满足不同的业务需要,Kubernetes提供了多种Controller,包括Deployment、DaemonSet、Job、CronJob等。

4.8 Service

我们不应该期望Kubernetes Pod 是健壮的,而是要假设Pod中的容器很可能因为各种原因发生故障而死掉。Deployment等controller 会通过动态创建和销毁Pod来保证应用整体的健壮性。换句话说,Pod是脆弱的,但应用是健壮的。

每个 Pod 都有自己的 IP 地址。当 controller用新Pod替代发生故障的 Pod 时,新 Pod 会分配到新的 IP 地址。这样就产生了一个问题:

如果一组 Pod 对外提供服务(比如 HTTP),它们的 IP 很有可能发生变化,那么客户端如何找到并访问这个服务呢?

Kubernetes 给出的解决方案是Service。

Kubernetes Service从逻辑上代表了一组Pod,具体是哪些Pod则是由label来挑选。Service有自己IP,而且这个IP是不变的。客户端只需要访问Service的 IP,Kubernetes 则负责建立和维护Service与Pod的映射关系。无论后端 Pod 如何变化,对客户端不会有任何影响,因为 Service 没有变。

4.8.1 Ip service

[root@k8s_master ~]# kubectl apply -f http.yml

deployment "httpd" created

[root@k8s_master ~]# cat http.yml

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

  name: httpd

spec:

  replicas: 3

  template:

    metadata:

      labels:

        run: httpd

    spec:

      containers:

      - name: httpd

        image: httpd

        ports:

        - containerPort: 80

[root@k8s_master ~]# kubectl get pod

NAME                            READY     STATUS    RESTARTS   AGE

httpd-3937122862-00vn5          1/1       Running   0          8s

httpd-3937122862-7xbsp          1/1       Running   0          8s

httpd-3937122862-nkln3          1/1       Running   0          8s

node-exporter-daemonset-0b3zt   1/1       Running   1          5h

node-exporter-daemonset-mr3qp   1/1       Running   0          5h

node-exporter-daemonset-xl4gb   1/1       Running   0          5h

redis                           1/1       Running   4          15h

[root@k8s_master ~]# kubectl get pod -o wide

NAME                            READY     STATUS    RESTARTS   AGE       IP                NODE

httpd-3937122862-00vn5          1/1       Running   0          1m        172.8.18.2        192.168.138.161

httpd-3937122862-7xbsp          1/1       Running   0          1m        172.8.95.3        192.168.138.130

httpd-3937122862-nkln3          1/1       Running   0          1m        172.8.57.3        192.168.138.162

node-exporter-daemonset-0b3zt   1/1       Running   1          5h        192.168.138.130   192.168.138.130

node-exporter-daemonset-mr3qp   1/1       Running   0          5h        192.168.138.162   192.168.138.162

node-exporter-daemonset-xl4gb   1/1       Running   0          5h        192.168.138.161   192.168.138.161

redis                           1/1       Running   4          15h       172.8.95.2        192.168.138.130

[root@k8s_master ~]# curl 172.8.18.2

<html><body><h1>It works!</h1></body></html>

[root@k8s_master ~]# curl 172.8.95.3

<html><body><h1>It works!</h1></body></html>

[root@k8s_master ~]# curl 172.8.57.3

<html><body><h1>It works!</h1></body></html>

[root@k8s_master ~]# cat httpd_sv.yml

apiVersion: v1

kind: Service

metadata:

  name: httpd-svc

spec:

  type: NodePort

  selector:

    run: httpd

  ports:

  - protocol: TCP

    port: 8082

targetPort: 80

[root@k8s_master ~]# kubectl apply -f httpd_sv.yml

service "httpd-svc" created

[root@k8s_master ~]# kubectl get service

NAME         CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE

httpd-svc    10.254.192.170   <nodes>       8082:32304/TCP   21s

kubernetes   10.254.0.1       <none>        443/TCP          16h

[root@k8s_master ~]# curl 10.254.192.170:8082

<html><body><h1>It works!</h1></body></html>

httpd-svc分配到一个CLUSTER-IP 10.99.229.179。可以通过该 IP 访问后端的 httpd Pod。

通过 kubectl describe 可以查看 httpd-svc 与 Pod 的对应关系

[root@k8s_master ~]# kubectl describe service httpd-svc

Name:           httpd-svc

Namespace:      default

Labels:         <none>

Selector:       run=httpd

Type:           NodePort

IP:         10.254.192.170

Port:           <unset> 8082/TCP

NodePort:       <unset> 32304/TCP

Endpoints:      172.8.18.3:80,172.8.57.4:80,172.8.95.4:80

Session Affinity:   None

No events.

[root@k8s_master ~]# iptables-save

可以通过iptables-save命令打印出当前节点的iptables规则,因为输出较多,这里只截取与 httpd-svc Cluster IP10.99.229.179相关的信息:

这两条规则的含义是:

如果 Cluster 内的 Pod(源地址来自 10.244.0.0/16)要访问 httpd-svc,则允许。

其他源地址访问 httpd-svc,跳转到规则 KUBE-SVC-RL3JAE4GN7VOGDGP。

# Generated by iptables-save v1.4.21 on Fri Aug 31 09:39:39 2018

-A KUBE-SERVICES -d 10.254.192.170/32 -p tcp -m comment --comment "default/httpd-svc: cluster IP" -m tcp --dport 8082 -j KUBE-SVC-RL3JAE4GN7VOGDGP

-A KUBE-SVC-NPX46M4PTMTKRN6Y -m comment --comment "default/kubernetes:https" -j KUBE-SEP-JHVFJXYDAZENHIRG

KUBE-SVC-RL3JAE4GN7VOGDGP 规则如下:

1/3 的概率跳转到规则 KUBE-SEP-C5KB52P4BBJQ35PH。

1/3 的概率(剩下 2/3 的一半)跳转到规则 KUBE-SEP-HGVKQQZZCF7RV4IT。

1/3 的概率跳转到规则 KUBE-SEP-XE25WGVXLHEIRVO5。

上面三个跳转的规则如下:

-A KUBE-SVC-RL3JAE4GN7VOGDGP -m comment --comment "default/httpd-svc:" -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-QOREOV6IV3RD2ZSR

-A KUBE-SVC-RL3JAE4GN7VOGDGP -m comment --comment "default/httpd-svc:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-U44RF5KARTOM77DD

-A KUBE-SVC-RL3JAE4GN7VOGDGP -m comment --comment "default/httpd-svc:" -j KUBE-SEP-CAUY7XVTLK4ODWZ4

即将请求分别转发到后端的三个 Pod。通过上面的分析,我们得到如下结论:

iptables 将访问 Service 的流量转发到后端 Pod,而且使用类似轮询的负载均衡策略。

另外需要补充一点:Cluster 的每一个节点都配置了相同的 iptables 规则,这样就确保了整个Cluster都能够通过 Service的Cluster IP访问Service。

4.8.2 K8s-dns

待补充

4.8.3 对外ip

除了 Cluster 内部可以访问 Service,很多情况我们也希望应用的 Service 能够暴露给 Cluster 外部。Kubernetes 提供了多种类型的 Service,默认是ClusterIP。

ClusterIP

Service 通过 Cluster 内部的 IP 对外提供服务,只有 Cluster 内的节点和 Pod 可访问,这是默认的 Service 类型,前面实验中的 Service 都是 ClusterIP。

NodePort

Service 通过 Cluster 节点的静态端口对外提供服务。Cluster 外部可以通过 <NodeIP>:<NodePort> 访问 Service。

LoadBalancer

Service 利用 cloud provider 特有的 load balancer 对外提供服务,cloud provider 负责将 load balancer 的流量导向 Service。目前支持的 cloud provider 有 GCP、AWS、Azur 等。

下面我们来实践 NodePort,Service httpd-svc 的配置文件修改如下:

Kubernetes 依然会为 httpd-svc 分配一个 ClusterIP,不同的是:

EXTERNAL-IP 为 nodes,表示可通过 Cluster 每个节点自身的 IP 访问 Service。

PORT(S)为 8082:32304。8082是 ClusterIP 监听的端口,32304 则是节点上监听的端口。Kubernetes会从30000-32767中分配一个可用的端口,每个节点都会监听此端口并将请求转发给Service。

 

[root@k8s_master ~]# cat httpd_sv.yml

apiVersion: v1

kind: Service

metadata:

  name: httpd-svc

spec:

  type: NodePort

  selector:

    run: httpd

  ports:

  - protocol: TCP

    port: 8082

targetPort: 80

[root@k8s_master ~]# kubectl apply -f httpd_sv.yml

service "httpd-svc" created

[root@k8s_master ~]# kubectl get service

NAME         CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE

httpd-svc    10.254.192.170   <nodes>       8082:32304/TCP   46m

kubernetes   10.254.0.1       <none>        443/TCP          17h

[root@k8s_master ~]# netstat -lntup|grep 32304

tcp6       0      0 :::32304                :::*                    LISTEN      1138/kube-proxy    

[root@k8s_master ~]# curl 192.168.138.130:32304

<html><body><h1>It works!</h1></body></html>

[root@k8s_master ~]# curl 192.168.138.161:32304

<html><body><h1>It works!</h1></body></html>

[root@k8s_master ~]# curl 192.168.138.162:32304

<html><body><h1>It works!</h1></body></html>

NodePort 默认是的随机选择,不过我们可以用 nodePort 指定某个特定端口。

[root@k8s_master ~]# cat httpd_sv.yml

apiVersion: v1

kind: Service

metadata:

  name: httpd-svc

spec:

  type: NodePort

  selector:

    run: httpd

  ports:

  - protocol: TCP

    nodePort: 32100

    port: 8082

targetPort: 80

现在配置文件中就有三个 Port 了:

nodePort 是节点上监听的端口。

port 是 ClusterIP 上监听的端口。

targetPort 是 Pod 监听的端口。

最终,Node和ClusterIP在各自端口上接收到的请求都会通过iptables转发到Pod的targetPort。

[root@k8s_master ~]# kubectl apply -f httpd_sv.yml

service "httpd-svc" configured

[root@k8s_master ~]# netstat -lntup|grep 32100

tcp6       0      0 :::32100                :::*                    LISTEN      1138/kube-proxy    

[root@k8s_master ~]# curl 192.168.138.130:32100

<html><body><h1>It works!</h1></body></html>

[root@k8s_master ~]# curl 192.168.138.161:32100

<html><body><h1>It works!</h1></body></html>

[root@k8s_master ~]# curl 192.168.138.162:32100

<html><body><h1>It works!</h1></body></html>

4.9 Rolling&&update

[root@k8s_master ~]# cat httpd.yml

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

  name: httpd

spec:

  replicas: 2

  template:

    metadata:

      labels:

        run: httpd

    spec:

      containers:

      - name: httpd

        image: httpd

        ports:

        - containerPort: 80

 

[root@k8s_master ~]# kubectl apply -f httpd.yml

deployment "httpd" configured

[root@k8s_master ~]# kubectl get pod -o wide

NAME                            READY     STATUS    RESTARTS   AGE       IP                NODE

httpd-3457006022-0bsbg          1/1       Running   0          2h        172.8.18.3        192.168.138.161

httpd-3457006022-932jz          1/1       Running   0          2h        172.8.57.4        192.168.138.162

node-exporter-daemonset-0b3zt   1/1       Running   1          8h        192.168.138.130   192.168.138.130

node-exporter-daemonset-mr3qp   1/1       Running   0          8h        192.168.138.162   192.168.138.162

node-exporter-daemonset-xl4gb   1/1       Running   0          8h        192.168.138.161   192.168.138.161

redis                           1/1       Running   4          17h       172.8.95.2        192.168.138.130

[root@k8s_master ~]# cat httpd.yml

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

  name: httpd

spec:

  replicas: 2

  template:

    metadata:

      labels:

        run: httpd

    spec:

      containers:

      - name: httpd

        image: httpd:2.2.31

        ports:

        - containerPort: 80

 

[root@k8s_master ~]# kubectl apply -f httpd.yml

deployment "httpd" configured

[root@k8s_master ~]# kubectl get pod -o wide

NAME                            READY     STATUS    RESTARTS   AGE       IP                NODE

httpd-1478671140-c99t1          1/1       Running   0          5m        172.8.95.3        192.168.138.130

httpd-1478671140-zb8jc          1/1       Running   0          5m        172.8.18.2        192.168.138.161

[root@k8s_master ~]# docker ps

CONTAINER ID        IMAGE                                                        COMMAND                  CREATED             STATUS              PORTS               NAMES

f7314640b4c5        httpd:2.2.31                                                 "httpd-foreground"       5 minutes ago       Up 5 minutes                            k8s_httpd.1cd1feb7_httpd-1478671140-c99t1_default_65fc99ab-acd1-11e8-a26f-000c29cc726b_f457a097

[root@k8s_master ~]# kubectl get replicaset -o wide

NAME               DESIRED   CURRENT   READY     AGE       CONTAINER(S)   IMAGE(S)       SELECTOR

httpd-1478671140   2         2         2         8m        httpd          httpd:2.2.31   pod-template-hash=1478671140,run=httpd

httpd-3457006022   0         0         0         2h        httpd          httpd          pod-template-hash=3457006022,run=httpd

httpd-3510483496   0         0         0         2h        httpd          httpd          pod-template-hash=3510483496,run=httpd

httpd-3937122862   0         0         0         2h        httpd          httpd          pod-template-hash=3937122862,run=httpd

部署过程如下:

 

创建Deployment httpd

创建ReplicaSet httpd-1478671140

创建三个 Pod

当前镜像为 httpd

将配置文件中httpd替换为 httpd:2.2.31,再次执行kubectl apply。

Deployment httpd 的镜像更新为 httpd:2.2.31

新创建了 ReplicaSet httpd-1276601241,镜像为 httpd:2.2.31,并且管理了三个新的 Pod。

之前的 ReplicaSet httpd-551879778 里面已经没有任何 Pod。

结论是:ReplicaSet httpd-551879778 的三个 httpd Pod 已经被 ReplicaSet httpd-1276601241 的三个 httpd:2.2.31 Pod 替换了。

 

具体过程可以通过 kubectl describe deployment httpd 查看

[root@k8s_master ~]# kubectl describe deployment httpd

Name:           httpd

Namespace:      default

CreationTimestamp:  Fri, 31 Aug 2018 09:20:11 +0800

Labels:         run=httpd

Selector:       run=httpd

Replicas:       2 updated | 2 total | 2 available | 0 unavailable

StrategyType:      RollingUpdate

MinReadySeconds:    0

RollingUpdateStrategy:  1 max unavailable, 1 max surge

Conditions:

  Type      Status  Reason

  ----      ------  ------

  Available     True    MinimumReplicasAvailable

OldReplicaSets: <none>

NewReplicaSet:  httpd-1478671140 (2/2 replicas created)

Events:

  FirstSeen LastSeen    Count   From               SubObjectPath   Type        Reason          Message

  --------- --------    -----   ----               -------------   --------    ------          -------

  2h        12m     2   {deployment-controller }            Normal      ScalingReplicaSet   Scaled down replica set httpd-3457006022 to 2

  11m       11m     1   {deployment-controller }            Normal      ScalingReplicaSet   Scaled up replica set httpd-1478671140 to 1

  11m       11m     1   {deployment-controller }            Normal      ScalingReplicaSet   Scaled down replica set httpd-3457006022 to 1

  11m       11m     1   {deployment-controller }            Normal      ScalingReplicaSet   Scaled up replica set httpd-1478671140 to 2

  10m       10m     1   {deployment-controller }            Normal      ScalingReplicaSet   Scaled down replica set httpd-3457006022 to 0

每次只更新替换一个 Pod:

 

ReplicaSet httpd-1276601241 增加一个 Pod,总数为 1。

ReplicaSet httpd-551879778 减少一个 Pod,总数为 2。

ReplicaSet httpd-1276601241 增加一个 Pod,总数为 2。

ReplicaSet httpd-551879778 减少一个 Pod,总数为 1。

ReplicaSet httpd-1276601241 增加一个 Pod,总数为 3。

ReplicaSet httpd-551879778 减少一个 Pod,总数为 0。

每次替换的 Pod 数量是可以定制的。Kubernetes 提供了两个参数 maxSurge 和 maxUnavailable 来精细控制 Pod 的替换数量,我们将在后面结合 Health Check 特性一起讨论。

 

金丝雀 把不同版本放到各自的deployment,通过同一个service访问。具体例子https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments

https://www.jianshu.com/p/022685baba7d

记录回滚

--record的作用是将当前命令记录到revision记录中,这样我们就可以知道每个revison对应的是哪个配置文件。通过 kubectl rollout history deployment httpd 查看revison历史记录。

[root@k8s_master ~]# kubectl apply -f http2.yml --record

deployment "httpd" configured

[root@k8s_master ~]# kubectl apply -f http.yml --record

error: the path "http.yml" does not exist

[root@k8s_master ~]# kubectl apply -f httpd.yml --record

deployment "httpd" configured

[root@k8s_master ~]# kubectl rollout history deployment httpd

deployments "httpd"

REVISION    CHANGE-CAUSE

1       <none>

3       <none>

6       kubectl apply -f http2.yml --record

7       kubectl apply -f httpd.yml --record

CHANGE-CAUSE就是--record 的结果。如果要回滚到某个版本,比如revision 1,可以执行命令 kubectl rollout undo deployment httpd --to-revision=1:

[root@k8s_master ~]# kubectl rollout undo deployment httpd --to-revision=6

deployment "httpd" rolled back

此时revison历史记录也会发生相应变化。

Revison7变成了revison8。不过我们可以通过 CHANGE-CAUSE 知道每个 revison 的具体含义。所以一定要在执行 kubectl apply 时加上 --record参数。

[root@k8s_master ~]# kubectl rollout history deployment httpd

deployments "httpd"

REVISION    CHANGE-CAUSE

1       <none>

3       <none>

7       kubectl apply -f httpd.yml --record

8       kubectl apply -f http2.yml --record

 

4.10 Health check

强大的自愈能力是Kubernetes 这类容器编排引擎的一个重要特性。自愈的默认实现方式是自动重启发生故障的容器。除此之外,用户还可以利用Liveness 和Readiness 探测机制设置更精细的健康检查,进而实现如下需求:

零停机部署。

避免部署无效的镜像。

更加安全的滚动升级。

下面通过实践学习 Kubernetes 的 Health Check 功能。

我们首先学习Kubernetes默认的健康检查机制:

每个容器启动时都会执行一个进程,此进程由Dockerfile的CMD或ENTRYPOINT 指定。

如果进程退出时返回码非零,则认为容器发生故障,Kubernetes就会根据restartPolicy重启容器。

 

下面我们模拟一个容器发生故障的场景,Pod 配置文件如下:

[root@k8s_master ~]# cat healthcheck.yml

apiVersion: v1

kind: Pod

metadata:

  labels:

    test: healthcheck

  name: healthcheck

spec:

  restartPolicy: OnFailure

  containers:

  - name: healthcheck

    image: busybox

    args:

    - /bin/sh

    - -c

    - sleep 10; exit 1

[root@k8s_master ~]# kubectl apply -f healthcheck.yml --record

pod "healthcheck" created

Pod 的 restartPolicy 设置为 OnFailure,默认为 Always。

sleep 10; exit 1模拟容器启动10秒后发生故障。

执行kubectl apply创建 Pod,命名为healthcheck。

[root@k8s_master ~]# kubectl get pod healthcheck

NAME          READY     STATUS    RESTARTS   AGE

healthcheck   0/1       Error     3          1m

在上面的例子中,容器进程返回值非零,Kubernetes则认为容器发生故障,需要重启。

但有不少情况是发生了故障,但进程并不会退出。

比如访问Web服务器时显示500内部错误,可能是系统超载,也可能是资源死锁,此时httpd进程并没有异常退出,在这种情况下重启容器可能是最直接最有效的解决方案,那我们如何利用Health Check机制来处理这类场景呢?

4.10.1 lineness

Liveness探测让用户可以自定义判断容器是否健康的条件。如果探测失败,Kubernetes就会重启容器。

还是举例说明,创建如下Pod:

[root@k8s_master ~]# cat liveness.yml

apiVersion: v1

kind: Pod

metadata:

  labels:

    test: liveness

  name: liveness

spec:

  restartPolicy: OnFailure

  containers:

  - name: liveness

    image: busybox

    args:

    - /bin/sh

    - -c

    - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600

    livenessProbe:

      exec:

        command:

        - cat

        - /tmp/healthy

      initialDelaySeconds: 10

      periodSeconds: 5

启动进程首先创建文件 /tmp/healthy,30 秒后删除,在我们的设定中,如果 /tmp/healthy 文件存在,则认为容器处于正常状态,反正则发生故障。

livenessProbe 部分定义如何执行 Liveness 探测:

探测的方法是:通过 cat 命令检查 /tmp/healthy 文件是否存在。如果命令执行成功,返回值为零,Kubernetes 则认为本次 Liveness 探测成功;如果命令返回值非零,本次 Liveness 探测失败。

initialDelaySeconds: 10 指定容器启动 10 之后开始执行 Liveness 探测,我们一般会根据应用启动的准备时间来设置。比如某个应用正常启动要花 30 秒,那么 initialDelaySeconds 的值就应该大于 30。

periodSeconds: 5 指定每 5 秒执行一次 Liveness 探测。Kubernetes 如果连续执行 3 次 Liveness 探测均失败,则会杀掉并重启容器。

下面创建 Pod liveness:

[root@k8s_master ~]# kubectl apply -f liveness.yml

pod "liveness" created

从配置文件可知,最开始的30秒,/tmp/healthy存在,cat命令返回0,Liveness探测成功,这段时间 kubectl describe pod liveness的Events部分会显示正常的日志。

[root@k8s_master ~]# kubectl describe pod liveness

Events:

  FirstSeen LastSeen    Count   From               SubObjectPath          Type        Reason          Message

  --------- --------    -----   ----               -------------          --------    ------          -------

  1m        1m      1   {kubelet 192.168.138.161}   spec.containers{liveness}    Normal      Pulling         pulling image "busybox"

  1m        1m      2   {kubelet 192.168.138.161}                  Warning     MissingClusterDNS   kubelet does not have ClusterDNS IP configured and cannot create Pod using "ClusterFirst" policy. Falling back to DNSDefault policy.

  1m        1m      1   {kubelet 192.168.138.161}   spec.containers{liveness}    Normal      Pulled          Successfully pulled image "busybox"

  1m        1m      1   {kubelet 192.168.138.161}   spec.containers{liveness}    Normal      Created         Created container with docker id d86661d05d5d; Security:[seccomp=unconfined]

  1m        1m      1   {kubelet 192.168.138.161}   spec.containers{liveness}    Normal      Started         Started container with docker id d86661d05d5d

  27s       27s     1   {default-scheduler }                       Normal      Scheduled       Successfully assigned liveness to 192.168.138.161

35秒之后,日志会显示/tmp/healthy已经不存在,Liveness 探测失败。再过几十秒,几次探测都失败后,容器会被重启。

Events:

  FirstSeen LastSeen    Count   From               SubObjectPath          Type        Reason      Message

  --------- --------    -----   ----               -------------          --------    ------      -------

  2m        2m      1   {kubelet 192.168.138.161}   spec.containers{liveness}    Normal      Created     Created container with docker id d86661d05d5d; Security:[seccomp=unconfined]

  2m        2m      1   {kubelet 192.168.138.161}   spec.containers{liveness}    Normal      Started     Started container with docker id d86661d05d5d

  1m        1m      3   {kubelet 192.168.138.161}   spec.containers{liveness}    Warning     Unhealthy   Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory

 

  1m    1m  1   {default-scheduler }                       Normal  Scheduled       Successfully assigned liveness to 192.168.138.161

  2m    1m  2   {kubelet 192.168.138.161}   spec.containers{liveness}   Normal    Pulling         pulling image "busybox"

  1m    1m  1   {kubelet 192.168.138.161}   spec.containers{liveness}   Normal    Killing         Killing container with docker id d86661d05d5d: pod "liveness_default(415f53a6-acdd-11e8-a26f-000c29cc726b)" container "liveness" is unhealthy, it will be killed and re-created.

  2m    1m  2   {kubelet 192.168.138.161}   spec.containers{liveness}   Normal    Pulled          Successfully pulled image "busybox"

  2m    1m  3   {kubelet 192.168.138.161}                  Warning    MissingClusterDNS   kubelet does not have ClusterDNS IP configured and cannot create Pod using "ClusterFirst" policy. Falling back to DNSDefault policy.

  1m    1m  1   {kubelet 192.168.138.161}   spec.containers{liveness}   Normal    Created         Created container with docker id d166192948c7; Security:[seccomp=unconfined]

  1m    1m  1   {kubelet 192.168.138.161}   spec.containers{liveness}   Normal    Started         Started container with docker id d166192948c7

[root@k8s_master ~]# kubectl get pod liveness

NAME       READY     STATUS    RESTARTS   AGE

liveness   1/1       Running   3          4m

4.10.2 readiness

用户通过Liveness探测可以告诉Kubernetes什么时候通过重启容器实现自愈;Readiness 探测则是告诉 Kubernetes什么时候可以将容器加入到Service负载均衡池中,对外提供服务。

Readiness探测的配置语法与Liveness探测完全一样,下面是个例子:

[root@k8s_master ~]# cat readiness.yml

apiVersion: v1

kind: Pod

metadata:

  labels:

    test: readiness

  name: readiness

spec:

  restartPolicy: OnFailure

  containers:

  - name: readiness

    image: busybox

    args:

    - /bin/sh

    - -c

    - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600

    readinessProbe:

      exec:

        command:

        - cat

        - /tmp/healthy

      initialDelaySeconds: 10

      periodSeconds: 5

[root@k8s_master ~]# kubectl apply -f readiness.yml

pod "readiness" created

[root@k8s_master ~]# kubectl get pod readiness

NAME        READY     STATUS    RESTARTS   AGE

readiness   1/1       Running   0          47s

[root@k8s_master ~]# kubectl get pod readiness

NAME        READY     STATUS    RESTARTS   AGE

readiness   0/1       Running   0          54s

 

Pod readiness 的 READY 状态经历了如下变化:

 

刚被创建时,READY 状态为不可用。

15 秒后(initialDelaySeconds + periodSeconds),第一次进行 Readiness 探测并成功返回,设置 READY 为可用。

30 秒后,/tmp/healthy 被删除,连续 3 次 Readiness 探测均失败后,READY 被设置为不可用。

[root@k8s_master ~]#  kubectl describe pod readiness

Name:       readiness

Namespace:  default

Node:       192.168.138.130/192.168.138.130

Start Time: Fri, 31 Aug 2018 13:32:25 +0800

Labels:     test=readiness

Status:     Running

IP:     172.8.95.4

Events:

  FirstSeen LastSeen    Count   From               SubObjectPath          Type        Reason          Message

  --------- --------    -----   ----               -------------          --------    ------          -------

  2m        2m      1   {default-scheduler }                       Normal      Scheduled       Successfully assigned readiness to 192.168.138.130

  2m        2m      1   {kubelet 192.168.138.130}   spec.containers{readiness}    Normal      Pulling         pulling image "busybox"

  2m        2m      2   {kubelet 192.168.138.130}                  Warning     MissingClusterDNS   kubelet does not have ClusterDNS IP configured and cannot create Pod using "ClusterFirst" policy. Falling back to DNSDefault policy.

  2m        2m      1   {kubelet 192.168.138.130}   spec.containers{readiness}    Normal      Pulled          Successfully pulled image "busybox"

  2m        2m      1   {kubelet 192.168.138.130}   spec.containers{readiness}    Normal      Created         Created container with docker id 265ac2f4e37a; Security:[seccomp=unconfined]

  2m        2m      1   {kubelet 192.168.138.130}   spec.containers{readiness}    Normal      Started         Started container with docker id 265ac2f4e37a

  1m        2s      19  {kubelet 192.168.138.130}   spec.containers{readiness}    Warning     Unhealthy       Readiness probe failed: cat: can't open '/tmp/healthy': No such file or directory

Liveness探测和Readiness探测是两种 Health Check 机制,如果不特意配置,Kubernetes 将对两种探测采取相同的默认行为,即通过判断容器启动进程的返回值是否为零来判断探测是否成功。

两种探测的配置方法完全一样,支持的配置参数也一样。不同之处在于探测失败后的行为:Liveness 探测是重启容器;Readiness 探测则是将容器设置为不可用,不接收 Service 转发的请求。

Liveness 探测和 Readiness 探测是独立执行的,二者之间没有依赖,所以可以单独使用,也可以同时使用。用 Liveness 探测判断容器是否需要重启以实现自愈;用 Readiness 探测判断容器是否已经准备好对外提供服务

readiness的作用是有些服务需要一段时间的预热,对于这种服务,如果不设置readiness,就永远不可用

4.10.3 scale up && health

对于多副本应用,当执行 Scale Up 操作时,新副本会作为 backend 被添加到 Service 的负责均衡中,与已有副本一起处理客户的请求。考虑到应用启动通常都需要一个准备阶段,比如加载缓存数据,连接数据库等,从容器启动到正真能够提供服务是需要一段时间的。我们可以通过 Readiness探测判断容器是否就绪,避免将请求发送到还没有 ready 的 backend。

[root@k8s_master ~]# kubectl apply -f httpd_health.yml --record

deployment "httpd" created

service "httpd-svc" created

[root@k8s_master ~]# cat httpd_health.yml

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

  name: httpd

spec:

  replicas: 3

  template:

    metadata:

      labels:

        run: httpd

    spec:

      containers:

      - name: httpd

        image: httpd:2.2.31

        ports:

        - containerPort: 80

        readinessProbe:

          httpGet:

            scheme: HTTP

            path: /

            port: 80

          initialDelaySeconds: 10

          periodSeconds: 5

 

---

 

apiVersion: v1

kind: Service

metadata:

  name: httpd-svc

spec:

  type: NodePort

  selector:

    run: httpd

  ports:

  - protocol: TCP

    nodePort: 30000

    port: 8082

targetPort: 80

上面配置的作用是:

 

容器启动 10 秒之后开始探测。

如果 http://[container_ip]:8080/healthy 返回代码不是 200-400,表示容器没有就绪,不接收 Service web-svc 的请求。

每隔 5 秒再探测一次。

直到返回代码为 200-400,表明容器已经就绪,然后将其加入到 web-svc 的负责均衡中,开始处理客户请求。

探测会继续以 5 秒的间隔执行,如果连续发生 3 次失败,容器又会从负载均衡中移除,直到下次探测成功重新加入。

对于生产环境中重要的应用都建议配置 Health Check,保证处理客户请求的容器都是准备就绪的 Service backend。

[root@k8s_master ~]# kubectl get pod -o wide

NAME                            READY     STATUS             RESTARTS   AGE       IP                NODE

healthcheck                     0/1       CrashLoopBackOff   14         54m       172.8.95.3        192.168.138.130

httpd-1649560772-0ggrh          1/1       Running            0          2m        172.8.18.3        192.168.138.161

httpd-1649560772-qm2vf          1/1       Running            0          2m        172.8.57.3        192.168.138.162

httpd-1649560772-r26zv          1/1       Running            0          2m        172.8.95.4        192.168.138.130

liveness                        1/1       Running            15         41m       172.8.18.2        192.168.138.161

node-exporter-daemonset-0b3zt   1/1       Running            1          10h       192.168.138.130   192.168.138.130

[root@k8s_master ~]# kubectl describe svc httpd-svc

Name:           httpd-svc

Namespace:      default

Labels:         <none>

Selector:       run=httpd

Type:           NodePort

IP:         10.254.76.56

Port:           <unset> 8082/TCP

NodePort:       <unset> 30000/TCP

Endpoints:      172.8.18.3:80,172.8.57.3:80,172.8.95.4:80

Session Affinity:   None

No events.

 

4.10.4 在滚动更新中使用

现有一个正常运行的多副本应用,接下来对应用进行更新(比如使用更高版本的 image),Kubernetes 会启动新副本,然后发生了如下事件:

正常情况下新副本需要 10 秒钟完成准备工作,在此之前无法响应业务请求。

但由于人为配置错误,副本始终无法完成准备工作(比如无法连接后端数据库)。

先别继续往下看,现在请花一分钟思考这个问题:如果没有配置 Health Check,会出现怎样的情况?

因为新副本本身没有异常退出,默认的 Health Check 机制会认为容器已经就绪,进而会逐步用新副本替换现有副本,其结果就是:当所有旧副本都被替换后,整个应用将无法处理请求,无法对外提供服务。如果这是发生在重要的生产系统上,后果会非常严重。

如果正确配置了 Health Check,新副本只有通过了 Readiness 探测,才会被添加到 Service;如果没有通过探测,现有副本不会被全部替换,业务仍然正常进行。

下面通过例子来实践 Health Check 在 Rolling Update 中的应用。

用如下配置文件 app.v1.yml 模拟一个 10 副本的应用:

[root@k8s_master ~]# kubectl apply -f app.v1.yml --record

deployment "app" created

[root@k8s_master ~]# kubectl get deployment app

NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE

app       10        10        10           0           10s

 [root@k8s_master ~]# kubectl get deployment app

NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE

app       10        10        10           10          47s

[root@k8s_master ~]# cat app.v1.yml

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

  name: app

spec:

  replicas: 10

  template:

    metadata:

      labels:

        run: app

    spec:

      containers:

      - name: app

        image: busybox

        args:

        - /bin/sh

        - -c

        - sleep 10; touch /tmp/healthy; sleep 30000

        readinessProbe:

          exec:

            command:

            - cat

            - /tmp/healthy

          initialDelaySeconds: 10

          periodSeconds: 5

[root@k8s_master ~]# kubectl get pod

NAME                            READY     STATUS             RESTARTS   AGE

app-3748071646-0fsct            1/1       Running            0          3m

app-3748071646-0gdm1            1/1       Running            0          3m

app-3748071646-1ftd4            1/1       Running            0          3m

app-3748071646-242cv            1/1       Running            0          3m

app-3748071646-281v1            1/1       Running            0          3m

app-3748071646-7j3wg            1/1       Running            0          3m

app-3748071646-8dd35            1/1       Running            0          3m

app-3748071646-msq6v            1/1       Running            0          3m

app-3748071646-rd17j            1/1       Running            0          3m

app-3748071646-xqqjk            1/1       Running            0          3m

[root@k8s_master ~]# kubectl get deployment app

NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE

app       10        10        10           10          37s

很显然,由于新副本中不存在 /tmp/healthy,是无法通过 Readiness 探测的。验证如下:

[root@k8s_master ~]# kubectl apply -f app.v2.yml --record

deployment "app" configured

[root@k8s_master ~]# cat app.v2.yml

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

  name: app

spec:

  replicas: 10

  template:

    metadata:

      labels:

        run: app

    spec:

      containers:

      - name: app

        image: busybox

        args:

        - /bin/sh

        - -c

        - sleep 30000

        readinessProbe:

          exec:

            command:

            - cat

            - /tmp/healthy

          initialDelaySeconds: 10

          periodSeconds: 5

[root@k8s_master ~]# kubectl get deployment app

NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE

app       10        11        2            9           57s

[root@k8s_master ~]# kubectl get pod

NAME                            READY     STATUS             RESTARTS   AGE

app-2073951902-2wld1            0/1       Running            0          21s

app-2073951902-mqw85            0/1       Running            0          21s

app-3748071646-3lk5z            1/1       Running            0          1m

app-3748071646-548ww            1/1       Terminating        0          1m

app-3748071646-fj9zz            1/1       Running            0          1m

app-3748071646-kh4xl            1/1       Running            0          1m

app-3748071646-nxrdh            1/1       Running            0          1m

app-3748071646-p5p86            1/1       Running            0          1m

app-3748071646-pcrww            1/1       Running            0          1m

app-3748071646-pwd2r            1/1       Running            0          1m

app-3748071646-sgqzx            1/1       Running            0          1m

一直保持

[root@k8s_master ~]# kubectl get deployment app

NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE

app       10        11        2            9           4m

很显然,由于新副本中不存在 /tmp/healthy,是无法通过 Readiness 探测的。验证如下:

这个截图包含了大量的信息,值得我们详细分析。

先关注 kubectl get pod 输出:

从 Pod 的 AGE 栏可判断,最后 5 个 Pod 是新副本,目前处于 NOT READY 状态。

旧副本从最初 10 个减少到 8 个。

再来看 kubectl get deployment app 的输出:

DESIRED 10 表示期望的状态是 10 个 READY 的副本。

CURRENT 13 表示当前副本的总数:即 8 个旧副本 + 5 个新副本。

UP-TO-DATE 5 表示当前已经完成更新的副本数:即 5 个新副本。

AVAILABLE 8 表示当前处于 READY 状态的副本数:即 8个旧副本。

在我们的设定中,新副本始终都无法通过 Readiness 探测,所以这个状态会一直保持下去。

上面我们模拟了一个滚动更新失败的场景。不过幸运的是:Health Check 帮我们屏蔽了有缺陷的副本,同时保留了大部分旧副本,业务没有因更新失败受到影响。

接下来我们要回答:为什么新创建的副本数是 5 个,同时只销毁了 2 个旧副本?

原因是:滚动更新通过参数 maxSurge 和 maxUnavailable 来控制副本替换的数量。

 

maxSurge

此参数控制滚动更新过程中副本总数的超过 DESIRED 的上限。maxSurge 可以是具体的整数(比如 3),也可以是百分百,向上取整。maxSurge 默认值为 25%。

 

在上面的例子中,DESIRED 为 10,那么副本总数的最大值为:

roundUp(10 + 10 * 25%) = 13

 

所以我们看到 CURRENT 就是 13。

 

maxUnavailable

此参数控制滚动更新过程中,不可用的副本相占 DESIRED 的最大比例。 maxUnavailable 可以是具体的整数(比如 3),也可以是百分百,向下取整。maxUnavailable 默认值为 25%。

 

在上面的例子中,DESIRED 为 10,那么可用的副本数至少要为:

10 - roundDown(10 * 25%) = 8

 

所以我们看到 AVAILABLE 就是 8。

 

maxSurge 值越大,初始创建的新副本数量就越多;maxUnavailable 值越大,初始销毁的旧副本数量就越多。

 

理想情况下,我们这个案例滚动更新的过程应该是这样的:

 

首先创建 3 个新副本使副本总数达到 13 个。

然后销毁 2 个旧副本使可用的副本数降到 8 个。

当这 2 个旧副本成功销毁后,可再创建 2 个新副本,使副本总数保持为 13 个。

当新副本通过 Readiness 探测后,会使可用副本数增加,超过 8。

进而可以继续销毁更多的旧副本,使可用副本数回到 8。

旧副本的销毁使副本总数低于 13,这样就允许创建更多的新副本。

这个过程会持续进行,最终所有的旧副本都会被新副本替换,滚动更新完成。

而我们的实际情况是在第 4 步就卡住了,新副本无法通过 Readiness 探测。这个过程可以在 kubectl describe deployment app 的日志部分查看。

[root@k8s_master ~]#  kubectl describe deployment app

Name:           app

Namespace:      default

CreationTimestamp:  Fri, 31 Aug 2018 14:31:26 +0800

Labels:         run=app

Selector:       run=app

Replicas:       2 updated | 10 total | 9 available | 2 unavailable

StrategyType:      RollingUpdate

MinReadySeconds:    0

RollingUpdateStrategy:  1 max unavailable, 1 max surge

Conditions:

 

  Type      Status  Reason

  ----      ------  ------

  Available     True    MinimumReplicasAvailable

OldReplicaSets: app-3748071646 (9/9 replicas created)

NewReplicaSet:  app-2073951902 (2/2 replicas created)

Events:

  FirstSeen LastSeen    Count   From               SubObjectPath   Type        Reason          Message

  --------- --------    -----   ----               -------------   --------    ------          -------

  9m        9m      1   {deployment-controller }            Normal      ScalingReplicaSet   Scaled up replica set app-3748071646 to 10

  9m        9m      1   {deployment-controller }            Normal      ScalingReplicaSet   Scaled up replica set app-2073951902 to 1

  9m        9m      1   {deployment-controller }            Normal      ScalingReplicaSet   Scaled down replica set app-3748071646 to 9

  9m        9m      1   {deployment-controller }            Normal      ScalingReplicaSet   Scaled up replica set app-2073951902 to 2

如果滚动更新失败,可以通过 kubectl rollout undo 回滚到上一个版本

[root@k8s_master ~]# kubectl rollout history deployment app

deployments "app"

REVISION    CHANGE-CAUSE

1       kubectl apply -f app.v1.yml --record

2       kubectl apply -f app.v2.yml –record

[root@k8s_master ~]# kubectl rollout undo deployment app --to-revision=1

deployment "app" rolled back

[root@k8s_master ~]# kubectl get deployment app

NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE

app       10        10        10           10          13m

如果要定制 maxSurge 和 maxUnavailable,可以如下配置:

 

 

 

4.11 数据管理

首先我们会学习Volume,以及 Kubernetes 如何通过 Volume 为集群中的容器提供存储;然后我们会实践几种常用的Volume 类型并理解它们各自的应用场景;最后,我们会讨论Kubernetes如何通过Persistent Volume和Persistent Volume Claim分离集群管理员与集群用户的职责,并实践 Volume 的静态供给和动态供给。

本节我们讨论 Kubernetes 的存储模型 Volume,学习如何将各种持久化存储映射到容器。

 

我们经常会说:容器和 Pod 是短暂的。

其含义是它们的生命周期可能很短,会被频繁地销毁和创建。容器销毁时,保存在容器内部文件系统中的数据都会被清除。

为了持久化保存容器的数据,可以使用 Kubernetes Volume。

Volume的生命周期独立于容器,Pod 中的容器可能被销毁和重建,但 Volume 会被保留。

本质上,Kubernetes Volume 是一个目录,这一点与 Docker Volume 类似。当 Volume 被 mount 到 Pod,Pod 中的所有容器都可以访问这个 Volume。Kubernetes Volume 也支持多种 backend 类型,包括 emptyDir、hostPath、GCE Persistent Disk、AWS Elastic Block Store、NFS、Ceph 等,完整列表可参考 https://kubernetes.io/docs/concepts/storage/volumes/#types-of-volumes

Volume 提供了对各种 backend 的抽象,容器在使用 Volume 读写数据的时候不需要关心数据到底是存放在本地节点的文件系统中呢还是云硬盘上。对它来说,所有类型的 Volume 都只是一个目录。

我们将从最简单的 emptyDir 开始学习 Kubernetes Volume。

emptyDir 是最基础的 Volume 类型。正如其名字所示,一个 emptyDir Volume 是 Host 上的一个空目录。

emptyDir Volume 对于容器来说是持久的,对于 Pod 则不是。当 Pod 从节点删除时,Volume 的内容也会被删除。但如果只是容器被销毁而 Pod 还在,则 Volume 不受影响。

也就是说:emptyDir Volume 的生命周期与 Pod 一致。

Pod 中的所有容器都可以共享 Volume,它们可以指定各自的mount路径。下面通过例子来实践emptyDir,配置文件如下

[root@k8s_master ~]# cat volume.yml

apiVersion: v1

kind: Pod

metadata:

  name: producer-consumer

spec:

  containers:

  - image: busybox

    name: producer

    volumeMounts:

    - mountPath: /producer_dir

      name: shared-volume

    args:

    - /bin/sh

    - -c

    - echo "hello world" > /producer_dir/hello; sleep 30000

 

  - image: busybox

    name: consumer

    volumeMounts:

    - mountPath: /consumer_dir

      name: shared-volume

    args:

    - /bin/sh

    - -c

    - cat /consumer_dir/hello; sleep 30000

 

  volumes:

  - name: shared-volume

emptyDir: {}

这里我们模拟了一个 producer-consumer 场景。Pod 有两个容器 producer和 consumer,它们共享一个 Volume。producer 负责往 Volume 中写数据,consumer 则是从 Volume 读取数据。

文件最底部 volumes 定义了一个 emptyDir 类型的 Volume shared-volume。

producer 容器将 shared-volume mount 到 /producer_dir 目录。

producer 通过 echo 将数据写到文件 hello 里。

consumer 容器将 shared-volume mount 到 /consumer_dir 目录。

consumer 通过 cat 从文件 hello 读数据。

执行如下命令创建 Pod:

[root@k8s_master ~]# kubectl get pod

NAME                            READY     STATUS             RESTARTS   AGE

producer-consumer               2/2       Running            0          21s

[root@k8s_master ~]# kubectl logs producer-consumer consumer

hello world

kubectl logs 显示容器 consumer 成功读到了 producer 写入的数据,验证了两个容器共享 emptyDir Volume。

因为 emptyDir 是 Docker Host 文件系统里的目录,其效果相当于执行了 docker run -v /producer_dir 和 docker run -v /consumer_dir。通过 docker inspect 查看容器的详细配置信息,我们发现两个容器都 mount 了同一个目录:

Docker inspect 容器id1

"Mounts": [

            {

                "Type": "bind",

                "Source": "/var/lib/kubelet/pods/d37d1a69-aced-11e8-a26f-000c29cc726b/volumes/kubernetes.io~empty-dir/shared-volume",

                "Destination": "/consumer_dir",

                "Mode": "",

                "RW": true,

                "Propagation": "rprivate"

            },

Docker inspect 容器id1

      {

                "Type": "bind",

                "Source": "/var/lib/kubelet/pods/d37d1a69-aced-11e8-a26f-000c29cc726b/volumes/kubernetes.io~empty-dir/shared-volume",

                "Destination": "/producer_dir",

                "Mode": "",

                "RW": true,

                "Propagation": "rprivate"

Hostpath

[root@k8s_master ~]# cat webserver.yml

apiVersion: v1

kind: Pod

metadata:

  name: test-pd

spec:

  containers:

  - image: test-webserver:123

    name: test-container

    volumeMounts:

    - mountPath: /test-pd

      name: test-volume

  volumes:

  - name: test-volume

    hostPath:

      # directory location on host

      path: /data

docker ps

gcr.io/google_containers/test-webserver               latest              25906c5a72ed        3 years ago              4.53 MB

test-webserver                                        123                 25906c5a72ed        3 years ago              4.53 MB

google镜像默认要下载,更改名称就可以了

[root@k8s_master ~]# kubectl apply -f webserver.yml

pod "test-pd" created

[root@k8s_master ~]# kubectl describe pod test-pd

Name:       test-pd

Namespace:  default

Node:       192.168.138.162/192.168.138.162

Start Time: Fri, 31 Aug 2018 18:14:57 +0800

Labels:     <none>

Status:     Running

IP:     172.8.57.4

Controllers:    <none>

Containers:

  test-container:

    Container ID:    docker://bb0bd6782853a168a7cac045ebc5986a39f9669a5923e847f751b79d46d25729

    Image:      test-webserver:123

    Image ID:   docker://sha256:25906c5a72eda6a5007c54bc9bd3ec39e446ef2289c95deb3abe53a0e115e0f0

    Port:      

    State:      Running

      Started:      Fri, 31 Aug 2018 18:14:58 +0800

    Ready:      True

    Restart Count:  0

    Volume Mounts:

      /test-pd from test-volume (rw)

[root@k8s_master ~]# kubectl get pods

NAME                            READY     STATUS             RESTARTS   AGE

healthcheck                     0/1       CrashLoopBackOff   65         5h

test-pd                         1/1       Running            0          9m

[root@k8s_client2 data]#  curl 172.8.57.4

<pre>

<a href="test-pd/">test-pd/</a>

<a href="run/">run/</a>

<a href="etc/">etc/</a>

<a href="dev/">dev/</a>

<a href="sys/">sys/</a>

<a href="proc/">proc/</a>

<a href=".dockerenv">.dockerenv</a>

<a href="test-webserver">test-webserver</a>

</pre>

[root@k8s_client2 data]#  curl 172.8.57.4/test-pd/1.html

houpj

[root@k8s_client2 data]# cat 1.html

Houpj

Ceph、GlusterFS 后续补充

 

4.12 PV&&PVC

Volume 提供了非常好的数据持久化方案,不过在可管理性上还有不足。

 

拿前面 AWS EBS 的例子来说,要使用 Volume,Pod 必须事先知道如下信息:

当前 Volume 来自 AWS EBS。

EBS Volume 已经提前创建,并且知道确切的 volume-id。

Pod 通常是由应用的开发人员维护,而 Volume 则通常是由存储系统的管理员维护。开发人员要获得上面的信息:

要么询问管理员。

要么自己就是管理员。

这样就带来一个管理上的问题:应用开发人员和系统管理员的职责耦合在一起了。如果系统规模较小或者对于开发环境这样的情况还可以接受。但当集群规模变大,特别是对于生成环境,考虑到效率和安全性,这就成了必须要解决的问题。

Kubernetes 给出的解决方案是 PersistentVolume 和 PersistentVolumeClaim。[pə'zɪstənt][klem]

PersistentVolume (PV) 是外部存储系统中的一块存储空间,由管理员创建和维护。与 Volume 一样,PV 具有持久性,生命周期独立于 Pod。

PersistentVolumeClaim (PVC) 是对 PV 的申请 (Claim)。PVC 通常由普通用户创建和维护。需要为 Pod 分配存储资源时,用户可以创建一个 PVC,指明存储资源的容量大小和访问模式(比如只读)等信息,Kubernetes 会查找并提供满足条件的 PV。

有了 PersistentVolumeClaim,用户只需要告诉 Kubernetes 需要什么样的存储资源,而不必关心真正的空间从哪里分配,如何访问等底层细节信息。这些 Storage Provider 的底层信息交给管理员来处理,只有管理员才应该关心创建 PersistentVolume 的细节信息。

Kubernetes 支持多种类型的 PersistentVolume,比如 AWS EBS、Ceph、NFS 等

[root@k8s_master ~]# cat nfs-pv1.yml

apiVersion: v1

kind: PersistentVolume

metadata:

  name: mypv1

spec:

  capacity:

    storage: 1Gi

  accessModes:

    - ReadWriteOnce

  persistentVolumeReclaimPolicy: Recycle

#  storageClassName: nfs  #不支持

  nfs:

    path: /nfsdata/pv1  #指定 PV 在 NFS 服务器上对应的目录。

    server: 192.168.138.130

#下面的也正确

#apiVersion: v1

#kind: PersistentVolume

#metadata:

#  name: nfs

#spec:

#  capacity:

#    storage: 1Mi

#  accessModes:

#    - ReadWriteMany

#  nfs:

#    server: 192.168.138.130

#    path: "/nfsdata/pv1"

capacity 指定 PV 的容量为 1G。

 

accessModes 指定访问模式为 ReadWriteOnce,支持的访问模式有:

ReadWriteOnce – PV 能以 read-write 模式 mount 到单个节点。

ReadOnlyMany – PV 能以 read-only 模式 mount 到多个节点。

ReadWriteMany – PV 能以 read-write 模式 mount 到多个节点。

 

persistentVolumeReclaimPolicy 指定当 PV 的回收策略为 Recycle,支持的策略有:

Retain – 需要管理员手工回收。

Recycle – 清除 PV 中的数据,效果相当于执行 rm -rf /thevolume/*。

Delete – 删除 Storage Provider 上的对应存储资源,例如 AWS EBS、GCE PD、Azure Disk、OpenStack Cinder Volume 等。

 

storageClassName 指定 PV 的 class 为 nfs。相当于为 PV 设置了一个分类,PVC 可以指定 class 申请相应 class 的 PV,这个版本不支持;

 

[root@k8s_master ~]# kubectl apply -f nfs-pv1.yml

error: error validating "nfs-pv1.yml": error validating data: found invalid field storageClassName for v1.PersistentVolumeSpec; if you choose to ignore these errors, turn validation off with --validate=false

 [root@k8s_master ~]# kubectl apply -f nfs-pv1.yml

persistentvolume "mypv1" configured

[root@k8s_master ~]# kubectl get pv

NAME      CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS      CLAIM     REASON    AGE

mypv1     1Gi        RWO           Recycle         Available                       3m

nfs       1Mi        RWX           Retain          Available                       5m

[root@k8s_master ~]# kubectl apply -f nfs-pvc1.yml

persistentvolumeclaim "mypvc1" created

[root@k8s_master ~]# kubectl get pvc

NAME      STATUS    VOLUME    CAPACITY   ACCESSMODES   AGE

mypvc1    Bound     mypv1     1Gi        RWO           17s

[root@k8s_master ~]# kubectl get pv

NAME      CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS      CLAIM            REASON    AGE

mypv1     1Gi        RWO           Recycle         Bound       default/mypvc1             10m

nfs       1Mi        RWX           Retain          Available                              11m

[root@k8s_master ~]# cat nfs-pvc1.yml

kind: PersistentVolumeClaim

apiVersion: v1

metadata:

  name: mypvc1

spec:

  accessModes:

    - ReadWriteOnce

  resources:

    requests:

      storage: 1Gi

[root@k8s_master ~]# cat pod1.yml

kind: Pod

apiVersion: v1

metadata:

  name: mypod1

spec:

  containers:

    - name: mypod1

      image: busybox

      args:

      - /bin/sh

      - -c

      - sleep 30000

      volumeMounts:

      - mountPath: "/mydata"

        name: mydata

  volumes:

    - name: mydata

      persistentVolumeClaim:

        claimName: mypvc1

[root@k8s_master ~]# kubectl apply -f pod1.yml

pod "mypod1" created

[root@k8s_master ~]# kubectl get pod -o wide

NAME                            READY     STATUS             RESTARTS   AGE       IP                NODE

mypod1                          1/1       Running            0          10s       172.8.18.4        192.168.138.161

[root@k8s_master ~]# kubectl exec mypod1 touch /mydata/houpj

[root@k8s_master ~]# ls /nfsdata/pv1/

Houpj

回收

[root@k8s_master ~]# kubectl delete pvc mypvc1

persistentvolumeclaim "mypvc1" deleted

[root@k8s_master ~]# kubectl get pod -o wide

recycler-for-mypv1              0/1       ContainerCreating   0          11s       <none>            192.168.138.161

[root@k8s_master ~]# kubectl get pv

NAME      CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS      CLAIM            REASON    AGE

mypv1     1Gi        RWO           Recycle         Released    default/mypvc1             25m

nfs       1Mi        RWX           Retain          Available                              27m

[root@k8s_master ~]# ls /nfsdata/pv1/

Houpj

[root@k8s_master ~]# kubectl get pv

NAME      CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS      CLAIM            REASON    AGE

mypv1     1Gi        RWO           Recycle         Failed      default/mypvc1             30m

nfs       1Mi        RWX           Retain          Available                              32m

[root@k8s_master ~]# kubectl apply -f nfs-pv1.yml

persistentvolume "mypv1" configured

[root@k8s_master ~]# kubectl get pv

NAME      CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS      CLAIM     REASON    AGE

mypv1     1Gi        RWO           Retain          Available                       49s

nfs       1Mi        RWX           Retain          Available                       35m

[root@k8s_master ~]# cat nfs-pv1.yml

apiVersion: v1

kind: PersistentVolume

metadata:

  name: mypv1

spec:

  capacity:

    storage: 1Gi

  accessModes:

    - ReadWriteOnce

  persistentVolumeReclaimPolicy: Retain

#  storageClassName: nfs  #不支持

  nfs:

    path: /nfsdata/pv1

    server: 192.168.138.130

#下面的也正确

#apiVersion: v1

#kind: PersistentVolume

#metadata:

#  name: nfs

#spec:

#  capacity:

#    storage: 1Mi

#  accessModes:

#    - ReadWriteMany

#  nfs:

#    server: 192.168.138.130

#    path: "/nfsdata/pv1"

[root@k8s_master ~]# cat nfs-pvc1.yml

kind: PersistentVolumeClaim

apiVersion: v1

metadata:

  name: mypvc1

spec:

  accessModes:

    - ReadWriteOnce

  resources:

    requests:

      storage: 1Gi

 

[root@k8s_master ~]# ls /nfsdata/pv1/

houpj

[root@k8s_master ~]# cd /nfsdata/pv1/

[root@k8s_master pv1]# ls

houpj

[root@k8s_master ~]# kubectl apply -f nfs-pvc1.yml

persistentvolumeclaim "mypvc1" created

[root@k8s_master ~]# kubectl get pod -o wide

NAME                            READY     STATUS             RESTARTS   AGE       IP                NODE

mypod1                          1/1       Running            0          23m       172.8.18.4        192.168.138.161

[root@k8s_master ~]# kubectl exec mypod1 touch /mydata/hello

[root@k8s_master ~]# ls /nfsdata/pv1/

hello

[root@k8s_master ~]# kubectl delete pvc mypvc1

persistentvolumeclaim "mypvc1" deleted

[root@k8s_master ~]# kubectl get pv

NAME      CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS      CLAIM            REASON    AGE

mypv1     1Gi        RWO           Retain          Released    default/mypvc1             6m

nfs       1Mi        RWX           Retain          Available                              41m

[root@k8s_master ~]# kubectl get pod -o wide

NAME                            READY     STATUS             RESTARTS   AGE       IP                NODE

mypod1                          1/1       Running            0          25m       172.8.18.4        192.168.138.161

[root@k8s_master ~]# ls /nfsdata/pv1

Hello

虽然 mypv1 中的数据得到了保留,但其 PV 状态会一直处于 Released,不能被其他 PVC 申请。为了重新使用存储资源,可以删除并重新创建 mypv1。删除操作只是删除了 PV 对象,存储空间中的数据并不会被删除。

[root@k8s_master ~]# kubectl get pv

NAME      CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS      CLAIM            REASON    AGE

mypv1     1Gi        RWO           Retain          Released    default/mypvc1             12m

nfs       1Mi        RWX           Retain          Available                              47m

[root@k8s_master ~]# kubectl delete pv mypv1

persistentvolume "mypv1" deleted

[root@k8s_master ~]# kubectl apply -f nfs-pv1.yml

persistentvolume "mypv1" created

[root@k8s_master ~]# kubectl get pv

NAME      CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS      CLAIM     REASON    AGE

mypv1     1Gi        RWO           Retain          Available                       5s

nfs       1Mi        RWX           Retain          Available                       48m

 

新建的 mypv1 状态为 Available,已经可以被 PVC 申请。

PV 还支持 Delete 的回收策略,会删除 PV 在 Storage Provider 上对应存储空间。NFS 的 PV 不支持 Delete,支持 Delete 的 Provider 有 AWS EBS、GCE PD、Azure Disk、OpenStack Cinder Volume 等

Pv 动态供给没有条件实验:https://kubernetes.io/docs/concepts/storage/storage-classes/#provisioner

问题:

一般实际中容器化mysql是采用deployment还是采用statefulset?另外容器化数据库会采用哪种类型的存储多一点?

如果是单个mysql实例,用deployment就可以了。如果是集群,可以用statefulset。statefulset和deploymenty都能用volume,区别主要在于statefulset中的pod都有明确的命名规则,以及启动顺序。存储的类型不一定,开源ceph较多。

 

[root@k8s_master ~]# cat mysql-pv.yml

apiVersion: v1

kind: PersistentVolume

metadata:

  name: mysql-pv

spec:

  capacity:

    storage: 1Gi

  accessModes:

    - ReadWriteOnce

  persistentVolumeReclaimPolicy: Retain

#  storageClassName: nfs  #不支持

  nfs:

    path: /nfsdata/mysql-pv

    server: 192.168.138.130

[root@k8s_master ~]# cat mysql-pvc.yml

kind: PersistentVolumeClaim

apiVersion: v1

metadata:

  name: mysql-pvc

spec:

  accessModes:

    - ReadWriteOnce

  resources:

    requests:

      storage: 1Gi

[root@k8s_master ~]# kubectl apply -f mysql-pv.yml

persistentvolume "mysql-pv" created

[root@k8s_master ~]# kubectl apply -f mysql-pvc.yml

persistentvolumeclaim "mysql-pvc" created

[root@k8s_master ~]# kubectl get pv,pvc

NAME          CAPACITY   ACCESSMODES   RECLAIMPOLICY   STATUS      CLAIM               REASON    AGE

pv/mypv1      1Gi        RWO           Retain          Bound       default/mysql-pvc             21m

pv/mysql-pv   1Gi        RWO           Retain          Available                                 45s

pv/nfs        1Mi        RWX           Retain          Available                                 1h

 

NAME            STATUS    VOLUME    CAPACITY   ACCESSMODES   AGE

pvc/mysql-pvc   Bound     mypv1     1Gi        RWO           24s

 

http://blog.51cto.com/goome/2134294?source=dra 格式很重要

[root@master ~]# cat mysql-pv.yml

apiVersion: v1

kind: PersistentVolume

metadata:

  name: mysql-pv

spec:

  capacity:

    storage: 1Gi

  accessModes:

    - ReadWriteOnce

  persistentVolumeReclaimPolicy: Retain

  storageClassName: nfs 

  nfs:

    path: /nfsdata/mysql-pv

    server: 192.168.138.166

[root@master ~]# cat mysql-pvc.yml

kind: PersistentVolumeClaim

apiVersion: v1

metadata:

  name: mysql-pvc

spec:

  accessModes:

    - ReadWriteOnce

  resources:

    requests:

      storage: 1Gi

  storageClassName: nfs

[root@master ~]# cat mysql2.yml

apiVersion: v1

kind: Service

metadata:

  name: mysql

spec:

  ports:

  - port: 3306

  selector:

    app: mysql

 

---

apiVersion: apps/v1beta1

kind: Deployment

metadata:

  name: mysql

spec:

  selector:

    matchLabels:

      app: mysql

  template:

      metadata:

        labels:

          app: mysql

      spec:

        containers:

        - image: mysql:5.6

          name: mysql

          env:

          - name: MYSQL_ROOT_PASSWORD

            value: password

          ports:

          - containerPort: 3306

            name: mysql

          volumeMounts:

          - name: mysql-data

            mountPath: /var/lib/mysql

        volumes:

          - name: mysql-data

            persistentVolumeClaim:

              claimName: mysql-pv

[root@master ~]# kubectl apply -f mysql2.yml

service "mysql" created

deployment "mysql" created

一直报错

[root@master ~]# kubectl get pod -o wide

NAME                     READY     STATUS              RESTARTS   AGE       IP        NODE

mysql-774cffbbcd-vw96h   0/1       ContainerCreating   0          11m       <none>    minion1

[root@master ~]# kubectl describe pod mysql-6b5cb74bfc-rbczg

Name:           mysql-6b5cb74bfc-rbczg

Namespace:      default

Node:           minion1/192.168.138.130

Start Time:     Tue, 18 Sep 2018 19:55:10 +0800

Labels:         app=mysql

mount: wrong fs type, bad option, bad superblock on 192.168.138.166:/nfsdata/k8s/mysqlpv1,

       missing codepage or helper program, or other error

       (for several filesystems (e.g. nfs, cifs) you might

       need a /sbin/mount.<type> helper program)

缺少nfs客户端

[root@minion1 ~]# yum install nfs-utils

[root@master ~]# kubectl delete -f mysql-svc.yml

service "mysql" deleted

deployment "mysql" deleted

[root@master ~]# kubectl apply -f mysql-svc.yml

service "mysql" created

deployment "mysql" created

[root@master ~]# kubectl get pod -o wide

NAME                     READY     STATUS    RESTARTS   AGE       IP           NODE

mysql-6b5cb74bfc-mpxs5   1/1       Running   0          8s        10.244.1.3   minion1

[root@minion1 ~]# docker ps

CONTAINER ID        IMAGE                                                                           COMMAND                  CREATED             STATUS              PORTS               NAMES

5a2a68004c33        mysql@sha256:4c44f46efaff3ebe7cdc7b35a616c77aa003dc5de4b26c80d0ccae1f9db4a372   "docker-entrypoint..."   About an hour ago   Up About an hour                        k8s_mysql_mysql-6b5cb74bfc-mpxs5_default_73394f6f-bb3a-11e8-8d88-000c29f19fb4_0

[root@minion1 ~]# docker inspect 5a2a68004c33

{

                "Type": "bind",

                "Source": "/var/lib/kubelet/pods/73394f6f-bb3a-11e8-8d88-000c29f19fb4/volumes/kubernetes.io~nfs/mysqlpv1",

                "Destination": "/var/lib/mysql",

                "Mode": "",

                "RW": true,

                "Propagation": "rprivate"

            }

[root@minion1 ~]# docker exec -it 5a2a /bin/bash

root@mysql-6b5cb74bfc-mpxs5:/#  mysql -uroot -ppassword

mysql> create database t1;

Query OK, 1 row affected (0.03 sec)

 

mysql> use t1;create table t1(i int);

Database changed

Query OK, 0 rows affected (0.06 sec)

 

mysql> use t1;insert into t1 values (1),(3),(3);

Database changed

Query OK, 3 rows affected (0.03 sec)

Records: 3  Duplicates: 0  Warnings: 0

 

mysql> use t1;select * from t1;

Database changed

+------+

| i    |

+------+

|    1 |

|    3 |

|    3 |

+------+

3 rows in set (0.00 sec)

[root@master ~]# ls /nfsdata/k8s/mysqlpv1/

auto.cnf  ibdata1  ib_logfile0  ib_logfile1  mysql  performance_schema  t1

模拟故障

[root@minion1 ~]# init 0

很长时间仍然显示正常,节点不切换;

[root@master ~]# kubectl get pod

NAME                     READY     STATUS    RESTARTS   AGE

mysql-6b5cb74bfc-mpxs5   1/1       Running   0          1h

https://www.sohu.com/a/127037247_515888

[root@master ~]# kubectl get nodes

NAME      STATUS     ROLES     AGE       VERSION

master    Ready      master    16h       v1.9.0

minion1   NotReady   <none>    16h       v1.9.0

[root@master ~]# kubectl get pod

NAME                     READY     STATUS    RESTARTS   AGE

mysql-6b5cb74bfc-mpxs5   1/1       Running   0          14h

[root@master ~]# kubectl get pod -o wide

NAME                     READY     STATUS    RESTARTS   AGE       IP           NODE

mysql-6b5cb74bfc-mpxs5   1/1       Running   0          14h       10.244.1.3   minion1

启动宕机节点任然应用运行

[root@minion1 ~]# docker ps

CONTAINER ID        IMAGE                                                                           COMMAND                  CREATED             STATUS              PORTS               NAMES

6f27926b39f3        mysql@sha256:4c44f46efaff3ebe7cdc7b35a616c77aa003dc5de4b26c80d0ccae1f9db4a372   "docker-entrypoint..."   38 seconds ago      Up 37 seconds                           k8s_mysql_mysql-6b5cb74bfc-mpxs5_default_73394f6f-bb3a-11e8-8d88-000c29f19fb4_1

数据仍然在

4.13 Secret

应用启动过程中可能需要一些敏感信息,比如访问数据库的用户名密码或者秘钥。将这些信息直接保存在容器镜像中显然不妥,Kubernetes 提供的解决方案是 Secret。

Secret 会以密文的方式存储数据,避免了直接在配置文件中保存敏感信息。Secret 会以 Volume 的形式被 mount 到 Pod,容器可通过文件的方式使用 Secret 中的敏感数据;此外,容器也可以环境变量的方式使用这些数据。

[root@master ~]# echo -n admin|base64

YWRtaW4=

[root@master ~]# echo -n 123456 |base64

MTIzNDU2

[root@master ~]# kubectl apply -f secret.yml

secret "mysecret" created

[root@master ~]# cat secret.yml

apiVersion: v1

kind: Secret

metadata:

  name: mysecret

data:

  username: YWRtaW4=

  password: MTIzNDU2

[root@master ~]# kubectl get secret

NAME                  TYPE                                  DATA      AGE

default-token-ps7cs   kubernetes.io/service-account-token   3         19h

mysecret              Opaque                                2         1h

[root@master ~]# kubectl get secret mysecret

NAME       TYPE      DATA      AGE

mysecret   Opaque    2         1h

[root@master ~]# kubectl describe secret mysecret

Name:         mysecret

Namespace:    default

Labels:       <none>

Annotations: 

Type:         Opaque

 

Data

====

password:  6 bytes

username:  5 bytes

[root@master ~]# kubectl edit secret mysecret

apiVersion: v1

data:

  password: MTIzNDU2

  username: YWRtaW4=

kind: Secret

metadata:

  annotations:

    kubectl.kubernetes.io/last-applied-configuration: |

      {"apiVersion":"v1","data":{"password":"MTIzNDU2","username":"YWRtaW4="},"kind":"Secret","metadata":{"annotations":{},"name":"mysecret","namespace":"default"}}

  creationTimestamp: 2018-09-19T03:53:12Z

  name: mysecret

  namespace: default

  resourceVersion: "70131"

  selfLink: /api/v1/namespaces/default/secrets/mysecret

  uid: 87a7dda2-bbbf-11e8-8d88-000c29f19fb4

type: Opaque

[root@master ~]# echo -n YWRtaW4=| base64 --decode

admin[root@master ~]#

[root@master ~]# echo -n MTIzNDU2|base64 --decode

123456[root@master ~]#

[root@master ~]# cat mypod.yml

apiVersion: v1

kind: Pod

metadata:

  name: mypod

spec:

  containers:

  - name: mypod

    image: busybox

    args:

      - /bin/sh

      - -c

      - sleep 10; touch /tmp/healthy; sleep 30000

    volumeMounts:

    - name: foo

      mountPath: "/etc/foo"

      readOnly: true

  volumes:

  - name: foo

    secret:

      secretName: mysecret

[root@master ~]# kubectl apply -f mypod.yml

pod "mypod" created

[root@master ~]# kubectl exec -it mypod sh

/ # ls /etc/foo

password  username

/ # cat /etc/foo/username

admin/ #

/ # cat /etc/foo/password

123456/ # [root@master ~]#

[root@master ~]# cat secret.yml

apiVersion: v1

kind: Secret

metadata:

  name: mysecret

data:

  username: YWRtaW4=

  password: YWJjZGVm

[root@master ~]# kubectl apply -f secret.yml

secret "mysecret" configured

[root@master ~]# kubectl exec -it mypod sh

/ # ls /etc/foo

password  username

/ # cat /etc/foo/username

admin/ #

/ # cat /etc/foo/password

abcdef/ # [root@master ~]#

[root@master ~]# kubectl apply -f mypod.yml

pod "mypod" created

[root@master ~]# cat mypod.yml

apiVersion: v1

kind: Pod

metadata:

  name: mypod

spec:

  containers:

  - name: mypod

    image: busybox

    args:

      - /bin/sh

      - -c

      - sleep 10; touch /tmp/healthy; sleep 30000

    volumeMounts:

    - name: foo

      mountPath: "/etc/foo"

      readOnly: true

  volumes:

  - name: foo

    secret:

      secretName: mysecret

      items:

      - key: username

        path: my-group/my-username

      - key: password

        path: my-group/my-password

[root@master ~]# kubectl exec -it mypod sh

/ #

/ # cd /etc/foo

/etc/foo # ls

my-group

/etc/foo # cd my-group/

/etc/foo/my-group # ls

my-password  my-username

/etc/foo/my-group # cat my-password

abcdef/etc/foo/my-group #

/etc/foo/my-group # cat my-username

admin/etc/foo/my-group #

[root@master ~]# kubectl apply -f mypod.yml

pod "mypod" created

[root@master ~]# cat mypod.yml

apiVersion: v1

kind: Pod

metadata:

  name: mypod

spec:

  containers:

  - name: mypod

    image: busybox

    args:

      - /bin/sh

      - -c

      - sleep 10; touch /tmp/healthy; sleep 30000

    env:

      - name: SECRET_USERNAME

        valueFrom:

          secretKeyRef:

            name: mysecret

            key: username

      - name: SECRET_PASSWORD

        valueFrom:

          secretKeyRef:

            name: mysecret

            key: password

[root@master ~]# kubectl exec -it mypod sh

/ # echo $SECRET_USERNAME

admin

/ # echo $SECRET_PASSWORD

Abcdef

通过环境变量 SECRET_USERNAME 和 SECRET_PASSWORD 成功读取到 Secret 的数据。

需要注意的是,环境变量读取 Secret 很方便,但无法支撑 Secret 动态更新。

4.13.1 ConfigMap

Secret 可以为 Pod 提供密码、Token、私钥等敏感数据;对于一些非敏感数据,比如应用的配置信息,则可以用 ConfigMap。

ConfigMap 的创建和使用方式与 Secret 非常类似,主要的不同是数据以明文的形式存放。

[root@master ~]# kubectl apply -fconfigmap.yml

configmap "myconfigmap" created

[root@master ~]# kubectl apply -f mypod2.yml

pod "mypod" created

[root@master ~]# kubectl get configmap myconfigmap

NAME          DATA      AGE

myconfigmap   2         3m

[root@master ~]# kubectl describe configmap myconfigmap

Name:         myconfigmap

Namespace:    default

Labels:       <none>

Annotations:  kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"v1","data":{"config1":"xxx","config2":"yyy"},"kind":"ConfigMap","metadata":{"annotations":{},"name":"myconfigmap","namespace":"default"}...

 

Data

====

config1:

----

xxx

config2:

----

yyy

Events:  <none>

[root@master ~]# cat configmap.yml

apiVersion: v1

kind: ConfigMap

metadata:

  name: myconfigmap

data:

  config1: xxx

  config2: yyy

[root@master ~]# kubectl apply -f configmap2.yml

configmap "myconfigmap" created

[root@master ~]# cat configmap2.yml

apiVersion: v1

kind: ConfigMap

metadata:

  name: myconfigmap

data:

  logging.conf: |

    class: logging.handlers.RotatingFileHandler

    formatter: precise

    level: INFO

    filename: %hostname-%timestamp.log

[root@master ~]# kubectl get configmap myconfigmap

NAME          DATA      AGE

myconfigmap   1         30s

[root@master ~]# kubectl describe configmap myconfigmap

Name:         myconfigmap

Namespace:    default

Labels:       <none>

Annotations:  kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"v1","data":{"logging.conf":"class: logging.handlers.RotatingFileHandler\nformatter: precise\nlevel: INFO\nfilename: %hostname-%timestamp...

 

Data

====

logging.conf:

----

class: logging.handlers.RotatingFileHandler

formatter: precise

level: INFO

filename: %hostname-%timestamp.log

 

Events:  <none>

[root@master ~]# cat mypod4.yml

apiVersion: v1

kind: Pod

metadata:

  name: mypod

spec:

  containers:

  - name: mypod

    image: busybox

    args:

      - /bin/sh

      - -c

      - sleep 10; touch /tmp/healthy; sleep 30000

    volumeMounts:

    - name: foo

      mountPath: "/etc"

  volumes:

  - name: foo

    configMap:

      name: myconfigmap

      items:

        - key: logging.conf

          path: myapp/logging.conf

[root@master ~]# kubectl apply -f mypod4.yml

pod "mypod" created

[root@master ~]# kubectl get pods

NAME                     READY     STATUS    RESTARTS   AGE

mypod                    1/1       Running   0          15s

mysql-6b5cb74bfc-mpxs5   1/1       Running   1          18h

[root@master ~]# kubectl exec -it mypod sh

/ # cat /etc/myapp/logging.conf

class: logging.handlers.RotatingFileHandler

formatter: precise

level: INFO

filename: %hostname-%timestamp.log

/ #

第5章 Helm

5.1 概念

每个成功的软件平台都有一个优秀的打包系统,比如 Debian、Ubuntu 的 apt,Redhat、Centos 的 yum。而 Helm 则是 Kubernetes 上的包管理器。

本章我们将讨论为什么需要 Helm,它的架构和组件,以及如何使用 Helm。

Why Helm

Helm 到底解决了什么问题?为什么 Kubernetes 需要 Helm?

答案是:Kubernetes 能够很好地组织和编排容器,但它缺少一个更高层次的应用打包工具,而 Helm 就是来干这件事的。

先来看个例子。

比如对于一个 MySQL 服务, Kubernetes 需要部署下面这些对象:

 

我们可以将上面这些配置保存到对象各自的文件中,或者集中写进一个配置文件,然后通过 kubectl apply -f 部署。

到目前为止,Kubernetes 对服务的部署支持得都挺好,如果应用只由一个或几个这样的服务组成,上面的部署方式完全足够了。

但是,如果我们开发的是微服务架构的应用,组成应用的服务可能多达十个甚至几十上百个,这种组织和管理应用的方式就不好使了:

很难管理、编辑和维护如此多的服务。每个服务都有若干配置,缺乏一个更高层次的工具将这些配置组织起来。

不容易将这些服务作为一个整体统一发布。部署人员需要首先理解应用都包含哪些服务,然后按照逻辑顺序依次执行 kubectl apply。即缺少一种工具来定义应用与服务,以及服务与服务之间的依赖关系。

不能高效地共享和重用服务。比如两个应用都要用到 MySQL 服务,但配置的参数不一样,这两个应用只能分别拷贝一套标准的 MySQL 配置文件,修改后通过 kubectl apply 部署。也就是说不支持参数化配置和多环境部署。

不支持应用级别的版本管理。虽然可以通过 kubectl rollout undo 进行回滚,但这只能针对单个 Deployment,不支持整个应用的回滚。

不支持对部署的应用状态进行验证。比如是否能通过预定义的账号访问 MySQL。虽然 Kubernetes 有健康检查,但那是针对单个容器,我们需要应用(服务)级别的健康检查。

Helm 能够解决上面这些问题,Helm 帮助 Kubernetes 成为微服务架构应用理想的部署平台。Helm 有两个重要的概念:chart 和 release。

chart 是创建一个应用的信息集合,包括各种 Kubernetes 对象的配置模板、参数定义、依赖关系、文档说明等。chart 是应用部署的自包含逻辑单元。可以将 chart 想象成 apt、yum 中的软件安装包。

release 是 chart 的运行实例,代表了一个正在运行的应用。当 chart 被安装到 Kubernetes 集群,就生成一个 release。chart 能够多次安装到同一个集群,每次安装都是一个 release。

Helm 是包管理工具,这里的包就是指的 chart。Helm 能够:

从零创建新 chart。

与存储 chart 的仓库交互,拉取、保存和更新 chart。

在 Kubernetes 集群中安装和卸载 release。

更新、回滚和测试 release。

Helm 包含两个组件:Helm 客户端 和 Tiller 服务器。

 

 

Helm 客户端是终端用户使用的命令行工具,用户可以:

 

在本地开发 chart。

管理 chart 仓库。

与 Tiller 服务器交互。

在远程 Kubernetes 集群上安装 chart。

查看 release 信息。

升级或卸载已有的 release。

Tiller 服务器运行在 Kubernetes 集群中,它会处理 Helm 客户端的请求,与 Kubernetes API Server 交互。Tiller 服务器负责:

 

监听来自 Helm 客户端的请求。

通过 chart 构建 release。

在 Kubernetes 中安装 chart,并跟踪 release 的状态。

通过 API Server 升级或卸载已有的 release。

简单的讲:Helm 客户端负责管理 chart;Tiller 服务器负责管理 release。

5.2 部署

https://yq.aliyun.com/articles/159601 tiller 安装

[root@master ~]# tar xf helm-v2.11.0-linux-amd64.tar.gz -C /tmp/helm-installer/helm/

[root@master linux-amd64]# ls

helm  LICENSE  README.md  tiller

[root@master linux-amd64]# cp helm /usr/local/bin/

[root@master ~]# helm version

Client: &version.Version{SemVer:"v2.11.0", GitCommit:"2e55dbe1fdb5fdb96b75ff144a339489417b146b", GitTreeState:"clean"}

Error: could not find tiller

 

helm init --upgrade -i registry.cn-hangzhou.aliyuncs.com/google_containers/tiller:v2.5.1 --stable-repo-url https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts

[root@master ~]# helm version

Client: &version.Version{SemVer:"v2.11.0", GitCommit:"2e55dbe1fdb5fdb96b75ff144a339489417b146b", GitTreeState:"clean"}

Server: &version.Version{SemVer:"v2.5.1", GitCommit:"7cf31e8d9a026287041bae077b09165be247ae66", GitTreeState:"clean"}

[root@master ~]# kubectl get --namespace=kube-system svc tiller-deploy

NAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)     AGE

tiller-deploy   ClusterIP   10.108.109.87   <none>        44134/TCP   7m

[root@master ~]# kubectl get --namespace=kube-system deployment tiller-deploy

NAME            DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE

tiller-deploy   1         1         1            1           8m

 [root@master ~]# kubectl get --namespace=kube-system pod

tiller-deploy-86558fdd5b-4bnd9   1/1       Running   0          11m

 

Helm 安装成功后,可执行 helm search 查看当前可安装的 chart。

[root@master ~]# helm search|head -10

NAME                            CHART VERSION   APP VERSION     DESCRIPTION                                                

stable/acs-engine-autoscaler    2.1.3           2.1.1           Scales worker nodes within agent pools                     

stable/aerospike                0.1.7           v3.14.1.2       A Helm chart for Aerospike in Kubernetes                   

stable/anchore-engine           0.1.3           0.1.6           Anchore container analysis and policy evaluation engine s...

stable/artifactory              7.0.3           5.8.4           Universal Repository Manager supporting all major packagi...

stable/artifactory-ha           0.1.0           5.8.4           Universal Repository Manager supporting all major packagi...

stable/aws-cluster-autoscaler   0.3.2                           Scales worker nodes within autoscaling groups.             

stable/bitcoind                 0.1.0           0.15.1          Bitcoin is an innovative payment network and a new kind o...

stable/buildkite                0.2.1           3               Agent for Buildkite                                        

stable/centrifugo               2.0.0           1.7.3           Centrifugo is a real-time messaging server.     

前面说过,Helm 可以像 apt 和 yum 管理软件包一样管理 chart。apt 和 yum 的软件包存放在仓库中,同样的,Helm 也有仓库。

[root@master ~]# helm repo list

NAME    URL                                                  

stable  https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts

local   http://127.0.0.1:8879/charts 

前面说过,Helm 可以像 apt 和 yum 管理软件包一样管理 chart。apt 和 yum 的软件包存放在仓库中,同样的,Helm 也有仓库。

Helm 安装时已经默认配置好了两个仓库:stable 和 local。stable 是官方仓库,local 是用户存放自己开发的 chart 的本地仓库。

helm search 会显示 chart 位于哪个仓库,比如 local/cool-chart 和 stable/acs-engine-autoscaler。

用户可以通过 helm repo add 添加更多的仓库,比如企业的私有仓库,仓库的管理和维护方法请参考官网文档 https://docs.helm.sh

与 apt 和 yum 一样,helm 也支持关键字搜索:

[root@master ~]# helm search mysql

NAME                            CHART VERSION   APP VERSION DESCRIPTION                                                

stable/mysql                    0.3.5                       Fast, reliable, scalable, and easy to use open-source rel...

stable/percona                  0.3.0                       free, fully compatible, enhanced, open source drop-in rep...

stable/percona-xtradb-cluster   0.0.2           5.7.19     free, fully compatible, enhanced, open source drop-in rep...

stable/gcloud-sqlproxy          0.2.3                       Google Cloud SQL Proxy                                     

stable/mariadb                  2.1.6           10.1.31    Fast, reliable, scalable, and easy to use open-source rel..

包括 DESCRIPTION 在内的所有信息,只要跟关键字匹配,都会显示在结果列表中。

安装 chart 也很简单,执行如下命令可以安装 MySQL。

helm install stable/mysql

没有权限报错

[root@master ~]#  helm install stable/mysql

Error: no available release name found

开启权限

[root@master ~]# kubectl create serviceaccount --namespace kube-system tiller

serviceaccount "tiller" created

[root@master ~]# kubectl create clusterrolebinding tiller-cluster-rule --clusterrole=cluster-admin --serviceaccount=kube-system:tiller

clusterrolebinding "tiller-cluster-rule" created

[root@master ~]# kubectl patch deploy --namespace kube-system tiller-deploy -p '{"spec":{"template":{"spec":{"serviceAccount":"tiller"}}}}'

deployment "tiller-deploy" patched

[root@master ~]# helm install stable/mysql

NAME:   wrapping-hog

LAST DEPLOYED: Fri Sep 21 00:29:02 2018

NAMESPACE: default

STATUS: DEPLOYED

 

RESOURCES:

==> v1/Secret

NAME                TYPE    DATA  AGE

wrapping-hog-mysql  Opaque  2     1d

 

==> v1/PersistentVolumeClaim

NAME                STATUS   VOLUME  CAPACITY  ACCESSMODES  STORAGECLASS  AGE

wrapping-hog-mysql  Pending  1d

 

==> v1/Service

NAME                CLUSTER-IP     EXTERNAL-IP  PORT(S)   AGE

wrapping-hog-mysql  10.104.115.37  <none>       3306/TCP  1d

 

==> v1beta1/Deployment

NAME                DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE

wrapping-hog-mysql  1        1        1           0          1d

 

 

NOTES:

MySQL can be accessed via port 3306 on the following DNS name from within your cluster:

wrapping-hog-mysql.default.svc.cluster.local

 

To get your root password run:

 

    MYSQL_ROOT_PASSWORD=$(kubectl get secret --namespace default wrapping-hog-mysql -o jsonpath="{.data.mysql-root-password}" | base64 --decode; echo)

 

To connect to your database:

 

1. Run an Ubuntu pod that you can use as a client:

 

    kubectl run -i --tty ubuntu --image=ubuntu:16.04 --restart=Never -- bash -il

 

2. Install the mysql client:

 

    $ apt-get update && apt-get install mysql-client -y

 

3. Connect using the mysql cli, then provide your password:

    $ mysql -h wrapping-hog-mysql -p

 

To connect to your database directly from outside the K8s cluster:

    MYSQL_HOST=127.0.0.1

    MYSQL_PORT=3306

 

    # Execute the following commands to route the connection:

    export POD_NAME=$(kubectl get pods --namespace default -l "app=wrapping-hog-mysql" -o jsonpath="{.items[0].metadata.name}")

    kubectl port-forward $POD_NAME 3306:3306

 

    mysql -h ${MYSQL_HOST} -P${MYSQL_PORT} -u root -p${MYSQL_ROOT_PASSWORD}

绿色部分

chart 本次部署的描述信息:

NAME 是 release 的名字,因为我们没用 -n 参数指定,Helm 随机生成了一个,这里是 wrapping-hog。

NAMESPACE 是 release 部署的 namespace,默认是 default,也可以通过 --namespace 指定。

STATUS 为 DEPLOYED,表示已经将 chart 部署到集群

当前 release 包含的资源:Service、Deployment、Secret 和 PersistentVolumeClaim,其名字都是 wrapping-hog-mysql,命名的格式为 ReleasName-ChartName。

NOTES 部分显示的是 release 的使用方法。比如如何访问 Service,如何获取数据库密码,以及如何连接数据库等。

[root@master ~]# kubectl get service

NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE

kubernetes           ClusterIP   10.96.0.1       <none>        443/TCP    23h

mysql                ClusterIP   10.108.20.158   <none>        3306/TCP   21h

wrapping-hog-mysql   ClusterIP   10.104.115.37   <none>        3306/TCP   8m

[root@master ~]# kubectl get deployment

NAME                 DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE

mysql                1         1         1            1           21h

wrapping-hog-mysql   1         1         1            0           8m

[root@master ~]# kubectl get pod

NAME                                  READY     STATUS     RESTARTS   AGE

mypod                                 1/1       Running    0          3h

mysql-6b5cb74bfc-mpxs5                1/1       Running    1          21h

wrapping-hog-mysql-697f95c9dd-b2t2s   0/1       Init:0/1   0          9m

[root@master ~]# kubectl get pvc

NAME                 STATUS    VOLUME     CAPACITY   ACCESS MODES   STORAGECLASS   AGE

mysqlpvc1            Bound     mysqlpv1   1Gi        RWO            nfs1           21h

wrapping-hog-mysql   Pending  

[root@master ~]# helm list

NAME            REVISION    UPDATED                     STATUS      CHART          NAMESPACE

wrapping-hog    1           Fri Sep 21 00:29:02 2018    DEPLOYED    mysql-0.3.5    default 

[root@master ~]# helm delete wrapping-hog

release "wrapping-hog" deleted                                                    9m

chart 是 Helm 的应用打包格式。chart 由一系列文件组成,这些文件描述了 Kubernetes 部署应用时所需要的资源,比如 Service、Deployment、PersistentVolumeClaim、Secret、ConfigMap 等。

单个的 chart 可以非常简单,只用于部署一个服务,比如 Memcached;chart 也可以很复杂,部署整个应用,比如包含 HTTP Servers、 Database、消息中间件、cache 等。

chart 将这些文件放置在预定义的目录结构中,通常整个 chart 被打成 tar 包,而且标注上版本信息,便于 Helm 部署。

[root@master ~]# ls .helm/cache/archive/

mysql-0.3.5.tgz

[root@master ~]# tree mysql

mysql

├── Chart.yaml

├── README.md

├── templates

│   ├── configmap.yaml

│   ├── deployment.yaml

│   ├── _helpers.tpl

│   ├── NOTES.txt

│   ├── pvc.yaml

│   ├── secrets.yaml

│   └── svc.yaml

└── values.yaml

 

1 directory, 10 files

Chart.yaml

YAML 文件,描述 chart 的概要信息。

name 和 version 是必填项,其他都是可选。

README.md

Markdown 格式的 README 文件,相当于 chart 的使用文档,此文件为可选。

LICENSE

文本文件,描述 chart 的许可信息,此文件为可选。

requirements.yaml

chart 可能依赖其他的 chart,这些依赖关系可通过 requirements.yaml 指定,比如:

在安装过程中,依赖的 chart 也会被一起安装。

values.yaml

chart 支持在安装的时根据参数进行定制化配置,而 values.yaml 则提供了这些配置参数的默认值。

templates 目录

各类 Kubernetes 资源的配置模板都放置在这里。Helm 会将 values.yaml 中的参数值注入到模板中生成标准的 YAML 配置文件。

模板是 chart 最重要的部分,也是 Helm 最强大的地方。模板增加了应用部署的灵活性,能够适用不同的环境,我们后面会详细讨论。

templates/NOTES.txt

chart 的简易使用文档,chart 安装成功后会显示此文档内容。

与模板一样,可以在 NOTE.txt 中插入配置参数,Helm 会动态注入参数值。

5.3 Chart模板

[root@master templates]# cat secrets.yaml

apiVersion: v1

kind: Secret

metadata:

  name: {{ template "mysql.fullname" . }}

  labels:

    app: {{ template "mysql.fullname" . }}

    chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"

    release: "{{ .Release.Name }}"

    heritage: "{{ .Release.Service }}"

type: Opaque

data:

  {{ if .Values.mysqlRootPassword }}

  mysql-root-password:  {{ .Values.mysqlRootPassword | b64enc | quote }}

  {{ else }}

  mysql-root-password: {{ randAlphaNum 10 | b64enc | quote }}

  {{ end }}

  {{ if .Values.mysqlPassword }}

  mysql-password:  {{ .Values.mysqlPassword | b64enc | quote }}

  {{ else }}

  mysql-password: {{ randAlphaNum 10 | b64enc | quote }}

  {{ end }}

从结构看,文件的内容非常像Secret 配置,只是大部分属性值变成了{{ xxx }}。这些 {{ xxx }}实际上是模板的语法。Helm 采用了Go语言的模板来编写chart。Go模板非常强大,支持变量、对象、函数、流控制等功能。下面我们通过解析templates/secrets.yaml快速学习模板。

{{ template "mysql.fullname" . }} 定义 Secret 的 name。

关键字 template 的作用是引用一个子模板 mysql.fullname。这个子模板是在 templates/_helpers.tpl 文件中定义的

[root@master templates]# cat _helpers.tpl

{{/* vim: set filetype=mustache: */}}

{{/*

Expand the name of the chart.

*/}}

{{- define "mysql.name" -}}

{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}

{{- end -}}

 

{{/*

Create a default fully qualified app name.

We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).

*/}}

{{- define "mysql.fullname" -}}

{{- $name := default .Chart.Name .Values.nameOverride -}}

{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}

{{- end -}}

这个定义还是很复杂的,因为它用到了模板语言中的对象、函数、流控制等概念。现在看不懂没关系,这里我们学习的重点是:如果存在一些信息多个模板都会用到,则可在 templates/_helpers.tpl 中将其定义为子模板,然后通过 templates 函数引用。

这里 mysql.fullname 是由 release 与 chart 二者名字拼接组成。

根据 chart 的最佳实践,所有资源的名称都应该保持一致,对于我们这个 chart,无论 Secret 还是 Deployment、PersistentVolumeClaim、Service,它们的名字都是子模板 mysql.fullname 的值。

Chart 和 Release 是 Helm 预定义的对象,每个对象都有自己的属性,可以在模板中使用。如果使用下面命令安装 chart:

 

helm install stable/mysql -n my

那么:

{{ .Chart.Name }} 的值为 mysql。

{{ .Chart.Version }} 的值为 0.3.0。

{{ .Release.Name }} 的值为 my。

{{ .Release.Service }} 始终取值为 Tiller。

{{ template "mysql.fullname" . }} 计算结果为 my-mysql。

{{ if .Values.mysqlRootPassword }}

  mysql-root-password:  {{ .Values.mysqlRootPassword | b64enc | quote }}

  {{ else }}

  mysql-root-password: {{ randAlphaNum 10 | b64enc | quote }}

  {{ end }}

  {{ if .Values.mysqlPassword }}

  mysql-password:  {{ .Values.mysqlPassword | b64enc | quote }}

  {{ else }}

  mysql-password: {{ randAlphaNum 10 | b64enc | quote }}

  {{ end }}

这里指定 mysql-root-password 的值,不过使用了 if-else 的流控制,其逻辑为:

如果 .Values.mysqlRootPassword 有值,则对其进行 base64 编码;否则随机生成一个 10 位的字符串并编码。

Values 也是预定义的对象,代表的是 values.yaml 文件。而 .Values.mysqlRootPassword 则是 values.yaml 中定义的 mysqlRootPassword 参数:

[root@master mysql]# cat values.yaml |head -20

## mysql image version

## ref: https://hub.docker.com/r/library/mysql/tags/

##

image: "mysql"

imageTag: "5.7.14"

 

## Specify password for root user

##

## Default: random 10 character string

# mysqlRootPassword: testing

 

## Create a database user

##

# mysqlUser:

# mysqlPassword:

 

## Allow unauthenticated access, uncomment to enable

##

# mysqlAllowEmptyPassword: true

因为 mysqlRootPassword 被注释掉了,没有赋值,所以逻辑判断会走 else,即随机生成密码。

randAlphaNum、b64enc、quote 都是 Go 模板语言支持的函数,函数之间可以通过管道 | 连接。{{ randAlphaNum 10 | b64enc | quote }} 的作用是首先随机产生一个长度为 10 的字符串,然后将其 base64 编码,最后两边加上双引号。

templates/secrets.yaml 这个例子展示了 chart 模板主要的功能,我们最大的收获应该是:模板将 chart 参数化了,通过 values.yaml 可以灵活定制应用

 

5.4 实践

作为准备工作,安装之前需要先清楚 chart 的使用方法。这些信息通常记录在 values.yaml 和 README.md 中。除了下载源文件查看,执行 helm inspect values 可能是更方便的方法。

[root@master ~]# helm inspect values stable/mysql

## mysql image version

## ref: https://hub.docker.com/r/library/mysql/tags/

##

image: "mysql"

imageTag: "5.7.14"

输出的实际上是 values.yaml 的内容。阅读注释就可以知道 MySQL chart 支持哪些参数,安装之前需要做哪些准备。其中有一部分是关于存储的:

## Persist data to a persistent volume

persistence:

  enabled: true

  ## database data Persistent Volume Storage Class

  ## If defined, storageClassName: <storageClass>

  ## If set to "-", storageClassName: "", which disables dynamic provisioning

  ## If undefined (the default) or set to null, no storageClassName spec is

  ##   set, choosing the default provisioner.  (gp2 on AWS, standard on

  ##   GKE, AWS & OpenStack)

  ##

  # storageClass: "-"

  accessMode: ReadWriteOnce

  size: 8Gi

chart 定义了一个 PersistentVolumeClaim,申请 8G 的 PersistentVolume。由于我们的实验环境不支持动态供给,所以得预先创建好相应的 PV,其配置文件 mysql-pv.yml 内容为:

[root@master ~]# cat mysql-pv.yml

apiVersion: v1

kind: PersistentVolume

metadata:

  name: mysql-pv

spec:

  capacity:

    storage: 8Gi

  accessModes:

    - ReadWriteOnce

  persistentVolumeReclaimPolicy: Retain

#  storageClassName: nfs1

  nfs:

    path: /nfsdata/mysql-pv

    server: 192.168.138.166

[root@master ~]# kubectl apply -f mysql-pv.yml

persistentvolume "mysql-pv" created

[root@master ~]# kubectl get pv

NAME       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM     STORAGECLASS   REASON    AGE

mysql-pv   8Gi        RWO            Retain           Available                                      15s

定制化安装 chart

除了接受 values.yaml 的默认值,我们还可以定制化 chart,比如设置 mysqlRootPassword。

 

Helm 有两种方式传递配置参数:

 

指定自己的 values 文件。

通常的做法是首先通过 helm inspect values mysql > myvalues.yaml生成 values 文件,然后设置 mysqlRootPassword,之后执行 helm install --values=myvalues.yaml mysql。

 

通过 --set 直接传入参数值,比如

[root@master ~]# helm install stable/mysql --set mysqlRootPassword=abc123 -n my

NAME:   my

LAST DEPLOYED: Fri Sep 21 03:23:31 2018

NAMESPACE: default

STATUS: DEPLOYED

 

RESOURCES:

==> v1/Secret

NAME      TYPE    DATA  AGE

my-mysql  Opaque  2     1d

 

==> v1/PersistentVolumeClaim

NAME      STATUS  VOLUME    CAPACITY  ACCESSMODES  STORAGECLASS  AGE

my-mysql  Bound   mysql-pv  8Gi       RWO          1d

 

==> v1/Service

NAME      CLUSTER-IP      EXTERNAL-IP  PORT(S)   AGE

my-mysql  10.111.229.211  <none>       3306/TCP  1d

 

==> v1beta1/Deployment

NAME      DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE

my-mysql  1        1        1           0          1d

mysqlRootPassword 设置为 abc123。另外,-n 设置 release 为 my,各类资源的名称即为my-mysql。

通过 helm list 和 helm status 可以查看 chart 的最新状态。

[root@master ~]# helm status my

LAST DEPLOYED: Fri Sep 21 03:23:31 2018

NAMESPACE: default

STATUS: DEPLOYED

 

RESOURCES:

==> v1/PersistentVolumeClaim

NAME      STATUS  VOLUME    CAPACITY  ACCESSMODES  STORAGECLASS  AGE

my-mysql  Bound   mysql-pv  8Gi       RWO          1d

 

==> v1/Service

NAME      CLUSTER-IP      EXTERNAL-IP  PORT(S)   AGE

my-mysql  10.111.229.211  <none>       3306/TCP  1d

 

==> v1beta1/Deployment

NAME      DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE

my-mysql  1        1        1           0          1d

 

==> v1/Secret

NAME      TYPE    DATA  AGE

my-mysql  Opaque  2     1d

 

升级和回滚 release

release 发布后可以执行 helm upgrade 对其升级,通过 --values 或 --set应用新的配置。比如将当前的 MySQL 版本升级到 5.7.15:

[root@master ~]# helm upgrade --set imageTag=5.7.15 my stable/mysql

Error: UPGRADE FAILED: transport is closing

[root@master ~]# kubectl get deployment my-mysql -o wide

NAME       DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE       CONTAINERS   IMAGES         SELECTOR

my-mysql   1         1         1            0           11m       my-mysql     mysql:5.7.14   app=my-mysql

[root@master ~]# helm history my

REVISION    UPDATED                     STATUS      CHART      DESCRIPTION    

1           Fri Sep 21 03:23:31 2018    DEPLOYED    mysql-0.3.5 Install complete

[root@master ~]# helm rollback my 1  #回滚

Kubernetes 给我们提供了大量官方 chart,不过要部署微服务应用,还是需要开发自己的 chart,下面就来实践这个主题。

[root@master ~]# helm create mychart

Creating mychart

[root@master ~]# tree mychart/

mychart/

├── charts

├── Chart.yaml

├── templates

│   ├── deployment.yaml

│   ├── _helpers.tpl

│   ├── ingress.yaml

│   ├── NOTES.txt

│   └── service.yaml

└── values.yaml

 

2 directories, 7 files

开发时建议大家参考官方chart中的模板、values.yaml、Chart.yaml,里面包含了大量最佳实践和最常用的函数、流控制

调试 chart

只要是程序就会有 bug,chart 也不例外。Helm 提供了 debug 的工具:helm lint 和 helm install --dry-run --debug。

helm lint 会检测 chart 的语法,报告错误以及给出建议。

[root@master ~]# helm lint mychart

==> Linting mychart

[INFO] Chart.yaml: icon is recommended

 

1 chart(s) linted, no failures

故意改错

[root@master mychart]# vim values.yaml

 

# Default values for mychart.

# This is a YAML-formatted file.

# Declare variables to be passed into your templates.

replicaCount: 1

image:

  repository: nginx

  tag: stable

  pullPolicy IfNotPresent

[root@master ~]# helm lint mychart

==> Linting mychart

[INFO] Chart.yaml: icon is recommended

[ERROR] values.yaml: unable to parse YAML

    error converting YAML to JSON: yaml: line 8: could not find expected ':'

 

Error: 1 chart(s) linted, 1 chart(s) failed

helm install --dry-run --debug 会模拟安装 chart,并输出每个模板生成的YAML内容。

[root@master ~]# helm install --dry-run mychart --debug

[debug] Created tunnel using local port: '35442'

 

[debug] SERVER: "localhost:35442"

 

[debug] Original chart version: ""

[debug] CHART PATH: /root/mychart

 

NAME:   eerie-chinchilla

REVISION: 1

RELEASED: Thu Sep 27 19:57:52 2018

CHART: mychart-0.1.0

USER-SUPPLIED VALUES:

{}

 

COMPUTED VALUES:

image:

  pullPolicy: IfNotPresent

  repository: nginx

  tag: stable

ingress:

  annotations: null

  enabled: false

  hosts:

  - chart-example.local

  tls: null

replicaCount: 1

resources: {}

service:

  externalPort: 80

  internalPort: 80

  name: nginx

  type: ClusterIP

 

HOOKS:

MANIFEST:

 

---

# Source: mychart/templates/service.yaml

apiVersion: v1

kind: Service

metadata:

  name: eerie-chinchilla-mychart

  labels:

    app: mychart

    chart: mychart-0.1.0

    release: eerie-chinchilla

    heritage: Tiller

spec:

  type: ClusterIP

  ports:

    - port: 80

      targetPort: 80

      protocol: TCP

      name: nginx

  selector:

    app: mychart

    release: eerie-chinchilla

---

# Source: mychart/templates/deployment.yaml

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

  name: eerie-chinchilla-mychart

  labels:

    app: mychart

    chart: mychart-0.1.0

    release: eerie-chinchilla

    heritage: Tiller

spec:

  replicas: 1

  template:

    metadata:

      labels:

        app: mychart

        release: eerie-chinchilla

    spec:

      containers:

        - name: mychart

          image: "nginx:stable"

          imagePullPolicy: IfNotPresent

          ports:

            - containerPort: 80

          livenessProbe:

            httpGet:

              path: /

              port: 80

          readinessProbe:

            httpGet:

              path: /

              port: 80

          resources:

            {}

我们可以检视这些输出,判断是否与预期相符。

同样,mychart 目录作为参数传递给 helm install --dry-run --debug。

5.5 管理和安装chart

当我们觉得准备就绪,就可以安装 chart,Helm 支持四种安装方法:

安装仓库中的 chart,例如:helm install stable/nginx

通过 tar 包安装,例如:helm install ./nginx-1.2.3.tgz

通过 chart 本地目录安装,例如:helm install ./nginx

通过 URL 安装,例如:helm install https://example.com/charts/nginx-1.2.3.tgz

[root@master ~]# helm install mychart

NAME:   yodeling-opossum

LAST DEPLOYED: Thu Sep 27 20:04:31 2018

NAMESPACE: default

STATUS: DEPLOYED

 

RESOURCES:

==> v1/Service

NAME                      CLUSTER-IP     EXTERNAL-IP  PORT(S)  AGE

yodeling-opossum-mychart  10.110.28.223  <none>       80/TCP   2s

 

==> v1beta1/Deployment

NAME                      DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE

yodeling-opossum-mychart  1        0        0           0          2s

 

 

NOTES:

1. Get the application URL by running these commands:

  export POD_NAME=$(kubectl get pods --namespace default -l "app=mychart,release=yodeling-opossum" -o jsonpath="{.items[0].metadata.name}")

  echo "Visit http://127.0.0.1:8080 to use your application"

  kubectl port-forward $POD_NAME 8080:80

将 chart 添加到仓库

chart 通过测试后可以将其添加到仓库,团队其他成员就能够使用。任何 HTTP Server 都可以用作 chart 仓库,下面演示在 k8s-node1 上搭建仓库。

a在 minion1上启动一个 httpd 容器

[root@minion1 ~]# mkdir /var/www

[root@minion1 ~]# docker run -d -p 8080:80 -v /var/www/:/usr/local/apache2/htdocs/ httpd

b通过helm package将mychart打包。

[root@master ~]# helm package mychart

Successfully packaged chart and saved it to: /root/mychart-0.1.0.tgz

c执行 helm repo index生成仓库的index文件。

[root@master ~]# helm repo index myrepo/ --url http://192.168.138.130:8080/charts

[root@master ~]# ls myrepo/

index.yaml  mychart-0.1.0.tgz

Helm 会扫描 myrepo 目录中的所有 tgz 包并生成 index.yaml。--url指定的是新仓库的访问路径。新生成的 index.yaml 记录了当前仓库中所有 chart 的信息:

[root@master myrepo]# cat index.yaml

apiVersion: v1

entries:

  mychart:

  - apiVersion: v1

    created: 2018-09-27T20:14:44.409532592+08:00

    description: A Helm chart for Kubernetes

    digest: 08092d2be29635ae9a32858e15ef3abb413b768d13d331d06fde87dd07b6fa21

    name: mychart

    urls:

    - http://192.168.138.130:8080/charts/mychart-0.1.0.tgz

    version: 0.1.0

generated: 2018-09-27T20:14:44.409049277+08:00

d将 mychart-0.1.0.tgz 和 index.yaml 上传到minion1的/var/www/charts目录。

e通过 helm repo add 将新仓库添加到 Helm。

[root@master myrepo]# helm repo add newrepo http://192.168.138.130:8080/charts

"newrepo" has been added to your repositories

[root@master myrepo]# helm repo list

NAME   URL                                                  

stable https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts

local  http://127.0.0.1:8879/charts                         

newrepo http://192.168.138.130:8080/charts                   

[root@master myrepo]# helm search mychart

NAME           VERSION DESCRIPTION               

local/mychart  0.1.0  A Helm chart for Kubernetes

newrepo/mychart 0.1.0  A Helm chart for Kubernetes

仓库命名为 newrepo,Helm会从仓库下载index.yaml。

除了 newrepo/mychart,这里还有一个 local/mychart。这是因为在执行第 2 步打包操作的同时,mychart 也被同步到了 local 的仓库。

f已经可以直接从新仓库安装 mychart了

[root@master myrepo]# helm install newrepo/mychart

NAME:   crusty-newt

LAST DEPLOYED: Thu Sep 27 20:36:05 2018

NAMESPACE: default

STATUS: DEPLOYED

 

RESOURCES:

==> v1/Service

NAME                 CLUSTER-IP      EXTERNAL-IP  PORT(S)  AGE

crusty-newt-mychart  10.110.231.167  <none>       80/TCP   0s

 

==> v1beta1/Deployment

NAME                 DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE

crusty-newt-mychart  1        0        0           0          0s

如果以后仓库添加了新的chart,需要用helm repo update更新本地的index。

[root@master myrepo]# helm repo update

Hang tight while we grab the latest from your chart repositories...

...Skip local chart repository

...Successfully got an update from the "newrepo" chart repository

...Successfully got an update from the "stable" chart repository

Update Complete. ⎈ Happy Helming!⎈

Helm 让我们能够像 apt 管理 deb 包那样安装、部署、升级和删除容器化应用。

Helm 由客户端和 Tiller 服务器组成。客户端负责管理 chart,服务器负责管理 release。

chart 是 Helm 的应用打包格式,它由一组文件和目录构成。其中最重要的是模板,模板中定义了 Kubernetes 各类资源的配置信息,Helm 在部署时通过 values.yaml 实例化模板。

Helm 允许用户开发自己的 chart,并为用户提供了调试工具。用户可以搭建自己的 chart 仓库,在团队中共享 chart。

Helm 帮助用户在 Kubernetes 上高效地运行和管理微服务架构应用,Helm 非常重要。

 

 

 

第6章 网络模型

6.1 概念

Kubernetes 作为编排引擎管理着分布在不同节点上的容器和 Pod。Pod、Service、外部组件之间需要一种可靠的方式找到彼此并进行通信,Kubernetes 网络则负责提供这个保障。

 

Kubernetes 网络模型

Kubernetes 采用的是基于扁平地址空间的网络模型,集群中的每个 Pod 都有自己的 IP 地址,Pod 之间不需要配置 NAT 就能直接通信。另外,同一个 Pod 中的容器共享 Pod 的 IP,能够通过 localhost 通信。

这种网络模型对应用开发者和管理员相当友好,应用可以非常方便地从传统网络迁移到 Kubernetes。每个 Pod 可被看作是一个个独立的系统,而 Pod 中的容器则可被看做同一系统中的不同进程。

 

Pod 内容器之间的通信

当 Pod 被调度到某个节点,Pod 中的所有容器都在这个节点上运行,这些容器共享相同的本地文件系统、IPC 和网络命名空间。

不同 Pod 之间不存在端口冲突的问题,因为每个 Pod 都有自己的 IP 地址。当某个容器使用 localhost 时,意味着使用的是容器所属 Pod 的地址空间。

比如 Pod A 有两个容器 container-A1 和 container-A2,container-A1 在端口 1234 上监听,当 container-A2 连接到 localhost:1234,实际上就是在访问 container-A1。这不会与同一个节点上的 Pod B 冲突,即使 Pod B 中的容器 container-B1 也在监听 1234 端口。

 

Pod 之间的通信

Pod 的 IP 是集群可见的,即集群中的任何其他 Pod 和节点都可以通过 IP 直接与 Pod 通信,这种通信不需要借助任何的网络地址转换、隧道或代理技术。Pod 内部和外部使用的是同一个 IP,这也意味着标准的命名服务和发现机制,比如 DNS 可以直接使用。

Pod 与 Service 的通信

Pod 间可以直接通过 IP 地址通信,但前提是 Pod 得知道对方的 IP。在 Kubernetes 集群中, Pod 可能会频繁的销毁和创建,也就是说 Pod 的 IP 不是固定的。为了解决这个问题,Service 提供了访问 Pod 的抽象层。无论后端的 Pod 如何变化,Service 都作为稳定的前端对外提供服务。同时,Service 还提供了高可用和负载均衡功能,Service 负责将请求转发给正确的 Pod。

 

外部访问

无论是 Pod 的 IP 还是 Service 的 Cluster IP,它们只能在 Kubernetes 集群中可见,对集群之外的世界,这些 IP 都是私有的。

Kubernetes 提供了两种方式让外界能够与 Pod 通信:

NodePort

Service 通过 Cluster 节点的静态端口对外提供服务。外部可以通过 <NodeIP>:<NodePort> 访问 Service。

 

LoadBalancer

Service 利用 cloud provider 提供的 load balancer 对外提供服务,cloud provider 负责将 load balancer 的流量导向 Service。目前支持的 cloud provider 有 GCP、AWS、Azur 等。

 

为了保证网络方案的标准化、扩展性和灵活性,Kubernetes 采用了 Container Networking Interface(CNI)规范。

CNI 是由 CoreOS 提出的容器网络规范,它使用了插件(Plugin)模型创建容器的网络栈。

CNI 的优点是支持多种容器 runtime,不仅仅是 Docker。CNI 的插件模型支持不同组织和公司开发的第三方插件,这对运维人员来说很有吸引力,可以灵活选择适合的网络方案。

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

6.2 Network Policy

Network Policy 是 Kubernetes 的一种资源。Network Policy 通过 Label 选择 Pod,并指定其他 Pod 或外界如何与这些 Pod 通信。

默认情况下,所有 Pod 是非隔离的,即任何来源的网络流量都能够访问 Pod,没有任何限制。当为 Pod 定义了 Network Policy,只有 Policy 允许的流量才能访问 Pod。

不过,不是所有的 Kubernetes 网络方案都支持 Network Policy。比如 Flannel 就不支持,Calico 是支持的。我们接下来将用 Canal 来演示 Network Policy。Canal 这个开源项目很有意思,它用 Flannel 实现 Kubernetes 集群网络,同时又用 Calico 实现 Network Policy。

6.3 部署canal

下载好文件镜像

 

[root@master ~]# kubectl apply -f rbac.yaml

clusterrole "calico" created

clusterrole "flannel" created

clusterrolebinding "canal-flannel" created

clusterrolebinding "canal-calico" created

[root@master ~]# kubectl apply -f canal.yaml

configmap "canal-config" created

daemonset "canal" created

customresourcedefinition "globalfelixconfigs.crd.projectcalico.org" created

customresourcedefinition "globalbgpconfigs.crd.projectcalico.org" created

customresourcedefinition "ippools.crd.projectcalico.org" created

customresourcedefinition "globalnetworkpolicies.crd.projectcalico.org" created

serviceaccount "canal" created

[root@master ~]#

[root@master ~]#

[root@master ~]#

[root@master ~]# kubectl get --namespace=kube-system daemonset

NAME         DESIRED   CURRENT   READY     UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE

canal        1         1         1         1            1           <none>          3m

kube-proxy   1         1         1         1            1           <none>          24m

[root@master ~]# cat httpd.yml

apiVersion: apps/v1beta1

kind: Deployment

metadata:

  name: httpd

spec:

  replicas: 3

  template:

    metadata:

      labels:

        run: httpd

    spec:

      containers:

      - name: httpd

        image: httpd:latest

        imagePullPolicy: IfNotPresent

        ports:

        - containerPort: 80

 

---

apiVersion: v1

kind: Service

metadata:

  name: httpd-svc

spec:

  type: NodePort

  selector:

    run: httpd

  ports:

  - protocol: TCP

    nodePort: 30000

    port: 8080

targetPort: 80

[root@master ~]# kubectl apply -f httpd.yml

deployment "httpd" created

service "httpd-svc" created

[root@master ~]# kubectl get pod -o wide

NAME                     READY     STATUS                 RESTARTS   AGE       IP            NODE

httpd-78bf969db7-27b2t   0/1       CreateContainerError   0          4s        10.244.1.22   slave1

httpd-78bf969db7-4gx7m   0/1       CreateContainerError   0          4s        <none>        slave2

httpd-78bf969db7-78nxs   0/1       CreateContainerError   0          4s        <none>        slave2

httpd-78bf969db7-tgdvd   0/1       Terminating            3          6m        10.244.1.6    slave1

[root@master ~]# kubectl describe pod httpd-78bf969db7-27b2t

报错Error: Error response from daemon: mkdir /var/lib/docker/overlay/ed1f3b667e779ee685ad979f581e3bb85643c5ce25a627560af9e65c7c8d8fab-init/merged/dev/pts: cannot allocate memory 内存不够

重新分配内存

[root@master ~]# kubectl get pod -o wide

NAME                     READY     STATUS    RESTARTS   AGE       IP            NODE

httpd-78bf969db7-27b2t   1/1       Running   1          7m        10.244.1.30   slave1

httpd-78bf969db7-4gx7m   1/1       Running   0          7m        10.244.2.13   slave2

httpd-78bf969db7-78nxs   1/1       Running   1          7m        10.244.2.15   slave2

[root@master ~]# kubectl get svc httpd-svc

NAME        TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE

httpd-svc   NodePort   10.107.139.120   <none>        8080:30000/TCP   12m

[root@master ~]# curl -I 192.168.138.130:30000

HTTP/1.1 200 OK

Date: Sat, 29 Sep 2018 12:56:44 GMT

Server: Apache/2.4.35 (Unix)

Last-Modified: Mon, 11 Jun 2007 18:53:14 GMT

ETag: "2d-432a5e4a73a80"

Accept-Ranges: bytes

Content-Length: 45

Content-Type: text/html

 

[root@master ~]# kubectl run busybox --rm -it --image=busybox /bin/sh

If you don't see a command prompt, try pressing enter.

/ # ping 10.244.1.30

PING 10.244.1.30 (10.244.1.30): 56 data bytes

64 bytes from 10.244.1.30: seq=0 ttl=63 time=0.082 ms

64 bytes from 10.244.1.30: seq=1 ttl=63 time=0.075 ms

64 bytes from 10.244.1.30: seq=2 ttl=63 time=0.054 ms

[root@slave1 ~]# curl -I 10.107.139.120:8080

HTTP/1.1 200 OK

Date: Sat, 29 Sep 2018 12:59:34 GMT

Server: Apache/2.4.35 (Unix)

Last-Modified: Mon, 11 Jun 2007 18:53:14 GMT

ETag: "2d-432a5e4a73a80"

Accept-Ranges: bytes

Content-Length: 45

Content-Type: text/html

 

[root@slave1 ~]# ping 10.244.1.30

PING 10.244.1.30 (10.244.1.30) 56(84) bytes of data.

64 bytes from 10.244.1.30: icmp_seq=1 ttl=64 time=0.047 ms

64 bytes from 10.244.1.30: icmp_seq=2 ttl=64 time=0.055 ms

 

 

[root@slave1 ~]# cat policy.yml

kind: NetworkPolicy

apiVersion: networking.k8s.io/v1

metadata:

  name: access-httpd

spec:

  podSelector:

    matchLabels:

      run: httpd

#定义将此 Network Policy 中的访问规则应用于 label 为 run: httpd 的 Pod,即 httpd 应用的三个副本 Pod。

  ingress:

  - from:

    - podSelector:

        matchLabels:

          access: "true"

#ingress 中定义只有 label 为 access: "true" 的 Pod 才能访问应用。

    ports:

    - protocol: TCP

      port: 80

#只能访问 80 端口

[root@master ~]# kubectl apply -f policy.yml

networkpolicy "access-httpd" created

[root@master ~]# kubectl get networkpolicy

NAME           POD-SELECTOR   AGE

access-httpd   run=httpd      18s

验证 Network Policy 的有效性

[root@master ~]# kubectl run houpj --rm -ti --labels="access=true" --image=busybox /bin/sh

If you don't see a command prompt, try pressing enter.

/ #

/ #

/ #

/ # wget httpd-svc:8080

Connecting to httpd-svc:8080 (10.107.139.120:8080)

index.html           100% |*****************************************************************************************************************************************************************|    45  0:00:00 ETA

/ # ls

bin         dev         etc         home        index.html  proc        root        sys         tmp         usr         var

/ # ping 10.244.1.30

PING 10.244.1.30 (10.244.1.30): 56 data bytes

^C

--- 10.244.1.30 ping statistics ---

2 packets transmitted, 0 packets received, 100% packet loss

[root@master ~]# kubectl run hou --rm -ti --image=busybox /bin/sh

If you don't see a command prompt, try pressing enter.

/ # wget httpd-svc:8080

Connecting to httpd-svc:8080 (10.107.139.120:8080)

^C

[root@master ~]# kubectl run hou --rm -ti --image=busybox /bin/sh

If you don't see a command prompt, try pressing enter.

/ # wget httpd-svc:8080

Connecting to httpd-svc:8080 (10.107.139.120:8080)

^C

[root@master ~]# curl -I 192.168.138.130:30000

^C

[root@master ~]# kubectl get pod -o wide

NAME                       READY     STATUS    RESTARTS   AGE       IP            NODE

busybox-6bdf9b5bbc-4njx5   1/1       Running   0          7m        10.244.1.34   slave1

httpd-78bf969db7-27b2t     1/1       Running   1          1h        10.244.1.30   slave1

httpd-78bf969db7-4gx7m     1/1       Running   0          1h        10.244.2.13   slave2

httpd-78bf969db7-78nxs     1/1       Running   1          1h        10.244.2.15   slave2

[root@master ~]# ping 10.244.1.30

PING 10.244.1.30 (10.244.1.30) 56(84) bytes of data.

^C

--- 10.244.1.30 ping statistics ---

4 packets transmitted, 0 received, 100% packet loss, time 2999ms

集群节点已经不能访问 Service, 也 Ping 不到副本 Pod。

集群外(192.168.56.1)已经不能访问 Service。

如果希望让集群节点和集群外(192.168.56.1)也能够访问到应用,可以对 Network Policy 做如下修改:

不管用;

[root@master ~]# kubectl get svc

NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE

httpd-svc    NodePort    10.107.139.120   <none>        8080:30000/TCP   1h

kubernetes   ClusterIP   10.96.0.1        <none>        443/TCP          18h

[root@master ~]# curl 10.107.139.120:8080

^C

[root@master ~]# curl 192.168.138.167:8080

curl: (7) Failed connect to 192.168.138.167:8080; Connection refused

[root@master ~]# curl 192.168.138.168:8080

curl: (7) Failed connect to 192.168.138.168:8080; Connection refused

[root@master ~]# curl -I 192.168.138.130:8080

curl: (7) Failed connect to 192.168.138.130:8080; Connection refused

Kubernetes 采用的是扁平化的网络模型,每个 Pod 都有自己的 IP,并且可以直接通信。

CNI 规范使得 Kubernetes 可以灵活选择多种 Plugin 实现集群网络。

Network Policy 则赋予了 Kubernetes 强大的网络访问控制机制。

 

参考:cloudman微信公众号

转载于:https://www.cnblogs.com/Honeycomb/p/9728088.html

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐