分类目录归档:容器服务

Kubernetes 环境下部署单节点 Redis


2022年6月03日 15:57:54   2,856 次浏览

创建namespace:  kubectl create namespace redis

创建configmap 存放redis配置文件信息

kind: ConfigMap
apiVersion: v1
metadata:
  name: redis-config
  namespace: redis
  labels:
    app: redis
data:
  redis.conf: |-
    dir /data
    port 6379
    bind 0.0.0.0
    appendonly yes
    protected-mode no
    requirepass yourpassword
    pidfile /data/redis-6379.pid

 

创建pvc存储

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: redis-pvc
  namespace: redis
  labels:
    app: redis
  annotations:
    volume.beta.kubernetes.io/storage-class: "nfs-storage"
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi

创建deployment

---
apiVersion: v1
kind: Service
metadata:
  name: redis-svc
  namespace: redis
  labels:
    app: redis
spec:
  type: ClusterIP
  ports:
    - name: redis
      port: 6379
  selector:
    app: redis
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-dep
  namespace: redis
  labels:
    app: redis
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
       initContainers:
        - name: system-init
          image: busybox:1.32
          imagePullPolicy: IfNotPresent
          command:
            - "sh"
            - "-c"
            - "echo 2048 > /proc/sys/net/core/somaxconn && echo never > /sys/kernel/mm/transparent_hugepage/enabled"
          securityContext:
            privileged: true
            runAsUser: 0
          volumeMounts:
          - name: sys
            mountPath: /sys
      containers:
        - name: redis
          image: redis:5.0.8
          command:
            - "sh"
            - "-c"
            - "redis-server /usr/local/etc/redis/redis.conf"
          ports:
            - containerPort: 6379
          resources:
            limits:
              cpu: 1000m
              memory: 1024Mi
            requests:
              cpu: 1000m
              memory: 1024Mi
          livenessProbe:
            tcpSocket:
              port: 6379
            initialDelaySeconds: 300
            timeoutSeconds: 1
            periodSeconds: 10
            successThreshold: 1
            failureThreshold: 3
          readinessProbe:
            tcpSocket:
              port: 6379
            initialDelaySeconds: 5
            timeoutSeconds: 1
            periodSeconds: 10
            successThreshold: 1
            failureThreshold: 3
          volumeMounts:
            - name: data
              mountPath: /data
            - name: config
              mountPath: /usr/local/etc/redis/redis.conf
              subPath: redis.conf
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: redis-pvc
        - name: config
          configMap:
            name: redis-config
        - name: sys
          hostPath:
            path: /sys

执行kubectl apply -f 应用资源

查看资源

[user@vm-centos-7 ~]$ kubectl get all -n redis
NAME                             READY   STATUS    RESTARTS   AGE
pod/redis-dep-77f876647c-g42vd   1/1     Running   1          23d

NAME                TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
service/redis-svc   ClusterIP   10.97.105.16   <none>        6379/TCP   23d

NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/redis-dep   1/1     1            1           23d

NAME                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/redis-dep-77f876647c   1         1         1       23d

测试

使用如下步骤测试 redis 是否正常运行

[user@vm-centos-7 ~]$ kubectl -it exec redis-dep-77f876647c-g42vd -n redis -- sh
# redis-cli
127.0.0.1:6379> auth yourpassword
OK
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "yourpassword"
127.0.0.1:6379>

 

kubeadm 更新证书


2022年3月03日 11:33:35   3,010 次浏览

安装 go 语言环境

Golang 官网下载地址:golang官网
打开官网下载地址选择对应的系统版本, 复制下载链接
这里我选择的是
go1.16.5.linux-amd64.tar.gz

下载解压

下载安装包

wget https://dl.google.com/go/go1.16.5.linux-amd64.tar.gz

解压到/usr/loacl目录下

tar -C /usr/local -zxvf  go1.16.5.linux-amd64.tar.gz

添加环境变量

添加/usr/loacl/go/bin 目录到 PATH 变量中。添加到 /etc/profile

vim /etc/profile 
# 在最后一行添加 
export GOROOT=/usr/local/go 
export PATH=$PATH:$GOROOT/bin 
# 保存退出后source一下 
source /etc/profile

验证

执行go version,如果现实版本号,则Go环境安装成功。

[root@master ~]# go version 
go version go1.16.5 linux/amd64

查看当前的证书时间

执行命令 查看当前证书时间

kubeadm alpha certs check-expiration

 

下载源码

打开github kubernetes 选择对应的版本下载
下载并解压
因为我是 v1.20.6 版本所以下载对应的

wget https://github.com/kubernetes/kubernetes/archive/refs/tags/v1.20.6.zip 
unzip v1.20.6.zip

修改 constants.go 文件
vim cmd/kubeadm/app/constants/constants.go 找到 CertificateValidity ,修改如下

cd kubernetes-1.20.6
vim cmd/kubeadm/app/constants/constants.go
....
const (
        // KubernetesDir is the directory Kubernetes owns for storing various configuration files
        KubernetesDir = "/etc/kubernetes"
        // ManifestsSubDirName defines directory name to store manifests
        ManifestsSubDirName = "manifests"
        // TempDirForKubeadm defines temporary directory for kubeadm
        // should be joined with KubernetesDir.
        TempDirForKubeadm = "tmp"

        // CertificateValidity defines the validity for all the signed certificates generated by kubeadm
        CertificateValidity = time.Hour * 24 * 365 * 100  #  修改此内容
....

编译 kubeadm

make WHAT=cmd/kubeadm

返回如下

[root@master kubernetes-1.20.6]# make WHAT=cmd/kubeadm

+++ [0624 10:59:21] Building go targets for linux/amd64:
    ./vendor/k8s.io/code-generator/cmd/prerelease-lifecycle-gen
+++ [0624 10:59:25] Building go targets for linux/amd64:
    ./vendor/k8s.io/code-generator/cmd/deepcopy-gen
+++ [0624 10:59:33] Building go targets for linux/amd64:
    ./vendor/k8s.io/code-generator/cmd/defaulter-gen
+++ [0624 10:59:44] Building go targets for linux/amd64:
    ./vendor/k8s.io/code-generator/cmd/conversion-gen
+++ [0624 11:00:04] Building go targets for linux/amd64:
    ./vendor/k8s.io/kube-openapi/cmd/openapi-gen
+++ [0624 11:00:19] Building go targets for linux/amd64:
    ./vendor/github.com/go-bindata/go-bindata/go-bindata
+++ [0624 11:00:20] Building go targets for linux/amd64:
    cmd/kubeadm

编译完生成如下目录和二进制文件

[root@master kubernetes-1.20.6]#  ll _output/bin/
总用量 75680
-rwxr-xr-x. 1 root root  5943296 6月  24 10:59 conversion-gen
-rwxr-xr-x. 1 root root  5689344 6月  24 10:59 deepcopy-gen
-rwxr-xr-x. 1 root root  5709824 6月  24 10:59 defaulter-gen
-rwxr-xr-x. 1 root root  3555111 6月  24 10:59 go2make
-rwxr-xr-x. 1 root root  1966080 6月  24 11:00 go-bindata
-rwxr-xr-x. 1 root root 39325696 6月  24 11:01 kubeadm
-rwxr-xr-x. 1 root root  9650176 6月  24 11:00 openapi-gen
-rwxr-xr-x. 1 root root  5656576 6月  24 10:59 prerelease-lifecycle-gen

备份文件

备份 kubeadm 和证书文件

cp /usr/bin/kubeadm{,.bak20210624} 
cp -r /etc/kubernetes/pki{,.bak20210624}

查看备份文件

[root@master kubernetes-1.20.6]# ll /usr/bin/kubeadm*
-rwxr-xr-x. 1 root root 39325696 6月  24 11:05 /usr/bin/kubeadm
-rwxr-xr-x. 1 root root 39210880 6月  24 11:02 /usr/bin/kubeadm.bak20210624

[root@master kubernetes-1.20.6 ll /etc/kubernetes/pki*
/etc/kubernetes/pki:
总用量 56
-rw-r--r--. 1 root root 1289 6月  24 11:05 apiserver.crt
-rw-r--r--. 1 root root 1139 6月  24 11:05 apiserver-etcd-client.crt
-rw-------. 1 root root 1675 6月  24 11:05 apiserver-etcd-client.key
-rw-------. 1 root root 1679 6月  24 11:05 apiserver.key
-rw-r--r--. 1 root root 1147 6月  24 11:05 apiserver-kubelet-client.crt
-rw-------. 1 root root 1675 6月  24 11:05 apiserver-kubelet-client.key
-rw-r--r--. 1 root root 1066 6月  22 15:01 ca.crt
-rw-------. 1 root root 1675 6月  22 15:01 ca.key
drwxr-xr-x. 2 root root  162 6月  22 15:01 etcd
-rwxr-xr-x. 1 root root 1078 6月  22 15:01 front-proxy-ca.crt
-rw-------. 1 root root 1675 6月  22 15:01 front-proxy-ca.key
-rw-r--r--. 1 root root 1103 6月  24 11:05 front-proxy-client.crt
-rw-------. 1 root root 1679 6月  24 11:05 front-proxy-client.key
-rw-------. 1 root root 1675 6月  22 15:01 sa.key
-rw-------. 1 root root  451 6月  22 15:01 sa.pub

/etc/kubernetes/pki.bak20210624:
总用量 56
-rw-r--r--. 1 root root 1289 6月  24 11:04 apiserver.crt
-rw-r--r--. 1 root root 1135 6月  24 11:04 apiserver-etcd-client.crt
-rw-------. 1 root root 1675 6月  24 11:04 apiserver-etcd-client.key
-rw-------. 1 root root 1679 6月  24 11:04 apiserver.key
-rw-r--r--. 1 root root 1143 6月  24 11:04 apiserver-kubelet-client.crt
-rw-------. 1 root root 1675 6月  24 11:04 apiserver-kubelet-client.key
-rw-r--r--. 1 root root 1066 6月  24 11:04 ca.crt
-rw-------. 1 root root 1675 6月  24 11:04 ca.key
drwxr-xr-x. 2 root root  162 6月  24 11:04 etcd
-rwxr-xr-x. 1 root root 1078 6月  24 11:04 front-proxy-ca.crt
-rw-------. 1 root root 1675 6月  24 11:04 front-proxy-ca.key
-rw-r--r--. 1 root root 1103 6月  24 11:04 front-proxy-client.crt
-rw-------. 1 root root 1679 6月  24 11:04 front-proxy-client.key
-rw-------. 1 root root 1675 6月  24 11:04 sa.key
-rw-------. 1 root root  451 6月  24 11:04 sa.pub

替换 kubeadm

将新生成的 kubeadm 进行替换

cp _output/bin/kubeadm /usr/bin/kubeadm

生成新的证书

cd /etc/kubernetes/pki kubeadm alpha certs renew all

返回内容

[root@master pki]# kubeadm alpha certs renew all
Command "all" is deprecated, please use the same command under "kubeadm certs"
[renew] Reading configuration from the cluster...
[renew] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'

certificate embedded in the kubeconfig file for the admin to use and for kubeadm itself renewed
certificate for serving the Kubernetes API renewed
certificate the apiserver uses to access etcd renewed
certificate for the API server to connect to kubelet renewed
certificate embedded in the kubeconfig file for the controller manager to use renewed
certificate for liveness probes to healthcheck etcd renewed
certificate for etcd nodes to communicate with each other renewed
certificate for serving etcd renewed
certificate for the front proxy client renewed
certificate embedded in the kubeconfig file for the scheduler manager to use renewed

Done renewing certificates. You must restart the kube-apiserver, kube-controller-manager, kube-scheduler and etcd, so that they can use the new certificates.

 

验证结果

到这里,证书就替换完成了。接下来验证下证书时间是否延长。

kubeadm alpha certs check-expiration

返回信息

[root@master pki]# kubeadm alpha certs check-expiration
Command "check-expiration" is deprecated, please use the same command under "kubeadm certs"
[check-expiration] Reading configuration from the cluster...
[check-expiration] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'

CERTIFICATE                EXPIRES                  RESIDUAL TIME   CERTIFICATE AUTHORITY   EXTERNALLY MANAGED
admin.conf                 May 31, 2121 03:05 UTC   99y                                     no      
apiserver                  May 31, 2121 03:05 UTC   99y             ca                      no      
apiserver-etcd-client      May 31, 2121 03:05 UTC   99y             etcd-ca                 no      
apiserver-kubelet-client   May 31, 2121 03:05 UTC   99y             ca                      no      
controller-manager.conf    May 31, 2121 03:05 UTC   99y                                     no      
etcd-healthcheck-client    May 31, 2121 03:05 UTC   99y             etcd-ca                 no      
etcd-peer                  May 31, 2121 03:05 UTC   99y             etcd-ca                 no      
etcd-server                May 31, 2121 03:05 UTC   99y             etcd-ca                 no      
front-proxy-client         May 31, 2121 03:05 UTC   99y             front-proxy-ca          no      
scheduler.conf             May 31, 2121 03:05 UTC   99y                                     no      

CERTIFICATE AUTHORITY   EXPIRES                  RESIDUAL TIME   EXTERNALLY MANAGED
ca                      Jun 20, 2031 07:01 UTC   9y              no      
etcd-ca                 Jun 20, 2031 07:01 UTC   9y              no      
front-proxy-ca          Jun 20, 2031 07:01 UTC   9y              no      

查看 node 状态

[root@master pki]#  kubectl get node
NAME     STATUS   ROLES                  AGE   VERSION
master   Ready    control-plane,master   44h   v1.20.6
node1    Ready    <none>                 43h   v1.20.6

 

节点异常Pod驱逐时效设置


2020年12月01日 10:02:02   3,811 次浏览

判断节点Not Ready时效设置

k8s默认节点not ready需要经过40s时间,可以通过以下参数进行修改

vim /etc/kubernetes/manifest/kuber-controller-manager.yaml     

--node-monitor-grace-period=20s  

vim /var/lib/kubelet/kubeadm-flags.env     
--node-status-update-frequency=4s 

vim /etc/kubernetes/manifest/kuber-controller-manager.yaml     
--node-monitor-grace-period=20s 

vim /var/lib/kubelet/kubeadm-flags.env     
--node-status-update-frequency=4s

备注:
--node-status-update-frequency 指定kubelet的上报频率

--node-monitor-grace-period 失联指定时间后,将节点状态readey变成为not ready

--node-monitor-grace-period需要是--node-status-update-frequency整数倍

社区默认的配置
--node-status-update-frequency  10s 
--node-monitor-period           5s 
--node-monitor-grace-period     40s
快速更新和快速响应

--node-status-update-frequency  4s 
--node-monitor-period   2s 
--node-monitor-grace-period  20s

节点Not Ready之后 pod驱逐并重启时效设置

节点Not Ready 之后, k8s默认需要300s时间,在新的节点上启动重新启动Pod, 可以通过以下参数修改。

设置以下参数

vim /etc/kubernetes/manifest/kube-apiserver.yaml

   - --default-not-ready-toleration-seconds=10
   - --default-unreachable-toleration-seconds=10

设置之后pod会被自动加上以下容忍污点

 tolerations:

 - effect: NoExecute
   key: node.kubernetes.io/not-ready
   operator: Exists
   tolerationSeconds: 10

 - effect: NoExecute
   key: node.kubernetes.io/unreachable
   operator: Exists
   tolerationSeconds: 10

故障隔离时效

节点故障隔离由node-agent和节点隔离controller控制器组成,检测kubelet,docker,containerd,calico等组件是否异常,隔离机制如下图所示:

Node-Agent上报的心跳时间间隔1s,连续5次为失败,即5s触发隔离, 为节点设置NoExecute污点,立即驱逐故障节点上的pod,k8s自动调度到其他正常的节点运行pod。

Kubernetes 最佳安全实践指南


2020年11月27日 23:09:50   2,377 次浏览

对于大部分 Kubernetes 用户来说,安全是无关紧要的,或者说没那么紧要,就算考虑到了,也只是敷衍一下,草草了事。实际上 Kubernetes 提供了非常多的选项可以大大提高应用的安全性,只要用好了这些选项,就可以将绝大部分的攻击抵挡在门外。为了更容易上手,我将它们总结成了几个最佳实践配置,大家看完了就可以开干了。当然,本文所述的最佳安全实践仅限于 Pod 层面,也就是容器层面,于容器的生命周期相关,至于容器之外的安全配置(比如操作系统啦、k8s 组件啦),以后有机会再唠。

1. 为容器配置 Security Context

大部分情况下容器不需要太多的权限,我们可以通过 Security Context 限定容器的权限和访问控制,只需加上 SecurityContext 字段:

apiVersion: v1
kind: Pod
metadata:
  name: <Pod name>
spec:
  containers:
  - name: <container name>
  image: <image>
+   securityContext:

2. 禁用 allowPrivilegeEscalation

allowPrivilegeEscalation=true 表示容器的任何子进程都可以获得比父进程更多的权限。最好将其设置为 false,以确保 RunAsUser 命令不能绕过其现有的权限集。

apiVersion: v1
kind: Pod
metadata:
  name: <Pod name>
spec:
  containers:
  - name: <container name>
  image: <image>
    securityContext:
  +   allowPrivilegeEscalation: false

3. 不要使用 root 用户

为了防止来自容器内的提权攻击,最好不要使用 root 用户运行容器内的应用。UID 设置大一点,尽量大于 3000

apiVersion: v1
kind: Pod
metadata:
  name: <name>
spec:
  securityContext:
+   runAsUser: <UID higher than 1000>
+   runAsGroup: <UID higher than 3000>

4. 限制 CPU 和内存资源

这个就不用多说了吧,requests 和 limits 都加上。

5. 不必挂载 Service Account Token

ServiceAccount 为 Pod 中运行的进程提供身份标识,怎么标识呢?当然是通过 Token 啦,有了 Token,就防止假冒伪劣进程。如果你的应用不需要这个身份标识,可以不必挂载:

apiVersion: v1
kind: Pod
metadata:
  name: <name>
spec:
+ automountServiceAccountToken: false

6. 确保 seccomp 设置正确

对于 Linux 来说,用户层一切资源相关操作都需要通过系统调用来完成,那么只要对系统调用进行某种操作,用户层的程序就翻不起什么风浪,即使是恶意程序也就只能在自己进程内存空间那一分田地晃悠,进程一终止它也如风消散了。seccomp(secure computing mode)就是一种限制系统调用的安全机制,可以可以指定允许那些系统调用。

对于 Kubernetes 来说,大多数容器运行时都提供一组允许或不允许的默认系统调用。通过使用 runtime/default 注释或将 Pod 或容器的安全上下文中的 seccomp 类型设置为 RuntimeDefault,可以轻松地在 Kubernetes 中应用默认值。

apiVersion: v1
kind: Pod
metadata:
  name: <name>
  annotations:
  + seccomp.security.alpha.kubernetes.io/pod: "runtime/default"

7. 限制容器的 capabilities

容器依赖于传统的 Unix 安全模型,通过控制资源所属用户和组的权限,来达到对资源的权限控制。以 root 身份运行的容器拥有的权限远远超过其工作负载的要求,一旦发生泄露,攻击者可以利用这些权限进一步对网络进行攻击。

默认情况下,使用 Docker 作为容器运行时,会启用 NET_RAW capability,这可能会被恶意攻击者进行滥用。因此,建议至少定义一个PodSecurityPolicy(PSP),以防止具有 NET_RAW 功能的容器启动。

通过限制容器的 capabilities,可以确保受攻击的容器无法为攻击者提供横向攻击的有效路径,从而缩小攻击范围。

apiVersion: v1
kind: Pod
metadata:
  name: <name>
spec:
  securityContext:
  + runAsNonRoot: true
  + runAsUser: <specific user>
  capabilities:
  drop:
  + -NET_RAW
  + -ALL

8. 只读

如果容器不需要对根文件系统进行写入操作,最好以只读方式加载容器的根文件系统,可以进一步限制攻击者的手脚。

apiVersion: v1
kind: Pod
metadata:
  name: <Pod name>
spec:
  containers:
  - name: <container name>
  image: <image>
  securityContext:
  + readOnlyRootFilesystem: true

9 总结

总之,Kubernetes 提供了非常多的选项来增强集群的安全性,没有一个放之四海而皆准的解决方案,所以需要对这些选项非常熟悉,以及了解它们是如何增强应用程序的安全性,才能使集群更加稳定安全。

最后,请记住:你需要万分小心你的 YAML 文件内容缩进,如果你的 YAML 文件非常多,眼睛看不过来,希望下面的神器可以助你一臂之力:


kubernetes基础原理知识


2020年5月28日 15:05:12   2,910 次浏览

kubernetes 已经成为容器编排领域的王者,它是基于容器的集群编排引擎,具备扩展集群、滚动升级回滚、弹性伸缩、自动治愈、服务发现等多种特性能力。

本文将带着大家快速了解 kubernetes ,了解我们谈论 kubernetes 都是在谈论什么。

kubernetes 架构

从宏观上来看 kubernetes 的整体架构,包括 Master、Node 以及 Etcd。

Master 即主节点,负责控制整个 kubernetes 集群。它包括 Api Server、Scheduler、Controller 等组成部分。它们都需要和 Etcd 进行交互以存储数据。

  • Api Server: 主要提供资源操作的统一入口,这样就屏蔽了与 Etcd 的直接交互。功能包括安全、注册与发现等。
  • Scheduler: 负责按照一定的调度规则将 Pod 调度到 Node 上。
  • Controller: 资源控制中心,确保资源处于预期的工作状态。

Node 即工作节点,为整个集群提供计算力,是容器真正运行的地方,包括运行容器、kubelet、kube-proxy。

  • kubelet: 主要工作包括管理容器的生命周期、结合 cAdvisor 进行监控、健康检查以及定期上报节点状态。
  • kube-proxy: 主要利用 service 提供集群内部的服务发现和负载均衡,同时监听 service/endpoints 变化并刷新负载均衡。

从创建 deployment 开始

deployment 是用于编排 pod 的一种控制器资源,我们会在后面做介绍。这里以 deployment 为例,来看看架构中的各组件在创建 deployment 资源的过程中都干了什么。

  1. 首先是 kubectl 发起一个创建 deployment 的请求
  2. apiserver 接收到创建 deployment 请求,将相关资源写入 etcd;之后所有组件与 apiserver/etcd 的交互都是类似的
  3. deployment controller list/watch 资源变化并发起创建 replicaSet 请求
  4. replicaSet controller list/watch 资源变化并发起创建 pod 请求
  5. scheduler 检测到未绑定的 pod 资源,通过一系列匹配以及过滤选择合适的 node 进行绑定
  6. kubelet 发现自己 node 上需创建新 pod,负责 pod 的创建及后续生命周期管理
  7. kube-proxy 负责初始化 service 相关的资源,包括服务发现、负载均衡等网络规则

至此,经过 kubenetes 各组件的分工协调,完成了从创建一个 deployment 请求开始到具体各 pod 正常运行的全过程。

Pod

在 kubernetes 众多的 api 资源中,pod 是最重要和基础的,是最小的部署单元。

首先我们要考虑的问题是,我们为什么需要 pod?pod 可以说是一种容器设计模式,它为那些”超亲密”关系的容器而设计,我们可以想象 servelet 容器部署 war 包、日志收集等场景,这些容器之间往往需要共享网络、共享存储、共享配置,因此我们有了 pod 这个概念。

对于 pod 来说,不同 container 之间通过 infra container 的方式统一识别外部网络空间,而通过挂载同一份 volume 就自然可以共享存储了,比如它对应宿主机上的一个目录。

容器编排

容器编排是 kubernetes 的看家本领了,所以我们有必要了解一下。kubernetes 中有诸多编排相关的控制资源,例如编排无状态应用的 deployment,编排有状态应用的 statefulset,编排守护进程 daemonset 以及编排离线业务的 job/cronjob 等等。

我们还是以应用最广泛的 deployment 为例。deployment、replicatset、pod 之间的关系是一种层层控制的关系。简单来说,replicaset 控制 pod 的数量,而 deployment 控制 replicaset 的版本属性。这种设计模式也为两种最基本的编排动作实现了基础,即数量控制的水平扩缩容、版本属性控制的更新/回滚。

水平扩缩容

水平扩缩容非常好理解,我们只需修改 replicaset 控制的 pod 副本数量即可,比如从 2 改到 3,那么就完成了水平扩容这个动作,反之即水平收缩。

更新/回滚

更新/回滚则体现了 replicaset 这个对象的存在必要性。例如我们需要应用 3 个实例的版本从 v1 改到 v2,那么 v1 版本 replicaset 控制的 pod 副本数会逐渐从 3 变到 0,而 v2 版本 replicaset 控制的 pod 数会注解从 0 变到 3,当 deployment 下只存在 v2 版本的 replicaset 时变完成了更新。回滚的动作与之相反。

滚动更新

可以发现,在上述例子中,我们更新应用,pod 总是一个一个升级,并且最小有 2 个 pod 处于可用状态,最多有 4 个 pod 提供服务。这种”滚动更新”的好处是显而易见的,一旦新的版本有了 bug,那么剩下的 2 个 pod 仍然能够提供服务,同时方便快速回滚。

在实际应用中我们可以通过配置 RollingUpdateStrategy 来控制滚动更新策略,maxSurge 表示 deployment 控制器还可以创建多少个新 Pod;而 maxUnavailable 指的是,deployment 控制器可以删除多少个旧 Pod。

kubernetes 中的网络

我们了解了容器编排是怎么完成的,那么容器间的又是怎么通信的呢?

讲到网络通信,kubernetes 首先得有”三通”基础:

  1. node 到 pod 之间可以通
  2. node 的 pod 之间可以通
  3. 不同 node 之间的 pod 可以通

简单来说,不同 pod 之间通过 cni0/docker0 网桥实现了通信,node 访问 pod 也是通过 cni0/docker0 网桥通信即可。而不同 node 之间的 pod 通信有很多种实现方案,包括现在比较普遍的 flannel 的 vxlan/hostgw 模式等。flannel 通过 etcd 获知其他 node 的网络信息,并会为本 node 创建路由表,最终使得不同 node 间可以实现跨主机通信。

微服务—service

在了解接下来的内容之前,我们得先了解一个很重要的资源对象:service。

我们为什么需要 service 呢?在微服务中,pod 可以对应实例,那么 service 对应的就是一个微服务。而在服务调用过程中,service 的出现解决了两个问题:

  1. pod 的 ip 不是固定的,利用非固定 ip 进行网络调用不现实
  2. 服务调用需要对不同 pod 进行负载均衡

service 通过 label 选择器选取合适的 pod,构建出一个 endpoints,即 pod 负载均衡列表。实际运用中,一般我们会为同一个微服务的 pod 实例都打上类似app=xxx的标签,同时为该微服务创建一个标签选择器为app=xxx的 service。

kubernetes 中的服务发现与网络调用

 

服务间调用

首先是东西向的流量调用,即服务间调用。这部分主要包括两种调用方式,即 clusterIp 模式以及 dns 模式。

clusterIp 是 service 的一种类型,在这种类型模式下,kube-proxy 通过 iptables/ipvs 为 service 实现了一种 VIP(虚拟 ip)的形式。只需要访问该 VIP,即可负载均衡地访问到 service 背后的 pod。

上图是 clusterIp 的一种实现方式,此外还包括 userSpace 代理模式(基本不用),以及 ipvs 模式(性能更好)。

dns 模式很好理解,对 clusterIp 模式的 service 来说,它有一个 A 记录是 service-name.namespace-name.svc.cluster.local,指向 clusterIp 地址。所以一般使用过程中,我们直接调用 service-name 即可。

服务外访问

南北向的流量,即外部请求访问 kubernetes 集群,主要包括三种方式:nodePort、loadbalancer、ingress。

nodePort 同样是 service 的一种类型,通过 iptables 赋予了调用宿主机上的特定 port 就能访问到背后 service 的能力。

loadbalancer 则是另一种 service 类型,通过公有云提供的负载均衡器实现。

我们访问 100 个服务可能需要创建 100 个 nodePort/loadbalancer。我们希望通过一个统一的外部接入层访问内部 kubernetes 集群,这就是 ingress 的功能。ingress 提供了统一接入层,通过路由规则的不同匹配到后端不同的 service 上。ingress 可以看做是”service 的 service”。ingress 在实现上往往结合 nodePort 以及 loadbalancer 完成功能。

 

 

 

Istio流量管理


2020年3月13日 09:12:32   1,900 次浏览

Istio 现在是 Service Mesh 中最火热的项目了,它主要负责对服务网格中的流量进行管理,包括动态服务发现、服务路由、弹性功能等。它作为 Service Mesh 的控制平面,配合 Envoy 作为数据平面,构成了 Service Mesh 中流量管理体系。

Istio 体系中流量管理配置以及相关机制比较冗杂,本文会尽量从整体上进行描述,不抠细节,以求有一个宏观的理解。

为什么需要 Service Mesh

我们首先要明白为什么会催生出 service mesh 这样的架构,而要理解这一点,我们需要对比 2 种东西,即传统微服务架构以及原生 k8s 架构。

传统微服务

我们知道现有的微服务架构已经比较成熟了,拥有完善的服务注册发现与服务治理功能(包括限流熔断、负载均衡、流量路由等),那么仍然困扰我们的是什么呢?
现有的微服务框架几乎都是集成于客户端 sdk 的,开发者在自己的应用中集成微服务框架 sdk,从而具有服务治理相关功能,那么这会带来 2 个几乎无法避免的问题:应用需要不断更新代码从而更新客户端 sdk,而这中间可能出现各种依赖冲突;应用集成并使用客户端 sdk 依赖于开发者的素质,常常导致效率低下。

这种情况下,我们自然而然地会想到使用 sidecar,将原先在 sdk 中的逻辑搬到 sidecar 上,在运维的时候与应用服务集成在一起,形成一种较 sdk 集成更先进的边车模式。这种方式对应用代码几乎没有侵入,并且也不受开发者的水平限制,完全做到了控制与逻辑的分离。当然作为易用性的代价,我们会失去一些性能。

sidecar 模式是 service mesh 相较微服务架构最主要的优点之一,而这在 service mesh 架构中也称为”数据平面”。

原生 k8s

这里我们思考的是在 k8s 之上,我们还有什么不满足?k8s 使用 kube-dns 以及 kube-proxy 配合 service 的概念支持了服务的注册与发现,意味着我们有了可以在 k8s 上构建微服务的基础。但在实际生产中,我们往往需要对流量更精细的管控,我们希望对单个 pod 或者一组 pod 的流量进行管理,而不仅仅是 service 的维度。例如一个需求是我们需要将 endpoints 下的一部分 pod 作为 v1 版本,其余作为 v2 版本,而/v1 前缀的请求都将调度到 v1 版本,/v2 前缀的请求调度到 v2 版本。

sidecar 作为数据平面与每一个应用部署在同一 pod 中,这意味着我们可以做到对 pod 级别的流量做管理了,而具体实现这一逻辑的就是 service mesh 中的另一层:”控制平面”了。

Istio 的架构

Istio 是目前 Service Mesh 领域最火热的架构,我们来看看 Istio 的整体架构。

Istio 分为数据平面以及控制平面,数据平面以 sidecar 的形式与应用部署在一起,承载其流量的发送与接收,而控制平面则是通过配置和控制消息来组织编排网络的逻辑,并下发给数据平面。

我们简单讲一下其中的组件:

  • Envoy: Envoy 是 c++开发的高性能代理,在 Istio 中被用于数据平面(图中即 Proxy),控制应用的入站和出站流量,而在 Istio 中,它拥有了动态服务发现、负载均衡、Http2/gRpc 代理、熔断器、健康检查、故障注入等多种特性,当然这些都需要控制平面配合下发指令实现。
  • Mixer: Mixer 是 Istio 控制平面的组件之一,用于执行访问控制和遥测(在最新版本已经没有了)
  • Pilot: Pilot 就是实现流量管理的关键组件了,为 Envoy 提供服务发现、智能路由、弹性功能(超时/重试/熔断),我们在后面也会详细描述。
  • Citadel: Citadel 通过内置的身份和证书管理,可以支持强大的服务到服务以及最终用户的身份验证和流量加密。
  • Galley: Galley 是 Istio 的配置验证、提取、处理和分发组件。

Istio 中流量管理的基础概念

流量管理是 Istio 中最基础和最重要的功能了。我们之前说到我们的真实诉求往往是希望指定某个 pod 或者一组特定的 pod 接收流量,最基础的方式是通过人肉运维搞定,但更优雅地是将其委托给 Istio 本身,Istio 会通过 Pilot 与 Envoy 代理搞定这一切。

怎么搞定这一切呢,既然 kubernetes 中 service 的抽象概念满足不了我们的需求,很自然地我们尝试去抽象一些更上层的概念来解决问题。Istio 也是如此,我们来讲一下其中主要的一些基本概念(至于具体的配置,由于篇幅限制跳过):

  • VirtualService: 顾名思义,其实可以理解为对 service 的一层抽象,用于定义路由规则,控制流量路由到匹配的 service 子集(VirtualService)。同时你可以为每个 VirtualService 设置一些独立的网络弹性属性,例如超时、重试等。
  • DestinationRule: 一般是 VirtualService 路由生效后定义目的服务的策略,包括断路器、负载均衡等。当然它也可以定义可路由子集,即 VirtualService 中的路由规则可以完全委托给 DestinationRule。
  • ServiceEntry: 这是用于将 Istio 服务网格外部的服务添加到内部服务注册的,也就是说你能访问外部服务了。
  • Gateway: 用于控制南北流量的网关了,将 VirtualService 绑定到 Gateway 上,就可以控制进入的 HTTP/TCP 流量了。
  • EnvoyFilter: 主要为 Envoy 配置过滤器,用于动态扩展 Envoy 的能力。

控制面 Pilot 的流量管理

在 Istio 的控制平面中,Pilot 是负责流量管理的组件,也是本文的主角。Pilot 整体架构如下(来自old_pilot_repo,不过整体出入不大):

我们看到 pilot 主要包含 2 个组件,即 Discovery services 和 Agent。

  • Agent: 该进程对应的是 pilot-agent,负责生产 Envoy 配置文件和管理 Envoy 生命周期。它和 Proxy(即 Envoy)以及具体应用 Service A/B 在同一 Pod 中部署。
  • Discovery services: 该进程对应的是 pilot-discovery,负责 pilot 中最关键的逻辑,即服务发现与流量管理。Discovery services 一般是和应用分开使用单独的 deployment 部署的。它会和 2 个类型的数据打交道。一是图中 k8s API Server 中的服务信息,即 service、endpoint、pod、node 等资源。其二就是 K8s API Server 中的一些 CRD 资源,包括了上述的 VritualService、DestinationRule、Gateway、ServiceEntry 等 Istio 控制面的流量规则配置信息。然后 Discovery services 会将这两部分数据转换为数据面可以理解的格式,并通过标准的 API 下发到各个数据面 (Envoy)sidecar 中。

Pilot Agent

pilot agent 它不是本文的主角,我们简单介绍一下。它主要的工作包括:

  • 生成 envoy 的相关配置,这里是指少部分的静态配置,毕竟大多动态配置是通过标准 xDS 接口从 Pilot 获取的。
  • 负责 envoy 进程的监控与管理工作,比如 envoy 挂了负责重启 envoy,或者配置变更后负责 reload envoy。
  • 启动 envoy 进程

Pilot-discovery

pilot-discovery 是 pilot 中的关键组件,我们进行较为详细的描述。

整体模型

pilot-discovery 的整体模型如下所示:

pilot-discovery 有两部分输入信息:

  • 来自 istio 控制平面的信息,也就是图中的 Rules API,包括 VirtualService、DestinationRule 等,这些信息以 kubernetes CRD 资源的形式保存在 kubernetes api server 中。
  • 来自于服务注册中心的服务注册信息,也就是图中的 kubernetes、mesos、cloud foundry 等。当然我们默认都认为是 kubernetes,下文相关描述也默认 kubernetes。

为了支持对不同服务注册中心的支持,所以需要有一个统一的数据存储模型,即 Abstract Model,和一个转换器 Platform Adapter 来实现各服务注册中心数据到 Abstract Model 的数据转换。当然除了注册中心数据外,Platform Adapter 还需将 VirtualService、DestinationRule 等 CRD 资源信息转换成 Abstract Model。

基于统一数据存储模型 Abstract Model,pilot-discovery 为数据面提供了控制信息服务,也就是所谓的 xds api 服务,从而得以将控制信息下发到数据面 envoy。

接下来我们讲讲 pilot-discovery 的一个整体流程。

初始化工作

pilot-discovery 首先干的事儿是进行一些初始化,初始化的工作内容包括:

  • 创建一个 kubernetes client。想要与 kubernetes api server 交互那自然需要 kubeClient,kubeClient 有 2 种创建方式。一种是使用特定的 kubeConfig 文件从而联通 kubernetes api server。另一种是使用 in cluster config 方式,也就是如果本身在 kubernetes 集群中,可以通过感知集群上下文的方式自动完成配置。
  • 多集群配置。现实情况下,我们可能会有多个 kubernetes 集群,一种方式是每个 kubernetes 集群搭建一套 Istio,但也可能是多个集群共用一个 Istio。那么 Istio 就需要连接其他远程 kubernetes 集群了,它被称为 remote cluster,Istio 在一个 map 中保存每个 remote cluster 的远程访问信息。
  • 配置和 Mixer 相关的东西,这里不详细描述。
  • 初始化和配置存储中心的连接。之前说过 Istio 中的诸多流量管理相关的配置,包括 VirtualService、DestinationRule 等,这些都是需要保存在存储中心(不是指 Etcd)的。Istio 支持将这些配置通过文件存储或者 kubernetes CRD 的方式进行存储,当然基本上是后者。当 CRD 资源完成注册之后,还会创建一个 config controller 来对 CRD 资源的 crud 事件进行处理。
  • 配置和注册中心的连接。这里的注册中心基本上指的是 kubernetes,如上所述,我们需要对 kubernetes 中的 pod、service、endpoints 等信息进行监听,当然我们也需要一个 service controller 来进行事件处理。
  • 初始化 pilot-discovery 服务。由于 envoy sidecar 是通过连接 pilot-discovery 服务来获取服务注册发现信息以及流量控制策略的,所以这里需要初始化 discovery 服务,包括提供 REST 协议的服务以及提供 gRPC 协议的服务。
  • 一些健康检查以及监控。

流量策略等 CRD 信息处理

我们之前讲 Istio 中流量策略等相关规则都是以 kubernetes CRD 的形式保存在 kubernetes api server 背后的 Etcd 中的,包括 VirtualService、DestinationRule。这些 CRD 资源当完成注册后,还创建了 config controller 来处理 CRD 的事件。

config controller 主要内容包括对每个 CRD 资源实现一个 list/watch,然后为其 Add、Update、Delete 等事件创建一个统一的流程框架,即将 CRD 对象事件封装成一个 Task 并 push 到一个 queue 里。随后启动协程依次处理 CRD 资源事件,即从 queue 中取出 Task 对象,并调用其 ChainHandler。

服务注册信息处理

服务注册信息指的默认为 kubernetes 服务注册信息(当然也支持其他注册中心,但比较小众),即 pod、service、endpoints、node 等资源。我们之前讲 Pilot 创建了一个 service controller 来实现对 kubernetes 资源的监控和处理。

service controller 的逻辑和 config controller 基本一致。为每种 kubernetes 资源都实现了一个 list/watch,然后将其 Add、Update、Delete 事件封装成 Task 对象 push 到 queue 中,随后启动协程从 queue 中取出并调用 CHainHandler。

暴露针对 Envoy 的控制信息服务

我们知道,Envoy 是通过与 Pilot 暴露的信息服务交互从而得到服务发现/路由策略等相关信息的。pilot-discovery 创建了 gRPC 协议的 discovery 服务,通过 xDS api 与 Envoy 交互,包括 eds、cds、rds、lds 等。具体细节我们之后描述。

数据面 Envoy 与 xDS 服务

数据平面是 sidecar 方式部署的智能代理,在 Istio 体系中,数据面往往是由 Envoy 担任。Envoy 可以调整控制服务网格之间的网络通信,是控制面流量管理的实际执行者。

Envoy xDS 是为 Istio 控制平面与数据平面通信设计的一套 api 协议,也就是下发流量管理配置的关键。

Envoy 中的基本概念

首先在了解 xDS 之前,我们需要了解 Envoy 中的一些基本概念:

  • Host: 能够进行网络通信的实体。在 Envoy 中主机是指逻辑网络应用程序,一块物理硬件上可以运行多个主机,只要它们是独立寻址的。
  • Downstream: 下游主机连接到 Envoy,发送请求并接受响应。
  • Upstream: 上游主机获取来自 Envoy 的连接请求和响应。
  • Cluster: 表示 Envoy 连接到的一组上游主机集群。Envoy 通过服务发现发现集群中的成员,Envoy 可以通过主动运行状况检查来确定集群成员的健康状况。
  • Endpoint: 即上游主机标识,包括端点的地址和健康检查配置。
  • Listener: 监听器,Envoy 暴露一个或多个监听器给下游主机(downstream)连接,当监听器监听到请求时候,对请求的处理会全部抽象为 Filter,例如 ReadFilter、WriteFilter、HttpFilter 等。
  • Listener filter: 过滤器,在 Envoy 指一些可插拔和可组合的逻辑处理层,是 Envoy 的核心逻辑处理单元。
  • Http Router Table: 即 Http 路由规则,例如请求的什么域名转发到什么集群(cluster)。

Envoy 架构

Envoy 整体架构如下所示:

 

Envoy 的工作流程为下游主机(Host A)发送请求至上游主机(Host B/C/D),Envoy 拦截请求(代理注入及流量劫持我们不详细描述),Listener 监听到下游主机请求后将请求内容抽象为 Filter Chains,并根据流量策略相关的配置信息路由至相应的上游主机集群(Cluster),从而完成路由转发、负载均衡、流量策略等能力。

上述的流量策略相关的配置信息主要以动态配置的方式由 xDS api 实现,也是我们关注的重点。此外,Envoy 作为流量代理还可以将部分配置以静态配置的方式写入文件中,启动时候直接加载。

xDS 服务

所谓 xDS,这里的 x 是一个代词,在 Istio 中,主要包括 cds(cluster discovery service)、eds(endpoint discovery service)、rds(route discovery service)、lds(listener discovery service),ads(aggregated discovery service)则是对这些 api 的一个封装。

在以上 api 引入的概念中,endpoint 表示一个具体的应用实例,对应 ip 和端口,类似 kubernetes 中的一个 pod。cluster 是一个应用集群,对应一个或多个 endpoint,类似 kubernetes 中 service 的概念(实际上应该更小一些)。route 即当我们做类似灰发、金丝雀发布时,同一个服务会运行多个版本,每个版本对应一个 cluster,这时就需要通过 route 规则规定请求如何路由到某个版本的 cluster 上。

在实际请求过程中,xDS 接口采用的顺序如下:

  1. CDS 首先更新 Cluster 数据
  2. EDS 更新相应 Cluster 的 Endpoint 信息
  3. LDS 更新 CDS/EDS 相应的 Listener
  4. RDS 最后更新新增 Listener 相关的 Route 配置
  5. 删除不再使用的 CDS/EDS 配置

不过目前这个流程都已整合在 ADS 中,即聚合的服务发现,ADS 通过一个 gRPC 流以保证各个 xDS 接口的调用顺序。

我们之前讲过 pilot-discovery 的工作包括会创建一个 discovery 服务。现在它会与每一个 Envoy 建立一个双向 streaming 的 gRPC 连接,Envoy proxy 则会通过 ADS 接口按照上述的调用逻辑发起请求,并将 Pilot 的流量管理关键概念 VirtualService、DestinationRule 等组装成 cluster、endpoint、router、listener 等 envoy 配置。最终这些动态配置会作用在每一个 Envoy Proxy 上,当 Envoy listener 监听到下游主机请求时,就可以根据这些配置完成实际的动态服务发现、流量管理等功能。

Kubernetes服务发现总结


2019年12月02日 09:39:36   1,600 次浏览

Kubernetes服务发现主要可以归为三种情形:1.Kubernetes集群内部间服务如何互相通信;2.Kuberntes集群外部如何访问集群内部服务;3.Kubernetes集群内部如何访问集群外部服务。这节针对这三种情况做个总结。

集群间服务通信

1.通过Pod IP相互通信,但是Pod具有不确定性,Pod IP会发生改变,所以这种方式并不推荐。

2.部署Pod对应的Service,访问Service IP,请求负载均衡转发服务到各个对应的Pod实例。

比如有如下配置文件demo.yml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-d
spec:
  selector:
    matchLabels:
      name: web-app
  replicas: 3
  template:
    metadata:
      labels:
        name: web-app
    spec:
      containers:
        - name: nginx
          image: nginx
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-s
spec:
  ports:
    - port: 8080
      targetPort: 80
      protocol: TCP
  selector:
    name: web-app

运行该配置,查看Service对应的IP:

通过Service IP和端口就能访问对应的Pod服务:

但是我们事先并不知道Service的IP是多少,如果我们在启动服务后再去配置这个IP的话,这个过程也是非常麻烦的,所幸我们可以通过DNS解决这个问题,也就是下面这种方式:

3.部署Pod对应的Service,通过ServiceName,请求负载均衡转发服务到各个对应的Pod实例。

通过下面这段配置部署一个新的Pod服务:

apiVersion: v1
kind: Pod
metadata:
  name: centos-pod
spec:
  containers:
    - name: centos
      image: centos
      command:
        - sh
        - -c
        - 'while true; do sleep 360000000; done'

进入到该容器内部,测试下是否可以访问上面创建的Service:

试着通过ServiceName访问Nginx服务:

可以看到效果是一样的。这得益于Kube-dns,具体的格式为:<service_name>.<namespace>.svc.<cluster_domain>其中,<cluster_domain>默认值为cluster.local,可以通过kubelet的--cluster-domain=SomeDomain参数进行设置。为了在Pod中调用其他Service,kubelet会自动在容器中创建域名解析配置:

所以除了使用curl nginx-s:8080访问外,以下这些也是等效的:

curl nginx-s.default:8080
curl nginx-s.default.svc:8080
curl nginx-s.default.svc.cluster.local:8080

所以我们现在可以不用事先知道Service的IP了,只要事先知道ServiceName即可(ServiceName是我们自己定义的)。

4.通过Headless Service,返回Pod的所有实例。

有时候我们并不需要Service的负载均衡功能,而是手动获取Service对应的Pod实例,自己决定如何访问。创建一个Headless Service配置:

apiVersion: v1
kind: Service
metadata:
  name: nginx-headless-s
spec:
  ports:
    - port: 8080
  clusterIP: None
  selector:
    name: web-app

创建该Headless Service,然后到centos-pod容器内部查询DNS记录:

如果没有nslookup命令,可以使用yum -y install bind-utils命令安装。

可以看到,通过解析nginx-headless-s DNS,返回了三个Nginx Pod实例地址。

集群外部访问内部

1.Pod指定hostPort,或者开启hostNetwork,这样外部就可以通过Pod宿主机IP+Pod端口访问了,但Pod调度的不确定性,这种方式不推荐;

2.通过Service的NodePort暴露服务,如果服务较多的话不推荐,因为这种方式会在集群中的所有节点上都暴露该端口,所以当服务多的时候很容易造成端口冲突,并且端口维护不便;

3.通过Ingress集中暴露服务,要暴露的服务较多的时候推荐。

这三种方式是前面博客中都有介绍到,所以就不赘述了。

集群内部访问外部

1.直接通过外部服务的IP和端口进行访问;

2.通过Service和Endpoint绑定,集群内的服务通过DNS访问集群外部服务(推荐)。

创建如下配置(test-remote.yml):

apiVersion: v1
kind: Service
metadata:
  name: remote-s
spec:
  ports:
    - protocol: TCP
      port: 8081
      targetPort: 8080
---
apiVersion: v1
kind: Endpoints
metadata:
  name: remote-s
subsets:
  - addresses:
      - ip: 192.168.73.42
    ports:
      - port: 8080

Service和Endpoints名称一样,所以他们会绑定上,通过访问remote-s Service便可以访问到对应的Endpoint地址192.168.73.42:8080服务(这是个我在集群外部,通过Spring Boot搭建的简单web服务)。

运行该配置后,在master节点上,测试是否可以访问:

我们进到centos-pod容器内部,通过服务名称看看是否可以访问到:

可以看到这种方式也是没问题的,推荐使用这种方式,可以降低耦合度。

Kubernetes服务发现


2019年12月01日 23:27:49   1,693 次浏览

我们来说说 kubernetes 的服务发现。那么首先这个大前提是同主机通信以及跨主机通信都是 ok 的,即同一 kubernetes 集群中各个 pod 都是互通的。这点是由更底层的方案实现,包括 docker0/CNI 网桥、flannel vxlan/host-gw 模式等,在此篇就不展开讲了。

在各 pod 都互通的前提下,我们可以通过访问 podIp 来调用 pod 上的资源,那么离服务发现还有多少距离呢?首先 Pod 的 IP 不是固定的,另一方面我们访问一组 Pod 实例的时候往往会有负载均衡的需求,那么 service 对象就是用来解决此类问题的。

集群内通信

endPoints

service 首先解决的是集群内通信的需求,首先我们编写一个普通的 deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hostnames
spec:
  selector:
    matchLabels:
      app: hostnames
  replicas: 3
  template:
    metadata:
      labels:
        app: hostnames
    spec:
      containers:
        - name: hostnames
          image: mirrorgooglecontainers/serve_hostname
          ports:
            - containerPort: 9376
              protocol: TCP

这个应用干的事儿就是访问它是返回自己的 hostname,并且每个 pod 都带上了 app 为 hostnames 的标签。

那么我们为这些 pod 编写一个普通的 service:

apiVersion: v1
kind: Service
metadata:
  name: hostnames
spec:
  selector:
    app: hostnames
  ports:
    - name: default
      protocol: TCP
      port: 80
      targetPort: 9376

可以看到 service 通过 selector 选择  了带相应的标签 pod,而这些被选中的 pod,成为 endpoints,我们可以试一下:

 ~/cloud/k8s kubectl get ep hostnames
NAME        ENDPOINTS
hostnames   172.28.21.66:9376,172.28.29.52:9376,172.28.70.13:9376

当某一个 pod 出现问题,不处于 running 状态或者 readinessProbe 未通过时,endpoints 列表会将其摘除。

clusterIp

以上我们有了 service 和 endpoints,而默认创建 service 的类型是 clusterIp 类型,我们查看一下之前创建的 service:

 ~ kubectl get svc hostnames
NAME        TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
hostnames   ClusterIP   10.212.8.127   <none>        80/TCP    8m2s

我们看到 cluster-ip 是 10.212.8.127,那么我们此时可以在 kubernetes 集群内通过这个地址访问到 endpoints 列表里的任意 pod:

sh-4.2# curl 10.212.8.127
hostnames-8548b869d7-9qk6b
sh-4.2# curl 10.212.8.127
hostnames-8548b869d7-wzksp
sh-4.2# curl 10.212.8.127
hostnames-8548b869d7-bvlw8

访问了三次 clusterIp 地址,返回了三个不同的 hostname,我们意识到 clusterIp 模式的 service 自动对请求做了 round robin 形式的负载均衡。

对于此时 clusterIp 模式 serivice 来说,它有一个 A 记录是service-name.namespace-name.svc.cluster.local,指向 clusterIp 地址:

sh-4.2# nslookup hostnames.coops-dev.svc.cluster.local
Server:		10.212.0.2
Address:	10.212.0.2#53

Name:	hostnames.coops-dev.svc.cluster.local
Address: 10.212.8.127

理所当然我们通过此 A 记录去访问得到的效果一样:

sh-4.2# curl hostnames.coops-dev.svc.cluster.local
hostnames-8548b869d7-wzksp

那对 pod 来说它的 A 记录是啥呢,我们可以看一下:

sh-4.2# nslookup 172.28.21.66
66.21.28.172.in-addr.arpa	name = 172-28-21-66.hostnames.coops-dev.svc.cluster.local.

headless service

service 的 cluserIp 默认是 k8s 自动分配的,当然也可以自己设置,当我们将 clusterIp 设置成 none 的时候,它就变成了 headless service。

headless service 一般配合 statefulSet 使用。statefulSet 是一种有状态应用的容器编排方式,其核心思想是给予 pod 指定的编号名称,从而让 pod 有一个不变的唯一网络标识码。那这么说来,使用 cluserIp 负载均衡访问 pod 的方式显然是行不通了,因为我们渴望通过某个标识直接访问到 pod 本身,而不是一个虚拟 vip。

这个时候我们其实可以借助 dns,每个 pod 都会有一条 A 记录pod-name.service-name.namespace-name.svc.cluster.local指向 podIp,我们可以通过这条 A 记录直接访问到 pod。

我们编写相应的 statefulSet 和 service 来看一下:

---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: hostnames
spec:
  serviceName: "hostnames"
  selector:
    matchLabels:
      app: hostnames
  replicas: 3
  template:
    metadata:
      labels:
        app: hostnames
    spec:
      containers:
        - name: hostnames
          image: mirrorgooglecontainers/serve_hostname
          ports:
            - containerPort: 9376
              protocol: TCP

---
apiVersion: v1
kind: Service
metadata:
  name: hostnames
spec:
  selector:
    app: hostnames
  clusterIP: None
  ports:
    - name: default
      protocol: TCP
      port: 80
      targetPort: 9376

如上,statefulSet 和 deployment 并没有什么不同,多了一个字段spec.serviceName,这个字段的作用就是告诉 statefuleSet controller,在逻辑处理时使用hostnames这个 service 来保证 pod 的唯一可解析性。

当你执行 apply 之后,一会你就可以看到生成了对应的 pod:

 ~ kubectl get pods -w -l app=hostnames
NAME          READY   STATUS    RESTARTS   AGE
hostnames-0   1/1     Running   0          9m54s
hostnames-1   1/1     Running   0          9m28s
hostnames-2   1/1     Running   0          9m24s

如意料之中,这里对 pod 名称进行了递增编号,并不重复,同时这些 pod 的创建过程也是按照编号依次串行进行的。我们知道,使用 deployment 部署的 pod 名称会加上 replicaSet 名称和随机数,重启后是不断变化的。而这边使用 statefulSet 部署的 pod,虽然 podIp 仍然会变化,但名称是一直不会变的,基于此我们得以通过固定的 Dns A 记录来访问到每个 pod。

那么此时,我们来看一下 pod 的 A 记录:

sh-4.2# nslookup hostnames-0.hostnames
Server:		10.212.0.2
Address:	10.212.0.2#53

Name:	hostnames-0.hostnames.coops-dev.svc.cluster.local
Address: 172.28.3.57

sh-4.2# nslookup hostnames-1.hostnames
Server:		10.212.0.2
Address:	10.212.0.2#53

Name:	hostnames-1.hostnames.coops-dev.svc.cluster.local
Address: 172.28.29.31

sh-4.2# nslookup hostnames-2.hostnames
Server:		10.212.0.2
Address:	10.212.0.2#53

Name:	hostnames-2.hostnames.coops-dev.svc.cluster.local
Address: 172.28.23.31

和之前的推论一致,我们可以通过pod-name.service-name.namespace-name.svc.cluster.local这条 A 记录访问到 podIp,在同一个 namespace 中,我们可以简化为pod-name.service-name

而这个时候,service 的 A 记录是什么呢:

sh-4.2# nslookup hostnames
Server:		10.212.0.2
Address:	10.212.0.2#53

Name:	hostnames.coops-dev.svc.cluster.local
Address: 172.28.29.31
Name:	hostnames.coops-dev.svc.cluster.local
Address: 172.28.3.57
Name:	hostnames.coops-dev.svc.cluster.local
Address: 172.28.23.31

原来是 endpoints 列表里的一组 podIp,也就是说此时你依然可以通过service-name.namespace-name.svc.cluster.local这条 A 记录来负载均衡地访问到后端 pod。

iptables

或多或少我们知道 kubernetes 里面的 service 是基于 kube-proxy 和 iptables 工作的。service 创建之后可以被 kube-proxy 感知到,那么它会为此在宿主机上创建对应的 iptables 规则。

以 cluserIp 模式的 service 为例,首先它会创建一条KUBE-SERVICES规则作为入口:

-A KUBE-SERVICES -d 10.212.8.127/32 -p tcp -m comment --comment "default/hostnames: cluster IP" -m tcp --dport 80 -j KUBE-SVC-NWV5X2332I4OT4T3

这条记录的意思是:所有目的地址是 10.212.8.127 这条 cluserIp 的,都将跳转到KUBE-SVC iptables 链处理。

那么我们来看 KUBE-SVC链都是什么:

-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-WNBA2IHDGP2BOBGZ
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-X3P2623AGDH6CDF3
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -j KUBE-SEP-57KPRZ3JQVENLNBR

这组规则其实是用于负载均衡的,我们看到了–probability 依次是 1/3、1/2、1,由于 iptables 规则是自上而下匹配的,所以设置这些值能保证每条链匹配到的几率一样。处理完负载均衡的逻辑后,又分别将请求转发到了另外三条规则,我们来看一下:

-A KUBE-SEP-57KPRZ3JQVENLNBR -s 172.28.21.66/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000
-A KUBE-SEP-57KPRZ3JQVENLNBR -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 172.28.21.66:9376

-A KUBE-SEP-WNBA2IHDGP2BOBGZ -s 172.28.29.52/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000
-A KUBE-SEP-WNBA2IHDGP2BOBGZ -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 172.28.29.52:9376

-A KUBE-SEP-X3P2623AGDH6CDF3 -s 172.28.70.13/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000
-A KUBE-SEP-X3P2623AGDH6CDF3 -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 172.28.70.13:9376

可以看到 KUBE-SEP链 就是三条 DNAT 规则,并在 DNAT 之前设置了一个 0x00004000 的标志。DNAT 规则就是在 PREROUTING,即路由作用之前,将请求的目的地址和端口改为–to-destination 指定的 podIp 和端口。这样一来,我们起先访问 10.212.8.127 这个 cluserIp 的请求,就会被负载均衡到各个 pod 上。

那么 pod 重启了,podIp 变了怎么办?自然是 kube-proxy 负责监听 pod 变化以及更新维护 iptables 规则了。

而对于 headless service 来说,我们直接通过固定的 A 记录访问到了 pod,自然不需要这些 iptables 规则了。

iptables 理解起来比较简单,但实际上性能并不好。可以想象,当我们的 pod 非常多时,成千上万的 iptables 规则将被创建出来,并不断刷新,会占用宿主机大量的 cpu 资源。一个行之有效的方案是基于 IPVS 模式的 service,IPVS 不需要为每个 pod 都设置 iptables 规则,而是将这些规则都放到了内核态,极大降低了维护这些规则的成本。

集群间通信

外界访问 service

以上我们讲了请求怎么在 kubernetes 集群内互通,主要基于 kube-dns 生成的 dns 记录以及 kube-proxy 维护的 iptables 规则。而这些信息都是作用在集群内的,那么自然我们从集群外访问不到一个具体的 service 或者 pod 了。

service 除了默认的 cluserIp 模式外,还提供了很多其他的模式,比如 nodePort 模式,就是用于解决该问题的。

apiVersion: v1
kind: Service
metadata:
  name: hostnames
spec:
  selector:
    app: hostnames
  type: NodePort
  ports:
    - nodePort: 8477
      protocol: TCP
      port: 80
      targetPort: 9376

我们编写了一个 nodePort 模式的 service,并且设置 nodePort 为 8477,那么意味着我们可以通过任意一台宿主机的 8477 端口访问到 hostnames 这个 service。

sh-4.2# curl 10.1.6.25:8477
hostnames-8548b869d7-j5lj9
sh-4.2# curl 10.1.6.25:8477
hostnames-8548b869d7-66vnv
sh-4.2# curl 10.1.6.25:8477
hostnames-8548b869d7-szz4f

我们随便找了一台 node 地址去访问,得到了相同的返回配方。
那么这个时候它的 iptables 规则是怎么作用的呢:

-A KUBE-NODEPORTS -p tcp -m comment --comment "default/hostnames: nodePort" -m tcp --dport 8477 -j KUBE-SVC-67RL4FN6JRUPOJYM

kube-proxy 在每台宿主机上都生成了如上的 iptables 规则,通过–dport 指定了端口,访问该端口的请求都会跳转到KUBE-SVC链上,KUBE-SVC链和之前 cluserIp service 的配方一样,接下来就和访问 cluserIp service 没什么区别了。

不过还需要注意的是,在请求离开当前宿主机发往其他 node 时会对其做一次 SNAT 操作:

-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE

可以看到这条 postrouting 规则给即将离开主机的请求进行了一次 SNAT,判断条件为带有 0x4000 标志,这就是之前 DNAT 带的标志,从而判断请求是从 service 转发出来的,而不是普通请求。

需要做 SNAT 的原因很简单,首先这是一个外部的未经 k8s 处理的请求,
如果它访问 node1,node1 的负载均衡将其转发给 node2 上的某个 pod,这没什么问题,而这个 pod 处理完后直接返回给外部 client,那么外部 client 就很疑惑,明明自己访问的是 node1,给自己返回的确是 node2,这时往往会报错。

SNAT 的作用与 DNAT 相反,就是在请求从 node1 离开发往 node2 时,将源地址改为 node1 的地址,那么当 node2 上的 pod 返回时,会返回给 node1,然后再让 node1 返回给 client。

             client
               | ^
               | |
               v |
  node 2 <--- node 1
   | ^   SNAT
   | |   --->
   v |
endpoints

service 还有另外 2 种通过外界访问的方式。适用于公有云的 LoadBalancer 模式的 service,公有云 k8s 会调用 CloudProvider 在公有云上为你创建一个负载均衡服务,并且把被代理的 Pod 的 IP 地址配置给负载均衡服务做后端。另外一种是 ExternalName 模式,可以通过在spec.externalName来指定你想要的外部访问域名,例如hostnames.example.com,那么你访问该域名和访问service-name.namespace-name.svc.cluser.local效果是一样的,这时候你应该知道,其实 kube-dns 为你添加了一条 CNAME 记录。

ingress

service 有一种类型叫作 loadBalancer,不过如果每个 service 对外都配置一个负载均衡服务,成本很高而且浪费。一般来说我们希望有一个全局的负载均衡器,通过访问不同 url,转发到不同 service 上,而这就是 ingress 的功能,ingress 可以看做是 service 的 service。

ingress 其实是对反向代理的一种抽象,相信大家已经感觉到,这玩意儿和 nginx 十分相似,实际上 ingress 是抽象层,而其实现层其中之一就支持 nginx。

我们可以部署一个 nginx ingress controller:

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

mandatory.yaml是官方维护的 ingress controller,我们看一下:

kind: ConfigMap
apiVersion: v1
metadata:
  name: nginx-configuration
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nginx-ingress-controller
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: ingress-nginx
      app.kubernetes.io/part-of: ingress-nginx
  template:
    metadata:
      labels:
        app.kubernetes.io/name: ingress-nginx
        app.kubernetes.io/part-of: ingress-nginx
      annotations:
        ...
    spec:
      serviceAccountName: nginx-ingress-serviceaccount
      containers:
        - name: nginx-ingress-controller
          image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.20.0
          args:
            - /nginx-ingress-controller
            - --configmap=$(POD_NAMESPACE)/nginx-configuration
            - --publish-service=$(POD_NAMESPACE)/ingress-nginx
            - --annotations-prefix=nginx.ingress.kubernetes.io
          securityContext:
            capabilities:
              drop:
                - ALL
              add:
                - NET_BIND_SERVICE
            # www-data -> 33
            runAsUser: 33
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
            - name: http
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          ports:
            - name: http
              containerPort: 80
            - name: https
              containerPort: 443

总的来说,我们定义了一个基于 nginx-ingress-controller 镜像的 pod,
而这个 pod 自身,是一个监听 ingress 对象及其代理后端 service 变化的控制器。

当一个 ingress 对象被创建时,nginx-ingress-controller 就会根据 ingress 对象里的内容,生成一份 nginx 配置文件(nginx.conf),并依此启动一个 nginx 服务。

当 ingress 对象被更新时,nginx-ingress-controller 就会更新这个配置文件。nginx-ingress-controller 还通过 nginx lua 方案实现了 nginx upstream 的动态配置。

为了让外界可以访问到这个 nginx,我们还得给它创建一个 service 来把 nginx 暴露出去:


$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/provider/baremetal/service-nodeport.yaml

这里面的内容描述了一个 nodePort 类型的 service:

apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
spec:
  type: NodePort
  ports:
    - name: http
      port: 80
      targetPort: 80
      protocol: TCP
    - name: https
      port: 443
      targetPort: 443
      protocol: TCP
  selector:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx

可以看到这个 service 仅仅是把 nginx pod 的 80/443 端口暴露出去,完了你就可以通过宿主机 Ip 和 nodePort 端口访问到 nginx 了。

接下来我们来看 ingress 对象一般是如何编写的,我们可以参考一个例子

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: cafe-ingress
spec:
  tls:
  - hosts:
    - cafe.example.com
    secretName: cafe-secret
  rules:
  - host: cafe.example.com
    http:
      paths:
      - path: /tea
        backend:
          serviceName: tea-svc
          servicePort: 80
      - path: /coffee
        backend:
          serviceName: coffee-svc
          servicePort: 80

这个 ingress 表明我们整体的域名是cafe.example.com,希望通过cafe.example.com/tea访问tea-svc这个 service,通过cafe.example.com/coffee访问coffee-svc这个 service。这里我们通过关键字段spec.rules来编写转发规则。

我们可以查看到 ingress 对象的详细信息:

$ kubectl get ingress
NAME           HOSTS              ADDRESS   PORTS     AGE
cafe-ingress   cafe.example.com             80, 443   2h

$ kubectl describe ingress cafe-ingress
Name:             cafe-ingress
Namespace:        default
Address:
Default backend:  default-http-backend:80 (<none>)
TLS:
  cafe-secret terminates cafe.example.com
Rules:
  Host              Path  Backends
  ----              ----  --------
  cafe.example.com
                    /tea      tea-svc:80 (<none>)
                    /coffee   coffee-svc:80 (<none>)
Annotations:
Events:
  Type    Reason  Age   From                      Message
  ----    ------  ----  ----                      -------
  Normal  CREATE  4m    nginx-ingress-controller  Ingress default/cafe-ingress

我们之前讲了我们通过 nodePort 的方式将 nginx-ingress 暴露出去了,而这时候我们 ingress 配置又希望通过cafe.example.com来访问到后端 pod,那么首先cafe.example.com这个域名得指到任意一台宿主机Ip:nodePort上,请求到达 nginx-ingress 之后再转发到各个后端 service 上。当然,暴露 nginx-ingress 的方式有很多种,除了 nodePort 外还包括 loadBalancer、hostNetWork 方式等等。

我们最后来试一下请求:

$ curl cafe.example.com/coffee
Server name: coffee-7dbb5795f6-vglbv
$ curl cafe.example.com/tea
Server name: tea-7d57856c44-lwbnp

可以看到 nginx ingress controller 已经为我们成功将请求转发到了对应的后端 service。而当请求没有匹配到任何一条 ingress rule 的时候,理所当然我们会得到一个 404。

至此,kubernetes 的容器网络是怎么实现服务发现的已经讲完了,而服务发现正是微服务架构中最核心的问题,解决了这个问题,那么使用 kubernetes 来实现微服务架构也就实现了一大半。

Kubernetes StatefulSet


2019年11月27日 15:54:41   1,145 次浏览

前面介绍的Pod管理对象,如RC/RS、Deployment、DaemonSet等都是面向无状态服务的,而对于有状态的应用,比如MySQL集群,MongoDB集群等,则可以使用StatefulSet来完成。有状态的应用集群通常有以下这些特点:

  1. 每个节点都有固定的身份ID,通过这个ID,集群中的成员可以相互发现并通信;
  2. 集群的规模是比较固定的,集群规模不能随意变动;
  3. 集群中的每个节点都是有状态的,通常会持久化数据到永久存储中。

StatefulSet特性

通过StatefulSet搭建的集群通常有以下这些特性:

  1. StatefulSet里的每个Pod都有稳定、唯一的网络标识,可以用来发现集群内的其他成员。假设StatefulSet的名称为nginx,那么第1个Pod叫nginx-0,第2个叫nginx-1,以此类推;
  2. StatefulSet控制的Pod副本的启停顺序是受控的,操作第n个Pod时,前n-1个Pod已经是运行且准备好的状态。
  3. StatefulSet里的Pod采用稳定的持久化存储卷,通过PV或PVC来实现,删除Pod时默认不会删除与StatefulSet相关的存储卷(为了保证数据的安全);
  4. 配合Headless Service使用,用于发现和控制Pod实例数量。

StatefulSet实践

下面使用StatefulSet搭建个Nginx集群,持久化存储使用上一节搭建的名称为managed-nfs-storage的StorageClass。

创建nginx-headless-service.yml配置文件:

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    name: nginx
spec:
  ports:
    - port: 80
      targetPort: 80
  clusterIP: None # 表明为Headleass Service
  selector:
    role: web-app

创建该Headless Service:

kubectl create -f nginx-headless-service.yml

接着创建nginx-statefulset.yml配置文件:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx-sc
spec:
  serviceName: "nginx" # 对应刚刚创建的Headless Service名称
  replicas: 3
  selector:
    matchLabels:
      role: web-app
  template:
    metadata:
      labels:
        role: web-app
    spec:
      terminationGracePeriodSeconds: 10
      containers:
        - name: nginx
          image: nginx
          ports:
            - containerPort: 80
          volumeMounts:
            - mountPath: /usr/share/nginx/html
              name: nginx-persistent-storage # 和下面的pvc名称对应
  volumeClaimTemplates: # pvc模板
    - metadata:
        name: nginx-persistent-storage # pvc名称
      spec:
        storageClassName: managed-nfs-storage # 指定StorageClass名称
        accessModes: ["ReadWriteMany"]
        resources:
          requests:
            storage: 100Mi

创建该StatefulSet,观察pod的创建过程:

可以看到,pod的创建是严格one by one的,因为我们定义的StatefulSet的名称位nginx-sc,所以Pod的名称分别位nginx-sc-0、nginx-sc-1和nginx-sc-2。

查看对应的PVC和PV:

状态为Bound。

删除Pod,再次观察Pod的创建过程:

可以看到顺序性是严格保证的。

进入到Pod内部,查看其hostname:

hostname和pod名称一致。

到192.168.33.13的/nfs目录下可以看到挂载了三个nginx目录:

Kubernetes StorageClass实践


2019年11月26日 15:45:38   1,601 次浏览

手动创建PV不仅繁琐,还可能造成资源浪费。比如某个PV定义的存储空间为10Gi,该PV被某个声明需要8Gi内存的PVC绑定上了,这时候该PV处于Bound状态,无法再和别的PVC进行绑定,PV上剩下的2Gi内存实际上浪费的。StorageClass可以根据PVC的声明,动态创建对应的PV,这样不仅省去了创建PV的过程,还实现了存储资源的动态供应。

StorageClass构成

StorageClass的定义主要包括名称、后端存储的提供者(provisioner)和后端存储的相关参数配置。StorageClass一旦被创建出来,则将无法修改。如需更改,则只能删除原StorageClass的定义重建。

举个简单的StorageClass配置:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: managed-nfs-storage # StorageClass名称
provisioner: fuseim.pri/ifs  # 指定具体存储的提供者
parameters: # 后端存储相关参数配置
  archiveOnDelete: "false"

不同的StorageClass主要区别在于:不同的存储提供者需要填写不同的参数配置,下面实现个NFS作为动态StorageClass存储的例子。

基于NFS存储类型的实践

NFS环境在上一节已经搭建好了,IP为192.168.33.13,路径为/nfs。

在master节点上克隆相关代码:

git clone https://github.com/kubernetes-incubator/external-storage.git

切换到external-storage/nfs-client/deploy目录

cd external-storage/nfs-client/deploy

创建RBAC:

kubectl create -f rbac.yml

接着部署NFS Client Provisioner,部署前修改deployment.yml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-client-provisioner
  labels:
    app: nfs-client-provisioner
  # replace with namespace where provisioner is deployed
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nfs-client-provisioner
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: nfs-client-provisioner
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccountName: nfs-client-provisioner
      containers:
        - name: nfs-client-provisioner
          image: quay.io/external_storage/nfs-client-provisioner:latest
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: mrbird.cc/nfs # 名称随你定义
            - name: NFS_SERVER
              value: 192.168.33.13 # NFS 服务IP
            - name: NFS_PATH
              value: /nfs # NFS 目录
      volumes:
        - name: nfs-client-root
          nfs:
            server: 192.168.33.13 # NFS 服务IP
            path: /nfs # NFS 目录

然后创建该deployment:

kubectl create -f deployment.yml

修改class.yaml配置:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: managed-nfs-storage # StorageClass名称,随你定义
provisioner: mrbird.cc/nfs # 和上面deployment里定义的一致
parameters:
  archiveOnDelete: "false"

创建这个StorageClass:

kubectl create -f class.yml

创建PVC,PVC的定义对应test-claim.yml:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: test-claim
spec:
  storageClassName: managed-nfs-storage # 指定StorageClass名称,即我们上面定义的StorageClass
  accessModes:
    - ReadWriteMany
  resources: 
    requests:
      storage: 1Mi

创建这个PVC:

kubectl create -f test-claim.yml

状态为Bound,说明已经成功绑定上了存储。

查看PV,会看到系统自动创建了PV:

自动创建的PV名称为pvc-3b8c5364-aadb-4b07-a3b5-fddad280fc98。

最后,修改test-pod.yml配置:

kind: Pod
apiVersion: v1
metadata:
  name: test-pod
spec:
  containers:
  - name: test-pod
    image: busybox # 修改为这个,原先的镜像地址需要科学上网
    command:
      - "/bin/sh"
    args:
      - "-c"
      - "touch /mnt/SUCCESS && exit 0 || exit 1"
    volumeMounts:
      - name: nfs-pvc
        mountPath: "/mnt"
  restartPolicy: "Never"
  volumes:
    - name: nfs-pvc
      persistentVolumeClaim:
        claimName: test-claim # 指定PVC名称,对应上面创建的PVC

该Pod主要就是通过busybox在/mnt目录下创建了个SUCCESS文件。

创建该Pod:

kubectl create -f test-pod.yml

状态为Completed,说明busybox成功执行完了命令并结束了。如果一切顺利的话,在192.168.33.13的/nfs目录下会看到busybox pod创建的SUCCESS文件:

可以看到/nfs目录下新增了一个目录,目录名称格式为:[namespace]-[pvc名称]-[pv名称]。

我们还可以玩一下另一个测试,新建一个test-pod-rc.yml文件:

apiVersion: v1
kind: ReplicationController
metadata:
  name: busybox
spec:
  replicas: 2 # 副本数为2,为了测试共享存储
  selector:
    name: busybox
  template:
    metadata:
      labels:
        name: busybox
    spec:
      containers:
        - image: busybox
          command:
            - sh
            - -c
            - 'while true; do sleep $(($RANDOM % 5 + 5)); done'
          imagePullPolicy: IfNotPresent
          name: busybox
          volumeMounts:
            - name: nfs
              mountPath: "/mnt"
      volumes:
        - name: nfs
          persistentVolumeClaim:
            claimName: test-claim # 指定PVC名称,对应上面创建的PVC

上面busybox做的事情很简单,就是无限期休眠。创建该rc:

可以看到,pod分别部署到了node1和node2上。到node1节点上,进入busybox容器内部的/mnt目录,在该目录下创建一个hello文件:

然后到node2节点上,进入busybox容器内部/mnt目录,观察刚刚在node1节点busybox容器内部创建的hello文件是否已经同步过来了:

可以看到,文件同步成功。并且前面例子创建SUCCESS也同步过来了,这是因为它们指定了同一个PVC。

回到NFS服务器的/nfs目录下,可以看到刚刚创建的hello文件: