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

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

介绍

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

Skaffold的特点:

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

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

未分类

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

未分类

使用

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

安装

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

1. skaffold

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

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

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

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

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

2. Kubernetes集群

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

3. kubectl

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

4. Docker

这个应该不用多说了吧?

5. Docker 镜像仓库

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

开发

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

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

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

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

0 directories, 5 files

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

package main
import (
    "fmt"
    "time"
)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

package main
import (
    "fmt"
    "time"
)

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

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

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

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

总结

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

参考资料

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

在Kubernetes上运行高可用的WordPress和MySQL

WordPress是用于编辑和发布Web内容的主流平台。在本教程中,我将逐步介绍如何使用Kubernetes来构建高可用性(HA)WordPress部署。

WordPress由两个主要组件组成:WordPress PHP服务器和用于存储用户信息、帖子和网站数据的数据库。我们需要让整个应用程序中这两个组件在高可用的同时都具备容错能力。

在硬件和地址发生变化的时候,运行高可用服务可能会很困难:非常难维护。借助Kubernetes以及其强大的网络组件,我们可以部署高可用的WordPress站点和MySQL数据库,而无需(几乎无需)输入单个IP地址。

在本教程中,我将向你展示如何在Kubernetes中创建存储类、服务、配置映射和集合,如何运行高可用MySQL,以及如何将高可用WordPress集群挂载到数据库服务上。如果你还没有Kubernetes集群,你可以在Amazon、Google或者Azure上轻松找到并且启动它们,或者在任意的服务器上使用Rancher Kubernetes Engine (RKE)

架构概述

现在我来简要介绍一下我们将要使用的技术及其功能:
WordPress应用程序文件的存储:具有GCE持久性磁盘备份的NFS存储
数据库集群:带有用于奇偶校验的xtrabackup的MySQL
应用程序级别:挂载到NFS存储的WordPress DockerHub映像
负载均衡和网络:基于Kubernetes的负载均衡器和服务网络

该体系架构如下所示:

未分类

在K8s中创建存储类、服务和配置映射

在Kubernetes中,状态集提供了一种定义pod初始化顺序的方法。我们将使用一个有状态的MySQL集合,因为它能确保我们的数据节点有足够的时间在启动时复制先前pods中的记录。我们配置这个状态集的方式可以让MySQL主机在其他附属机器之前先启动,因此当我们扩展时,可以直接从主机将克隆发送到附属机器上。

首先,我们需要创建一个持久卷存储类和配置映射,以根据需要应用主从配置。我们使用持久卷,避免数据库中的数据受限于集群中任何特定的pods。这种方式可以避免数据库在MySQL主机pod丢失的情况下丢失数据,当主机pod丢失时,它可以重新连接到带xtrabackup的附属机器,并将数据从附属机器拷贝到主机中。MySQL的复制负责主机-附属的复制,而xtrabackup负责附属-主机的复制。

要动态分配持久卷,我们使用GCE持久磁盘创建存储类。不过,Kubernetes提供了各种持久性卷的存储方案:

# storage-class.yamlkind: StorageClassapiVersion: storage.k8s.io/v1metadata:
 name: slowprovisioner: kubernetes.io/gce-pdparameters:
 type: pd-standard  zone: us-central1-a

创建类,并且使用指令:$ kubectl create -f storage-class.yaml部署它。

接下来,我们将创建configmap,它指定了一些在MySQL配置文件中设置的变量。这些不同的配置由pod本身选择有关,但它们也为我们提供了一种便捷的方式来管理潜在的配置变量。

创建名为mysql-configmap.yaml的YAML文件来处理配置,如下:

# mysql-configmap.yamlapiVersion: v1kind: ConfigMapmetadata:
 name: mysql  labels:
   app: mysqldata:
 master.cnf: |    # Apply this config only on the master.
   [mysqld]
   log-bin
   skip-host-cache
   skip-name-resolve  slave.cnf: |    # Apply this config only on slaves.
   [mysqld]
   skip-host-cache
   skip-name-resolve

创建configmap并使用指令:$ kubectl create -f mysql-configmap.yaml
来部署它。

接下来我们要设置服务以便MySQL pods可以互相通信,并且我们的WordPress pod可以使用mysql-services.yaml与MySQL通信。这也为MySQL服务启动了服务负载均衡器。

# mysql-services.yaml# Headless service for stable DNS entries of StatefulSet members.apiVersion: v1kind: Servicemetadata:
 name: mysql  labels:
   app: mysqlspec:
 ports:
 - name: mysql    port: 3306  clusterIP: None  selector:
   app: mysql

通过此服务声明,我们就为实现一个多写入、多读取的MySQL实例集群奠定了基础。这种配置是必要的,每个WordPress实例都可能写入数据库,所以每个节点都必须准备好读写。

执行命令 $ kubectl create -f mysql-services.yaml来创建上述的服务。

到这为止,我们创建了卷声明存储类,它将持久磁盘交给所有请求它们的容器,我们配置了configmap,在MySQL配置文件中设置了一些变量,并且我们配置了一个网络层服务,负责对MySQL服务器请求的负载均衡。上面说的这些只是准备有状态集的框架, MySQL服务器实际在哪里运行,我们接下来将继续探讨。

配置有状态集的MySQL

本节中,我们将编写一个YAML配置文件应用于使用了状态集的MySQL实例。

我们先定义我们的状态集:

1、创建三个pods并将它们注册到MySQL服务上。
2、按照下列模版定义每个pod:

  • 为主机MySQL服务器创建初始化容器,命名为init-mysql.
  • 给这个容器使用mysql:5.7镜像
  • 运行一个bash脚本来启动xtrabackup
  • 为配置文件和configmap挂载两个新卷

3、为主机MySQL服务器创建初始化容器,命名为clone-mysql.

  • 为该容器使用Google Cloud Registry的xtrabackup:1.0镜像
  • 运行bash脚本来克隆上一个同级的现有xtrabackups
  • 为数据和配置文件挂在两个新卷
  • 该容器有效地托管克隆的数据,便于新的附属容器可以获取它

4、为附属MySQL服务器创建基本容器

  • 创建一个MySQL附属容器,配置它连接到MySQL主机
  • 创建附属xtrabackup容器,配置它连接到xtrabackup主机

5、创建一个卷声明模板来描述每个卷,每个卷是一个10GB的持久磁盘
下面的配置文件定义了MySQL集群的主节点和附属节点的行为,提供了运行附属客户端的bash配置,并确保在克隆之前主节点能够正常运行。附属节点和主节点分别获得他们自己的10GB卷,这是他们在我们之前定义的持久卷存储类中请求的。

apiVersion: apps/v1beta1kind: StatefulSetmetadata:
 name: mysqlspec:
 selector:
   matchLabels:
     app: mysql  serviceName: mysql  replicas: 3  template:
   metadata:
     labels:
       app: mysql    spec:
     initContainers:
     - name: init-mysql        image: mysql:5.7        command:
       - bash        - "-c"
       - |
         set -ex          # Generate mysql server-id from pod ordinal index.
         [[ `hostname` =~ -([0-9]+)$ ]] || exit 1
         ordinal=${BASH_REMATCH[1]}
         echo [mysqld] > /mnt/conf.d/server-id.cnf          # Add an offset to avoid reserved server-id=0 value.
         echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf          # Copy appropriate conf.d files from config-map to emptyDir.
         if [[ $ordinal -eq 0 ]]; then
           cp /mnt/config-map/master.cnf /mnt/conf.d/
         else
           cp /mnt/config-map/slave.cnf /mnt/conf.d/
         fi        volumeMounts:
       - name: conf          mountPath: /mnt/conf.d        - name: config-map          mountPath: /mnt/config-map      - name: clone-mysql        image: gcr.io/google-samples/xtrabackup:1.0        command:
       - bash        - "-c"
       - |
         set -ex          # Skip the clone if data already exists.
         [[ -d /var/lib/mysql/mysql ]] && exit 0          # Skip the clone on master (ordinal index 0).
         [[ `hostname` =~ -([0-9]+)$ ]] || exit 1
         ordinal=${BASH_REMATCH[1]}
         [[ $ordinal -eq 0 ]] && exit 0          # Clone data from previous peer.
         ncat --recv-only mysql-$(($ordinal-1)).mysql 3307 | xbstream -x -C /var/lib/mysql          # Prepare the backup.
         xtrabackup --prepare --target-dir=/var/lib/mysql        volumeMounts:
       - name: data          mountPath: /var/lib/mysql          subPath: mysql        - name: conf          mountPath: /etc/mysql/conf.d      containers:
     - name: mysql        image: mysql:5.7        env:
       - name: MYSQL_ALLOW_EMPTY_PASSWORD          value: "1"
       ports:
       - name: mysql          containerPort: 3306        volumeMounts:
       - name: data          mountPath: /var/lib/mysql          subPath: mysql        - name: conf          mountPath: /etc/mysql/conf.d        resources:
         requests:
           cpu: 500m            memory: 1Gi        livenessProbe:
         exec:
           command: ["mysqladmin", "ping"]
         initialDelaySeconds: 30          periodSeconds: 10          timeoutSeconds: 5        readinessProbe:
         exec:
           # Check we can execute queries over TCP (skip-networking is off).
           command: ["mysql", "-h", "127.0.0.1", "-e", "SELECT 1"]
         initialDelaySeconds: 5          periodSeconds: 2          timeoutSeconds: 1      - name: xtrabackup        image: gcr.io/google-samples/xtrabackup:1.0        ports:
       - name: xtrabackup          containerPort: 3307        command:
       - bash        - "-c"
       - |
         set -ex
         cd /var/lib/mysql          # Determine binlog position of cloned data, if any.
         if [[ -f xtrabackup_slave_info ]]; then            # XtraBackup already generated a partial "CHANGE MASTER TO" query
           # because we're cloning from an existing slave.
           mv xtrabackup_slave_info change_master_to.sql.in            # Ignore xtrabackup_binlog_info in this case (it's useless).
           rm -f xtrabackup_binlog_info
         elif [[ -f xtrabackup_binlog_info ]]; then            # We're cloning directly from master. Parse binlog position.
           [[ `cat xtrabackup_binlog_info` =~ ^(.*?)[[:space:]]+(.*?)$ ]] || exit 1
           rm xtrabackup_binlog_info
           echo "CHANGE MASTER TO MASTER_LOG_FILE='${BASH_REMATCH[1]}',                  MASTER_LOG_POS=${BASH_REMATCH[2]}" > change_master_to.sql.in
         fi          # Check if we need to complete a clone by starting replication.
         if [[ -f change_master_to.sql.in ]]; then
           echo "Waiting for mysqld to be ready (accepting connections)"
           until mysql -h 127.0.0.1 -e "SELECT 1"; do sleep 1; done

           echo "Initializing replication from clone position"
           # In case of container restart, attempt this at-most-once.
           mv change_master_to.sql.in change_master_to.sql.orig
           mysql -h 127.0.0.1 <<EOF
         $(<change_master_to.sql.orig),
           MASTER_HOST='mysql-0.mysql',
           MASTER_USER='root',
           MASTER_PASSWORD='',
           MASTER_CONNECT_RETRY=10;
         START SLAVE;
         EOF
         fi          # Start a server to send backups when requested by peers.
         exec ncat --listen --keep-open --send-only --max-conns=1 3307 -c             "xtrabackup --backup --slave-info --stream=xbstream --host=127.0.0.1 --user=root"
       volumeMounts:
       - name: data          mountPath: /var/lib/mysql          subPath: mysql
       - name: conf          mountPath: /etc/mysql/conf.d        resources:
         requests:
           cpu: 100m            memory: 100Mi      volumes:
     - name: conf        emptyDir: {}
     - name: config-map        configMap:
         name: mysql  volumeClaimTemplates:
 - metadata:
     name: data    spec:
     accessModes: ["ReadWriteOnce"]
     resources:
       requests:
         storage: 10Gi

将该文件存为mysql-statefulset.yaml,输入kubectl=”” create=”” -f=”” mysql-statefulset.yaml并让kubernetes部署你的数据库。
现在当你调用$=”” kubectl=”” get=”” pods,你应该看到3个pods启动或者准备好,其中每个pod上都有两个容器。主节点pod表示为mysql-0,而附属的pods为mysql-1和mysql-2.让pods执行几分钟来确保xtrabackup服务在pod之间正确同步,然后进行wordpress的部署。
您可以检查单个容器的日志来确认没有错误消息抛出。 查看日志的命令为$=”” logs=”” <container_name=””>

主节点xtrabackup容器应显示来自附属的两个连接,并且日志中不应该出现任何错误。

部署高可用的WordPress

整个过程的最后一步是将我们的WordPress pods部署到集群上。为此我们希望为WordPress的服务和部署进行定义。

为了让WordPress实现高可用,我们希望每个容器运行时都是完全可替换的,这意味着我们可以终止一个,启动另一个而不需要对数据或服务可用性进行修改。我们也希望能够容忍至少一个容器的失误,有一个冗余的容器负责处理slack。

WordPress将重要的站点相关数据存储在应用程序目录/var/www/html中。对于要为同一站点提供服务的两个WordPress实例,该文件夹必须包含相同的数据。

当运行高可用WordPress时,我们需要在实例之间共享/var/www/html文件夹,因此我们定义一个NGS服务作为这些卷的挂载点。
下面是设置NFS服务的配置,我提供了纯英文的版本:

# nfs.yaml# Define the persistent volume claimapiVersion: v1kind: PersistentVolumeClaimmetadata:
name: nfs  labels:
  demo: nfs  annotations:
  volume.alpha.kubernetes.io/storage-class: anyspec:
accessModes: [ "ReadWriteOnce" ]
resources:
  requests:
    storage: 200Gi---# Define the Replication ControllerapiVersion: v1kind: ReplicationControllermetadata:
name: nfs-serverspec:
replicas: 1  selector:
  role: nfs-server  template:
  metadata:
    labels:
      role: nfs-server    spec:
    containers:
    - name: nfs-server        image: gcr.io/google_containers/volume-nfs:0.8        ports:
        - name: nfs            containerPort: 2049          - name: mountd            containerPort: 20048          - name: rpcbind            containerPort: 111        securityContext:
        privileged: true        volumeMounts:
        - mountPath: /exports            name: nfs-pvc      volumes:
      - name: nfs-pvc          persistentVolumeClaim:
          claimName: nfs---# Define the Servicekind: ServiceapiVersion: v1metadata:
name: nfs-serverspec:
ports:
  - name: nfs      port: 2049    - name: mountd      port: 20048    - name: rpcbind      port: 111  selector:
  role: nfs-server

使用指令$ kubectl create -f nfs.yaml部署NFS服务。现在,我们需要运行$ kubectl describe services nfs-server获得IP地址,这在后面会用到。

注意:将来,我们可以使用服务名称讲这些绑定在一起,但现在你需要对IP地址进行硬编码。

# wordpress.yamlapiVersion: v1kind: Servicemetadata:
 name: wordpress  labels:
   app: wordpressspec:
 ports:
   - port: 80  selector:
   app: wordpress    tier: frontend  type: LoadBalancer---apiVersion: v1kind: PersistentVolumemetadata:
 name: nfsspec:
 capacity:
   storage: 20G  accessModes:
   - ReadWriteMany  nfs:
   # FIXME: use the right IP
   server: <ip of="" the="" nfs="" service="">    path: "/"---apiVersion: v1kind: PersistentVolumeClaimmetadata:
 name: nfsspec:
 accessModes:
   - ReadWriteMany  storageClassName: ""
 resources:
   requests:
     storage: 20G---apiVersion: apps/v1beta1 # for versions before 1.8.0 use apps/v1beta1kind: Deploymentmetadata:
 name: wordpress  labels:
   app: wordpressspec:
 selector:
   matchLabels:
     app: wordpress      tier: frontend  strategy:
   type: Recreate  template:
   metadata:
     labels:
       app: wordpress        tier: frontend    spec:
     containers:
     - image: wordpress:4.9-apache        name: wordpress        env:
       - name: WORDPRESS_DB_HOST          value: mysql        - name: WORDPRESS_DB_PASSWORD          value: ""
       ports:
       - containerPort: 80          name: wordpress        volumeMounts:
       - name: wordpress-persistent-storage          mountPath: /var/www/html      volumes:
     - name: wordpress-persistent-storage        persistentVolumeClaim:
           claimName: nfs

我们现在创建了一个持久卷声明,和我们之前创建的NFS服务建立映射,然后将卷附加到WordPress pod上,即/var/www/html根目录,这也是WordPress安装的地方。这里保留了集群中WordPress pods的所有安装和环境。有了这些配置,我们就可以对任何WordPress节点进行启动和拆除,而数据能够留下来。因为NFS服务需要不断使用物理卷,该卷将保留下来,并且不会被回收或错误分配。

使用指令$ kubectl create -f wordpress.yaml部署WordPress实例。默认部署只会运行一个WordPress实例,可以使用指令$ kubectl scale –replicas= deployment/wordpress扩展WordPress实例数量。

要获得WordPress服务负载均衡器的地址,你需要输入$ kubectl get services wordpress
并从结果中获取EXTERNAL-IP字段来导航到WordPress。

弹性测试

OK,现在我们已经部署好了服务,那我们来拆除一下它们,看看我们的高可用架构如何处理这些混乱。在这种部署方式中,唯一剩下的单点故障就是NFS服务(原因总结在文末结论中)。你应该能够测试其他任何的服务来了解应用程序是如何响应的。现在我已经启动了WordPress服务的三个副本,以及MySQL服务中的一个主两个附属节点。

首先,我们先kill掉其他而只留下一个WordPress节点,来看看应用如何响应:$ kubectl scale –replicas=1 deployment/wordpress现在我们应该看到WordPress部署的pod数量有所下降。$ kubectl get pods应该能看到WordPress pods的运行变成了1/1。

点击WordPress服务IP,我们将看到与之前一样的站点和数据库。如果要扩展复原,可以使用$ kubectl scale –replicas=3 deployment/wordpress再一次,我们可以看到数据包留在了三个实例中。

下面测试MySQL的状态集,我们使用指令缩小备份的数量:$ kubectl scale statefulsets mysql –replicas=1我们会看到两个附属从该实例中丢失,如果主节点在此时丢失,它所保存的数据将保存在GCE持久磁盘上。不过就必须手动从磁盘恢复数据。

如果所有三个MySQL节点都关闭了,当新节点出现时就无法复制。但是,如果一个主节点发生故障,一个新的主节点就会自动启动,并且通过xtrabackup重新配置来自附属节点的数据。因此,在运行生产数据库时,我不建议以小于3的复制系数来运行。在结论段中,我们会谈谈针对有状态数据有什么更好的解决方案,因为Kubernetes并非真正是为状态设计的。

结论和建议

到现在为止,你已经完成了在Kubernetes构建并部署高可用WordPress和MySQL的安装!

不过尽管取得了这样的效果,你的研究之旅可能还远没有结束。可能你还没注意到,我们的安装仍然存在着单点故障:NFS服务器在WordPress pods之间共享/var/www/html目录。这项服务代表了单点故障,因为如果它没有运行,在使用它的pods上html目录就会丢失。教程中我们为服务器选择了非常稳定的镜像,可以在生产环境中使用,但对于真正的生产部署,你可以考虑使用GlusterFS对WordPress实例共享的目录开启多读多写。

这个过程涉及在Kubernetes上运行分布式存储集群,实际上这不是Kubernetes构建的,因此尽管它运行良好,但不是长期部署的理想选择。

对于数据库,我个人建议使用托管的关系数据库服务来托管MySQL实例,因为无论是Google的CloudSQL还是AWS的RDS,它们都以更合理的价格提供高可用和冗余处理,并且不需担心数据的完整性。Kuberntes并不是围绕有状态的应用程序设计的,任何建立在其中的状态更多都是事后考虑。目前有大量的解决方案可以在选择数据库服务时提供所需的保证。

也就是说,上面介绍的是一种理想的流程,由Kubernetes教程、web中找到的例子创建一个有关联的现实的Kubernetes例子,并且包含了Kubernetes 1.8.x中所有的新特性。

我希望通过这份指南,你能在部署WordPress和MySQL时获得一些惊喜的体验,当然,更希望你的运行一切正常。

Kubernetes之路 2 – 利用LXCFS提升容器资源可见性

未分类

这是本系列的第2篇内容,将介绍在Docker和Kubernetes环境中解决遗留应用无法识别容器资源限制的问题。

Linuxs利用Cgroup实现了对容器的资源限制,但在容器内部依然缺省挂载了宿主机上的procfs的/proc目录,其包含如:meminfo, cpuinfo,stat, uptime等资源信息。一些监控工具如free/top或遗留应用还依赖上述文件内容获取资源配置和使用情况。当它们在容器中运行时,就会把宿主机的资源状态读取出来,引起错误和不便。

LXCFS简介

社区中常见的做法是利用 lxcfs来提供容器中的资源可见性。lxcfs 是一个开源的FUSE(用户态文件系统)实现来支持LXC容器,它也可以支持Docker容器。

LXCFS通过用户态文件系统,在容器中提供下列 procfs 的文件。

/proc/cpuinfo
/proc/diskstats
/proc/meminfo
/proc/stat
/proc/swaps
/proc/uptime

LXCFS的示意图如下

未分类

比如,把宿主机的 /var/lib/lxcfs/proc/memoinfo 文件挂载到Docker容器的/proc/meminfo位置后。容器中进程读取相应文件内容时,LXCFS的FUSE实现会从容器对应的Cgroup中读取正确的内存限制。从而使得应用获得正确的资源约束设定。

Docker环境下LXCFS使用

注:

  • 本文采用CentOS 7.4作为测试环境,并已经开启FUSE模块支持。
  • Docker for Mac/Minikube等开发环境由于采用高度剪裁过的操作系统,无法支持FUSE,并运行LXCFS进行测试。

安装 lxcfs 的RPM包

wget https://copr-be.cloud.fedoraproject.org/results/ganto/lxd/epel-7-x86_64/00486278-lxcfs/lxcfs-2.0.5-3.el7.centos.x86_64.rpm
yum install lxcfs-2.0.5-3.el7.centos.x86_64.rpm  

启动 lxcfs

lxcfs /var/lib/lxcfs &  

测试

$docker run -it -m 256m 
      -v /var/lib/lxcfs/proc/cpuinfo:/proc/cpuinfo:rw 
      -v /var/lib/lxcfs/proc/diskstats:/proc/diskstats:rw 
      -v /var/lib/lxcfs/proc/meminfo:/proc/meminfo:rw 
      -v /var/lib/lxcfs/proc/stat:/proc/stat:rw 
      -v /var/lib/lxcfs/proc/swaps:/proc/swaps:rw 
      -v /var/lib/lxcfs/proc/uptime:/proc/uptime:rw 
      ubuntu:16.04 /bin/bash

root@f4a2a01e61cd:/# free
              total        used        free      shared  buff/cache   available
Mem:         262144         708      261436        2364           0      261436
Swap:             0           0           0

我们可以看到total的内存为256MB,配置已经生效。

lxcfs 的 Kubernetes实践

一些同学问过如何在Kubernetes集群环境中使用lxcfs,我们将给大家一个示例方法供参考。

首先我们要在集群节点上安装并启动lxcfs,我们将用Kubernetes的方式,用利用容器和DaemonSet方式来运行 lxcfs FUSE文件系统。

本文所有示例代码可以通过以下地址从Github上获得

git clone https://github.com/denverdino/lxcfs-initializer
cd lxcfs-initializer

其manifest文件如下

apiVersion: apps/v1beta2
kind: DaemonSet
metadata:
  name: lxcfs
  labels:
    app: lxcfs
spec:
  selector:
    matchLabels:
      app: lxcfs
  template:
    metadata:
      labels:
        app: lxcfs
    spec:
      hostPID: true
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      containers:
      - name: lxcfs
        image: registry.cn-hangzhou.aliyuncs.com/denverdino/lxcfs:2.0.8
        imagePullPolicy: Always
        securityContext:
          privileged: true
        volumeMounts:
        - name: rootfs
          mountPath: /host
      volumes:
      - name: rootfs
        hostPath:
          path: /

注: 由于 lxcfs FUSE需要共享系统的PID名空间以及需要特权模式,所有我们配置了相应的容器启动参数。

可以通过如下命令在所有集群节点上自动安装、部署完成 lxcfs,是不是很简单?:-)

kubectl create -f lxcfs-daemonset.yaml

那么如何在Kubernetes中使用 lxcfs 呢?和上文一样,我们可以在Pod的定义中添加对 /proc 下面文件的 volume(文件卷)和对 volumeMounts(文件卷挂载)定义。然而这就让K8S的应用部署文件变得比较复杂,有没有办法让系统自动完成相应文件的挂载呢?

Kubernetes提供了 Initializer 扩展机制,可以用于对资源创建进行拦截和注入处理,我们可以借助它优雅地完成对lxcfs文件的自动化挂载。

注: 阿里云Kubernetes集群,已经默认开启了对 Initializer 的支持,如果是在自建集群上进行测试请参见文档开启相应功能

其 manifest 文件如下

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: lxcfs-initializer-default
  namespace: default
rules:
- apiGroups: ["*"]
  resources: ["deployments"]
  verbs: ["initialize", "patch", "watch", "list"]
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: lxcfs-initializer-service-account
  namespace: default
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: lxcfs-initializer-role-binding
subjects:
- kind: ServiceAccount
  name: lxcfs-initializer-service-account
  namespace: default
roleRef:
  kind: ClusterRole
  name: lxcfs-initializer-default
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  initializers:
    pending: []
  labels:
    app: lxcfs-initializer
  name: lxcfs-initializer
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: lxcfs-initializer
      name: lxcfs-initializer
    spec:
      serviceAccountName: lxcfs-initializer-service-account
      containers:
        - name: lxcfs-initializer
          image: registry.cn-hangzhou.aliyuncs.com/denverdino/lxcfs-initializer:0.0.2
          imagePullPolicy: Always
          args:
            - "-annotation=initializer.kubernetes.io/lxcfs"
            - "-require-annotation=true"
---
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: InitializerConfiguration
metadata:
  name: lxcfs.initializer
initializers:
  - name: lxcfs.initializer.kubernetes.io
    rules:
      - apiGroups:
          - "*"
        apiVersions:
          - "*"
        resources:
          - deployments

注: 这是一个典型的 Initializer 部署描述,首先我们创建了service account lxcfs-initializer-service-account,并对其授权了 “deployments” 资源的查找、更改等权限。然后我们部署了一个名为 “lxcfs-initializer” 的Initializer,利用上述SA启动一个容器来处理对 “deployments” 资源的创建,如果deployment中包含 initializer.kubernetes.io/lxcfs为true的注释,就会对该应用中容器进行文件挂载

我们可以执行如下命令,部署完成之后就可以愉快地玩耍了

kubectl apply -f lxcfs-initializer.yaml

下面我们部署一个简单的Apache应用,为其分配256MB内存,并且声明了如下注释 “initializer.kubernetes.io/lxcfs”: “true”

其manifest文件如下

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  annotations:
    "initializer.kubernetes.io/lxcfs": "true"
  labels:
    app: web
  name: web
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: web
      name: web
    spec:
      containers:
        - name: web
          image: httpd:2
          imagePullPolicy: Always
          resources:
            requests:
              memory: "256Mi"
              cpu: "500m"
            limits:
              memory: "256Mi"
              cpu: "500m"

我们可以用如下方式进行部署和测试

$ kubectl create -f web.yaml 
deployment "web" created

$ kubectl get pod
NAME                                 READY     STATUS    RESTARTS   AGE
web-7f6bc6797c-rb9sk                 1/1       Running   0          32s
$ kubectl exec web-7f6bc6797c-rb9sk free
             total       used       free     shared    buffers     cached
Mem:        262144       2876     259268       2292          0        304
-/+ buffers/cache:       2572     259572
Swap:            0          0          0

我们可以看到 free 命令返回的 total memory 就是我们设置的容器资源容量。

我们可以检查上述Pod的配置,果然相关的 procfs 文件都已经挂载正确

$ kubectl describe pod web-7f6bc6797c-rb9sk
...
    Mounts:
      /proc/cpuinfo from lxcfs-proc-cpuinfo (rw)
      /proc/diskstats from lxcfs-proc-diskstats (rw)
      /proc/meminfo from lxcfs-proc-meminfo (rw)
      /proc/stat from lxcfs-proc-stat (rw)
...

在Kubernetes中,还可以通过 Preset 实现类似的功能,篇幅有限。本文不再赘述了。

总结

本文介绍了通过 lxcfs 提供容器资源可见性的方法,可以帮助一些遗留系统更好的识别容器运行时的资源限制。

同时,在本文中我们介绍了利用容器和DaemonSet的方式部署lxcfs FUSE,这不但极大简化了部署。也可以方便地利用Kubernetes自身的容器管理能力,支持lxcfs进程失效时自动恢复,在集群伸缩时也可以保证节点部署的一致性。这个技巧对于其他类似的监控或者系统扩展都是适用的。

另外我们介绍了利用Kubernetes的扩展机制 Initializer,实现对 lxcfs 文件的自动化挂载。整个过程对于应用部署人员是透明的,可以极大简化运维复杂度。同时利用类似的方法,我们可以灵活地定制应用部署的行为,满足业务的特殊要求。

阿里云Kubernetes服务 全球首批通过Kubernetes一致性认证,简化了Kubernetes集群生命周期管理,内置了与阿里云产品集成,也将进一步简化Kubernetes的开发者体验,帮助用户关注云端应用价值创新。

Kubernetes之路 1 – Java应用资源限制的迷思

未分类

随着容器技术的成熟,越来越多的企业客户在企业中选择Docker和Kubernetes作为应用平台的基础。然而在实践过程中,还会遇到很多具体问题。本系列文章会记录阿里云容器服务团队在支持客户中的一些心得体会和最佳实践。我们也欢迎您通过邮件和钉钉群和我们联系,分享您的思路和遇到的问题。

在对Java应用容器化部署的过程中,有些同学反映:自己设置了容器的资源限制,但是Java应用容器在运行中还是会莫名奇妙地被OOM Killer干掉。

这背后一个非常常见的原因是:没有正确设置容器的资源限制以及对应的JVM的堆空间大小。

我们拿一个tomcat应用为例,其实例代码和Kubernetes部署文件可以从Github中获得。

git clone https://github.com/denverdino/system-info
cd system-info`

下面是一个Kubernetes的Pod的定义描述:

  • Pod中的app是一个初始化容器,负责把一个JSP应用拷贝到 tomcat 容器的 “webapps”目录下。注: 镜像中JSP应用index.jsp用于显示JVM和系统资源信息。
  • tomcat 容器会保持运行,而且我们限制了容器最大的内存用量为256MB内存。
apiVersion: v1
kind: Pod
metadata:
  name: test
spec:
  initContainers:
  - image: registry.cn-hangzhou.aliyuncs.com/denverdino/system-info
    name: app
    imagePullPolicy: IfNotPresent
    command:
      - "cp"
      - "-r"
      - "/system-info"
      - "/app"
    volumeMounts:
    - mountPath: /app
      name: app-volume
  containers:
  - image: tomcat:9-jre8
    name: tomcat
    imagePullPolicy: IfNotPresent
    volumeMounts:
    - mountPath: /usr/local/tomcat/webapps
      name: app-volume
    ports:
    - containerPort: 8080
    resources:
      requests:
        memory: "256Mi"
        cpu: "500m"
      limits:
        memory: "256Mi"
        cpu: "500m"
  volumes:
  - name: app-volume
    emptyDir: {}

我们执行如下命令来部署、测试应用

$ kubectl create -f test.yaml
pod "test" created
$ kubectl get pods test
NAME      READY     STATUS    RESTARTS   AGE
test      1/1       Running   0          28s
$ kubectl exec test curl http://localhost:8080/system-info/
...

我们可以看到HTML格式的系统CPU/Memory等信息,我们也可以用 html2text 命令将其转化成为文本格式。

注意:本文是在一个 2C 4G的节点上进行的测试,在不同环境中测试输出的结果会有所不同

$ kubectl exec test curl http://localhost:8080/system-info/ | html2text

Java version     Oracle Corporation 1.8.0_162
Operating system Linux 4.9.64
Server           Apache Tomcat/9.0.6
Memory           Used 29 of 57 MB, Max 878 MB
Physica Memory   3951 MB
CPU Cores        2
                                          **** Memory MXBean ****
Heap Memory Usage     init = 65011712(63488K) used = 19873704(19407K) committed
                      = 65536000(64000K) max = 921174016(899584K)
Non-Heap Memory Usage init = 2555904(2496K) used = 32944912(32172K) committed =
                      33882112(33088K) max = -1(-1K)

我们可以发现,容器中看到的系统内存是 3951MB,而JVM Heap Size最大是 878MB。纳尼?!我们不是设置容器资源的容量为256MB了吗?如果这样,当应用内存的用量超出了256MB,JVM还没对其进行GC,而JVM进程就会被系统直接OOM干掉了。

问题的根源在于:

  • 对于JVM而言,如果没有设置Heap Size,就会按照宿主机环境的内存大小缺省设置自己的最大堆大小。
  • Docker容器利用CGroup对进程使用的资源进行限制,而在容器中的JVM依然会利用宿主机环境的内存大小和CPU核数进行缺省设置,这导致了JVM Heap的错误计算。

类似,JVM缺省的GC、JIT编译线程数量取决于宿主机CPU核数。如果我们在一个节点上运行多个Java应用,即使我们设置了CPU的限制,应用之间依然有可能因为GC线程抢占切换,导致应用性能收到影响。

了解了问题的根源,我们就可以非常简单地解决问题了

解决思路

开启CGroup资源感知

Java社区也关注到这个问题,并在JavaSE8u131+和JDK9 支持了对容器资源限制的自动感知能力 https://blogs.oracle.com/java-platform-group/java-se-support-for-docker-cpu-and-memory-limits

其用法就是添加如下参数

java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap …

我们在上文示例的tomcat容器添加环境变量 “JAVA_OPTS”参数

apiVersion: v1
kind: Pod
metadata:
  name: cgrouptest
spec:
  initContainers:
  - image: registry.cn-hangzhou.aliyuncs.com/denverdino/system-info
    name: app
    imagePullPolicy: IfNotPresent
    command:
      - "cp"
      - "-r"
      - "/system-info"
      - "/app"
    volumeMounts:
    - mountPath: /app
      name: app-volume
  containers:
  - image: tomcat:9-jre8
    name: tomcat
    imagePullPolicy: IfNotPresent
    env:
    - name: JAVA_OPTS
      value: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap"
    volumeMounts:
    - mountPath: /usr/local/tomcat/webapps
      name: app-volume
    ports:
    - containerPort: 8080
    resources:
      requests:
        memory: "256Mi"
        cpu: "500m"
      limits:
        memory: "256Mi"
        cpu: "500m"
  volumes:
  - name: app-volume
    emptyDir: {}

我们部署一个新的Pod,并重复相应的测试

$ kubectl create -f cgroup_test.yaml
pod "cgrouptest" created

$ kubectl exec cgrouptest curl http://localhost:8080/system-info/ | html2txt
Java version     Oracle Corporation 1.8.0_162
Operating system Linux 4.9.64
Server           Apache Tomcat/9.0.6
Memory           Used 23 of 44 MB, Max 112 MB
Physica Memory   3951 MB
CPU Cores        2
                                          **** Memory MXBean ****
Heap Memory Usage     init = 8388608(8192K) used = 25280928(24688K) committed =
                      46661632(45568K) max = 117440512(114688K)
Non-Heap Memory Usage init = 2555904(2496K) used = 31970840(31221K) committed =
                      32768000(32000K) max = -1(-1K)

我们看到JVM最大的Heap大小变成了112MB,这很不错,这样就能保证我们的应用不会轻易被OOM了。随后问题又来了,为什么我们设置了容器最大内存限制是256MB,而JVM只给Heap设置了112MB的最大值呢?

这就涉及到JVM的内存管理的细节了,JVM中的内存消耗包含Heap和Non-Heap两类;类似Class的元信息,JIT编译过的代码,线程堆栈(thread stack),GC需要的内存空间等都属于Non-Heap内存,所以JVM还会根据CGroup的资源限制预留出部分内存给Non Heap,来保障系统的稳定。(在上面的示例中我们可以看到,tomcat启动后Non Heap占用了近32MB的内存)

在最新的JDK 10中,又对JVM在容器中运行做了进一步的优化和增强。

容器内部感知CGroup资源限制

如果无法利用JDK 8/9的新特性,比如还在使用JDK6的老应用,我们还可以在容器内部利用脚本来获取容器的CGroup资源限制,并通过设置JVM的Heap大小。

Docker1.7开始将容器cgroup信息挂载到容器中,所以应用可以从 /sys/fs/cgroup/memory/memory.limit_in_bytes 等文件获取内存、 CPU等设置,在容器的应用启动命令中根据Cgroup配置正确的资源设置 -Xmx, -XX:ParallelGCThreads等参数

在 https://yq.aliyun.com/articles/18037 一文中已经有相应的示例和代码,本文不再赘述

总结

本文分析了Java应用在容器使用中一个常见Heap设置的问题。容器与虚拟机不同,其资源限制通过CGroup来实现。而容器内部进程如果不感知CGroup的限制,就进行内存、CPU分配可能导致资源冲突和问题。

我们可以非常简单地利用JVM的新特性和自定义脚本来正确设置资源限制。这个可以解决绝大多数资源限制的问题。

关于容器应用中资源限制还有一类问题是,一些比较老的监控工具或者free/top等系统命令,在容器中运行时依然会获取到宿主机的CPU和内存,这导致了一些监控工具在容器中运行时无法正常计算资源消耗。社区中常见的做法是利用 lxcfs 来让容器在资源可见性的行为和虚机保持一致,后续文章会介绍其在Kubernetes上的使用方案。

阿里云Kubernetes服务 全球首批通过Kubernetes一致性认证,简化了Kubernetes集群生命周期管理,内置了与阿里云产品集成,也将进一步简化Kubernetes的开发者体验,帮助用户关注云端应用价值创新。

实战Kubernetes动态卷存储(NFS)

之前的《 Kubernetes持久卷实战两部曲》系列中,我们实战了先声明一个存储卷,再使用这个存储卷,这种方式要求每次都要提前申明存储,不是很方便,而动态卷存储不需要提前申明,而是使用时自动申明,今天我们就来一起实战;

原文地址:https://blog.csdn.net/boling_cavalry/article/details/79598905

本章概要

今天的实战,我们要做下列操作:

  1. 准备好NFS服务;
  2. 创建namespace;
  3. 创建NFS服务的provisioner;
  4. 创建存储类StorageClass,与刚刚创建的provisioner绑定;
  5. 创建应用Pod,此应用是个web服务,外部通过HTTP请求将二进制文件上传到服务端,存储在Pod的本地路径,而这个路径已经被挂载到NFS;
  6. 通过客户端上传文件,检查文件是否保存在NFS上;
  7. 将web应用的Pod数扩展到两个,检查动态卷存储是否自动扩展;

网络服务图

本次实战涉及到客户端、K8S、NFS等网络节点,如下图:

未分类

源码下载

您可以在GitHub下载web服务的源码,地址和链接信息如下表所示:

未分类

这个git项目中有多个目录,本次需要其中的三个,如下:

  1. 在K8S上创建Pod,service,StorageClass等资源的时候用到的各种yaml文件,存放在storageclass目录;
  2. 接收客户端上传文件的web应用,源码在k8spvdemo目录;
  3. 上传文件client工程,源码在uploadfileclient目录;

准备好NFS服务

参照 https://blog.csdn.net/boling_cavalry/article/details/79498346 搭建好NFS服务,服务器的IP地址是192.168.238.132,开放的目录/usr/local/work/nfs;

创建namespace

在能够执行kubectl的机器上执行命令kubectl create namespace bolingcavalry,即可创建本次实战要用到的namespace;

创建NFS服务的provisioner

创建provisioner对应的deployment-nfs.yaml脚本:

kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: nfs-client-provisioner
  namespace: bolingcavalry
spec:
  replicas: 1
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      containers:
        - name: nfs-client-provisioner
          image: registry.cn-hangzhou.aliyuncs.com/open-ali/nfs-client-provisioner
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: fuseim.pri/ifs
            - name: NFS_SERVER
              value: 192.168.238.132
            - name: NFS_PATH
              value: /usr/local/work/nfs
      volumes:
        - name: nfs-client-root
          nfs:
            server: 192.168.238.132
            path: /usr/local/work/nfs

在此文件所在目录下执行kubectl create -f deployment-nfs.yaml,即可创建NFS的provisioner的pod,创建成功后,在dashboard页面看到,如下图:

未分类

这里三处需要注意:

1、设置了NFS的共享目录后,记得执行以下命令使设置生效:

systemctl restart rpcbind.service && systemctl restart nfs.service

2、此处用到的镜像是”registry.cn-hangzhou.aliyuncs.com/open-ali/nfs-client-provisioner”,这是我在阿里云的搜索到的,原本打算使用”quay.io/kubernetes_incubator/nfs-client-provisioner:v1”,但是在pull的时候报错:{“error”: “Permission Denied”},在实战过程中用了阿里云的镜像可以正常工作,没有影响;
3、环境变量中有个PROVISIONER_NAME,值为fuseim.pri/ifs,后面的StorageClass就以“fuseim.pri/ifs”作为参数来定位到此Pod作为NFS服务的提供者;

创建存储类StorageClass

1、创建provisioner对应的nfs-class.yaml脚本:

apiVersion: storage.k8s.io/v1beta1
kind: StorageClass
metadata:
  name: managed-nfs-storage
provisioner: fuseim.pri/ifs

2、在此文件所在目录下执行kubectl create -f nfs-class.yaml,即可创建存储类的资源,创建成功后,在dashboard页面看到,如下图:

未分类

创建web应用的Pod

存储已经准备好了,接下来制作一个web服务的应用来使用这个存储,该应用我已经做成镜像,可以直接使用;

1、创建web服务的部署脚本k8spvdemo.yaml:

apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: k8spvdemo
  namespace:  bolingcavalry
spec:
  serviceName: "k8spvdemo"
  replicas: 1
  volumeClaimTemplates:
  - metadata:
      name: test 
      annotations:
        volume.beta.kubernetes.io/storage-class: "managed-nfs-storage"
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 2Gi 
  template:
    metadata:
     labels:
       name: k8spvdemo
    spec:
     containers:
     - name: k8spvdemo
       image: bolingcavalry/k8spvdemo:0.0.1-SNAPSHOT
       volumeMounts:
       - mountPath: "/usr/local/uploadfiles"
         name: test
       tty: true
       ports:
       - containerPort: 8080

2、在k8spvdemo.yaml文件所在目录执行命令kubectl create -f k8spvdemo.yaml,即可创建应用的Pod,如下图:

未分类

3、将应用Pod包裹成Service以便外部访问,创建服务配置文件k8spvdemo-svc.yaml:

apiVersion: v1
kind: Service
metadata:
  name: k8spvdemo
  namespace:  bolingcavalry
spec:
  type: NodePort
  ports:
       - port: 8080
         nodePort: 30010
  selector:
    name: k8spvdemo

4、在k8spvdemo.yaml文件所在目录执行命令kubectl create -f k8spvdemo-svc.yaml,即可创建服务,在dashboard页面看到服务如下图:

未分类

经历了以上四步,就把服务启动起来了,并且使用了NFS,小结出以下几个关键点需要注意:

  • 参数kind: StatefulSet表示使用了有状态资源,因为普通deployment是无法使用StorageClass资源的;
  • web工程中,收到客户端POST上来的文件后,保存在本地的/usr/local/uploadfiles目录,该目录在k8spvdemo.yaml的配置中通过volumeMounts参数挂在到StorageClass上;
  • 在k8spvdemo-svc.yaml的配置中做了端口映射,我的k8s节点机器的IP是192.168.238.130,那么通过192.168.238.130:30010就能访问到Pod服务;
  • 将springboot工程制作成镜像的方法请参考《maven构建docker镜像三部曲》系列文章;
  • 在NFS机器的/usr/local/work/nfs目录下,可以看到新创建的文件夹,如下图:

未分类

  • 在容器的事件中可以看到挂载信息,挂载的PV名称与NFS上创建的文件夹是可以对应起来的,如下图红框3所示:

未分类

服务已经准备好了,接下来试试上传文件,看应用能否正常使用动态卷存储;

运行客户端,上传本地文件到Tomcat

1、创建一个maven工程,pom.xml如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.bolingcavalry</groupId>
    <artifactId>uploadfileclient</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- 指明编译源代码时使用的字符编码,maven编译的时候默认使用的GBK编码, 通过project.build.sourceEncoding属性设置字符编码,告诉maven这个项目使用UTF-8来编译 -->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.3.5</version>
        </dependency>

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpmime</artifactId>
            <version>4.3.5</version>
        </dependency>
    </dependencies>

</project>
  1. 工程中创建一个java类,源码如下:
package com.bolingcavalry;

import org.apache.http.HttpEntity;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import java.io.File;
import java.io.IOException;

/**
 * @Description : 上传文件的类,将本地文件POST到server
 * @Author : [email protected]
 * @Date : 2018-02-24 18:12
 */
public class UploadFileClient {

    /**
     * 文件服务的ULR
     */
    private static final String POST_URL = "http://192.168.238.130:30010//upload";

    /**
     * 要上传的本地文件的完整路径加文件名
     */
    private static final String UPLOAD_FILE_FULLPATH = "D:\temp\201802\21\abc.zip";

    public static void main(String[] args) throws Exception{
        System.out.println("start upload");
        CloseableHttpClient httpclient = HttpClients.createDefault();
        try {
            HttpPost httppost = new HttpPost(POST_URL);

            //基本的配置信息
            RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(200000).setSocketTimeout(200000).build();

            httppost.setConfig(requestConfig);

            //要上传的文件
            FileBody bin = new FileBody(new File(UPLOAD_FILE_FULLPATH));

            //在POST中添加一个字符串请求参数
            StringBody comment = new StringBody("This is comment", ContentType.TEXT_PLAIN);

            HttpEntity reqEntity = MultipartEntityBuilder.create().addPart("file", bin).addPart("comment", comment).build();

            httppost.setEntity(reqEntity);

            System.out.println("executing request " + httppost.getRequestLine());

            //发起POST
            CloseableHttpResponse response = httpclient.execute(httppost);

            try {

                HttpEntity resEntity = response.getEntity();
                if (resEntity != null) {
                    String responseEntityStr = EntityUtils.toString(response.getEntity());
                    System.out.println("response status : " + response.getStatusLine());
                    System.out.println("response content length: " + resEntity.getContentLength());
                    System.out.println("response entity str : " + responseEntityStr);
                }
                EntityUtils.consume(resEntity);
            } finally {
                response.close();
            }
        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                httpclient.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        System.out.println("end upload");
    }
}

以上源码中,有两处要注意:
第一,POST_URL = “http://192.168.238.130:30010//upload”中的192.168.238.130是Pod所在的节点机器的IP地址,请替换为您的k8s环境中的节点IP地址;
第二,UPLOAD_FILE_FULLPATH = “D:temp20180221abc.zip”,这里配置的是要上传的本地文件的路径,请替换为您要上传的文件路径;

3、直接运行UploadFileClient.java,看到控制台输出如下信息表示上传成功:

start upload
executing request POST http://192.168.238.130:30010/upload HTTP/1.1
response status : HTTP/1.1 200 
response content length: 40
response entity str : SpringBoot环境下,上传文件成功
end upload

Process finished with exit code 0

上传成功了,在dashboard上查看容器的日志,能看到上传有关的日志如下图红框所示:

未分类

现在理论上现在文件已经保存在NFS Server的/usr/local/work/nfs目录下了,去检查一下;

去NFS Server检查上传的文件

登录NFS Server,进入/usr/local/work/nfs,查看文件信息如下图:

未分类

可见k8s上的tomcat应用可以通过动态卷存储的方式将客户端上传的文件保存在NFS服务器上;

扩展web应用的Pod数量

目前web应用的Pod数量是1,执行以下命令扩展到两个:

kubectl scale StatefulSet k8spvdemo --replicas=2 --namespace=bolingcavalry

执行完毕后,在dashboard可以看见创建的动态卷,如下图:

未分类

您可以再次上传文件,然后去NFS服务器检查是否已经创建了新的文件夹,并且存放了新的上传文件;

至此,Kubernetes动态卷存储的实战就全部完成了,希望PV&&PVC和动态存储两种方案可以帮助您在应用中解决独立存储的问题;

kubernetes 的资源配额控制器

有很多团队在使用kubernetes的时候是将一个namespace当成一个租户的,所以对namespace的权限控制,资源控制就很重要了,你总是会担心你的某个租户使用的资源就超出了应有的配额。幸运的是kubernetes本身就为我们提供了解决这一问题的工具:资源配额控制器(ResourceQuotaController)

资源配额控制器确保了指定的资源对象始终不会超过配置的资源,能够有效的降低整个系统宕机的机率,增强系统的鲁棒性,对整个集群的稳定性有非常重要的作用。

介绍

kubernetes主要有3个层级的资源配额控制:

  • 容器:可以对 CPU 和 Memory 进行限制
  • POD:可以对一个 POD 内所有容器的的资源进行限制
  • Namespace:为一个命名空间下的资源进行限制

其中容器层次主要利用容器本身的支持,比如 Docker 对 CPU、内存等的支持;POD 方面可以限制系统内创建 POD 的资源范围,比如最大或者最小的 CPU、memory 需求;Namespace 层次就是对用户级别的资源限额了,包括 CPU、内存,还可以限定 POD、RC、Service 的数量。

要使用资源配额的话需要确保apiserver的–admission-control参数中包含ResourceQuota,当 namespace 中存在一个 ResourceQuota对象时,该 namespace 即开始实施资源配额的管理工作了,另外需要注意的是一个 namespace 中最多只应存在一个 ResourceQuota 对象。

ResourceQuotaController支持的配额控制资源主要包括:计算资源配额、存储资源配额、对象数量资源配额以及配额作用域,下面我们来分别看看这些资源的具体信息:

计算资源配额

用户可以对给定 namespace 下的计算资源总量进行限制,ResourceQuotaController所支持的资源类型如下:

未分类

示例如下:

$ cat <<EOF > compute-resources.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute-resources
  namespace: myspace
spec:
  hard:
    requests.cpu: "1"
    requests.memory: 1Gi
    limits.cpu: "2"
    limits.memory: 2Gi
EOF
$ kubectl create -f compute-resources.yaml

然后我们查看下上面我们创建的 ResourceQuota对象:

$ kubectl describe quota compute-resources -n myspace
Name:            compute-resources
Namespace:       myspace
Resource         Used  Hard
--------         ----  ----
limits.cpu       0     2
limits.memory    0     2Gi
requests.cpu     0     1
requests.memory  0     1Gi

另外需要注意的是:如果 namespace 下的计算资源 (如 cpu 和 memory)的配额被启用,则用户必须为这些资源设定请求值(request) 和约束值(limit),否则配额系统将拒绝Pod的创建;或者也可以为资源设置默认的资源限制,如下 yaml 文件:(同样需要在–admission-control参数中设置值LimitRanger)

apiVersion: v1
kind: LimitRange
metadata:
  name: limits
spec:
  limits:
  - default:
    cpu: 200m
    memory: 200Mi
  type: Container

存储资源配额

用户可以对给定 namespace 下的存储资源总量进行限制,此外,还可以根据相关的存储类(Storage Class)来限制存储资源的消耗。

未分类

对象数量配额

给定类型的对象数量可以被限制。 支持以下类型:

未分类

如下示例:

$ cat <<EOF > object-counts.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: object-counts
  namespace: myspace
spec:
  hard:
    pods: "40"
    configmaps: "10"
    replicationcontrollers: "20"
    secrets: "10"
    services: "10"
    services.nodeports: "2"
EOF
$ kubectl create -f object-counts.yaml

同样可以查看上面创建的资源限额对象:

$ kubectl describe quota object-counts -n myspace
Name:                   object-counts
Namespace:              myspace
Resource                Used  Hard
--------                ----  ----
configmaps              0     10
pods                    0     40
replicationcontrollers  0     20
secrets                 1     10
services                0     10
services.nodeports      0     2

配额作用域

每个配额都有一组相关的作用域(scope),配额只会对作用域内的资源生效。当一个作用域被添加到配额中后,它会对作用域相关的资源数量作限制。 如配额中指定了允许(作用域)集合之外的资源,会导致验证错误。

未分类

其中BestEffort 作用域限制配额跟踪以下资源: PODS

Terminating、 NotTerminating 和 NotBestEffort限制配额跟踪以下资源:cpu、limits.cpu、limits.memory、memory、pods、requests.cpu、requests.memory。

总结

物理资源 CPU 和内存受到 namespace 资源配额管理器的控制,几乎所有使用到的资源都能够被简单的调整,我们可以结合我们自身的业务和物理资源实际情况来进行分配配额,所以这里没有一个完全通用的配额方案,需要根据实际情况来定,因为只有你自己最清楚你的集群的整体情况了。

Kubernetes性能测试实践

概述

随着容器技术的发展,容器服务已经成为行业主流,然而想要在生产环境中成功部署和操作容器,关键还是容器编排技术。市场上有各种各样的容器编排工具,如Docker原生的Swarm、Mesos、Kubernetes等,其中Google开发的Kubernetes因为业界各大巨头的加入和开源社区的全力支撑,成为了容器编排的首选。

简单来讲,Kubernetes是容器集群管理系统,为容器化的应用提供资源调度、部署运行、滚动升级、扩容缩容等功能。容器集群管理给业务带来了便利,但是随着业务的不断增长,应用数量可能会发生爆发式的增长。那在这种情况下,Kubernetes能否快速地完成扩容、扩容到大规模时Kubernetes管理能力是否稳定成了挑战。

因此,结合近期对社区Kubernetes性能测试的调研和我们平时进行的kubernetes性能测试实践,和大家探讨下Kubernetes性能测试的一些要点,不足之处还望大家多多指正!

测试目的

未分类

如上Kubernetes架构图所示,无论外部客户端,还是Kubernetes集群内部组件,其通信都需要经过Kubernetes的apiserver,API的响应性决定着集群性能的好坏。

其次,对于外部客户而言,他只关注创建容器服务所用的时间,因此,pod的启动时间也是影响集群性能的另外一个因素。

目前,业内普遍采用的性能标准是:

  • API响应性:99%的API调用响应时间小于1s。
  • Pod启动时间:99%的pods(已经拉取好镜像)启动时间在5s以内。

“pod启动时间”包括了ReplicationController的创建,RC依次创建pod,调度器调度pod,Kubernetes为pod设置网络,启动容器,等待容器成功响应健康检查,并最终等待容器将其状态上报回API服务器,最后API服务器将pod的状态报告给正在监听中的客户端。

除此之外,网络吞吐量、镜像大小(需要拉取)都会影响Kubernetes的整体性能。

测试要点

1、社区测试Kubernetes性能的关键点

  • 当集群资源使用率是X%(50%、90% 、99%等不同规模下)的时候,创建新的pod所需的时间(这种场景需要提前铺底,然后在铺底基础上用不同的并发梯度创建pod,测试pod创建耗时,评估集群性能)。在测试kubernetes新版本时,一般是以老版本稳定水位(node、pod等)铺底,然后梯度增加进行测试。
  • 当集群使用率高于90%时,容器启动时延的增大(系统会经历一个异常的减速)还有etcd测试的线性性质和“模型建立”的因素。调优方法是:调研etcd新版本是否有解决该问题。
  • 测试的过程中要找出集群的一个最高点,低于和高于这个阈值点,集群性能都不是最优的。
  • 组件负载会消耗master节点的资源,资源消耗所产生的不稳定性和性能问题,会导致集群不可用。所以,在测试过程中要时刻关注资源情况。
  • 客户端创建资源对象的格式 —— API服务对编码和解码JSON对象也需要花费大量的时间 —— 这也可以作为一个优化点。

2、网易云容器服务Kubernetes集群性能测试关键点总结

集群整体

  • 不同的集群使用水位线(0%,50%, 90%)上,pod/deployment(rs 等资源)创建、扩缩容等核心操作的性能。可以通过预先创建出一批dp(副本数默认设置为3)来填充集群,达到预期的水位,即铺底。
  • 不同水位对系统性能的影响——安全水位,极限水位
  • 容器有无挂载数据盘对容器创建性能的影响。例如,挂载数据盘增加了kubelet挂载磁盘的耗时,会增加pod的启动时长。

测试kubernetes集群的性能时,重点关注在不同水位、不同并发数下,长时间执行压力测试时,系统的稳定性,包括:

  • 系统性能表现,在较长时间范围内的变化趋势
  • 系统资源使用情况,在较长时间范围内的变化趋势
  • 各个服务组件的TPS、响应时间、错误率
  • 内部模块间访问次数、耗时、错误率等内部性能数据
  • 各个模块资源使用情况
  • 各个服务端组件长时间运行时,是否出现进程意外退出、重启等情况
  • 服务端日志是否有未知错误
  • 系统日志是否报错

apiserver

  1. 关注api的响应时间。数据写到etcd即可,然后根据情况关注异步操作是否真正执行完成。
  2. 关注apiserver缓存的存储设备对性能的影响。例如,master端节点的磁盘io。
  3. 流控对系统、系统性能的影响。
  4. apiserver 日志中的错误响应码。
  5. apiserver 重启恢复的时间。需要考虑该时间用户是否可接受,重启后请求或者资源使用是否有异常。
  6. 关注apiserver在压力测试情况下,响应时间和资源使用情况。

scheduler

1、压测scheduler处理能力

  • 并发创建大量pod,测试各个pod被调度器调度的耗时(从Pod创建到其被bind到host)
  • 不断加大新建的pod数量来增加调度器的负载
  • 关注不同pod数量级下,调度器的平均耗时、最大时间、最大QPS(吞吐量)

2、scheduler 重启恢复的时间(从重启开始到重启后系统恢复稳定)。需要考虑该时间用户是否可接受,重启后请求或者资源使用是否有异常。

3、关注scheduler日志中的错误信息。

controller

1、压测 deployment controller处理能力

  • 并发创建大量rc(1.3 以后是deployment,单副本),测试各个deployment被空感知并创建对应rs的耗时
  • 观察rs controller创建对应pod的耗时
  • 扩容、缩容(缩容到0副本)的耗时
  • 不断加大新建deployment的数,测试在不同deployment数量级下,控制器处理deployment的平均耗时、最大时间、最大QPS(吞吐量)和控制器负载等情况

2、controller 重启恢复的时间(从重启开始到重启后系统恢复稳定)。需要考虑该时间用户是否可接受,重启后请求或者资源使用是否有异常。

3、关注controller日志中的错误信息。

kubelet

  • node心跳对系统性能的影响。
  • kubelet重启恢复的时间(从重启开始到重启后系统恢复稳定)。需要考虑该时间用户是否可接受,重启后请求或者资源使用是否有异常。
  • 关注kubelet日志中的错误信息。

etcd

1、关注etcd 的写入性能

  • 写最大并发数
  • 写入性能瓶颈,这个主要是定期持久化snapshot操作的性能

2、etcd 的存储设备对性能的影响。例如,写etcd的io。

3、watcher hub 数对kubernetes系统性能的影响。

解决 k8s 上 traefik-ingress 响应慢的问题

[摘要] 在 K8s 上配置的 traefik-ingress 作为LB,在配置 traefik-ingress 的节点上配置keepalived起VIP做高可用,可以起到app发现的功能,统一访问入口,并不需要知道后端具体启动的应用。不过测试中访问速度并不是很理想,基本每个请求都在10s左右,很是令人费解..

测试结果,测试用例是之前配置的 K8S rolling-update 的go程序:

# kubectl get pods
NAME                                   READY     STATUS    RESTARTS   AGE
glusterfs                              1/1       Running   0          3d
rolling-update-test-759964656c-dp6jj   1/1       Running   0          3d
rolling-update-test-759964656c-jtpfb   1/1       Running   0          3d
rolling-update-test-759964656c-pjgrg   1/1       Running   0          3d

# time curl -H Host:rolling-update-test.sudops.in http://172.30.0.251/
This is version 3.
real    0m11.744s
user    0m0.004s
sys 0m0.006s
#

traefik.yaml 的内容如下:

apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  name: traefik-ingress-lb
  namespace: kube-system
  labels:
    k8s-app: traefik-ingress-lb
spec:
  template:
    metadata:
      labels:
        k8s-app: traefik-ingress-lb
        name: traefik-ingress-lb
    spec:
      terminationGracePeriodSeconds: 60
      hostNetwork: true
      restartPolicy: Always
      serviceAccountName: ingress
      containers:
      - image: traefik
        name: traefik-ingress-lb
        resources:
          limits:
            cpu: 200m
            memory: 30Mi
          requests:
            cpu: 100m
            memory: 20Mi
        ports:
        - name: http
          containerPort: 80
          hostPort: 80
        - name: admin
          containerPort: 8580
          hostPort: 8580
        args:
        - --web
        - --web.address=:8580
        - --kubernetes
      nodeSelector:
        edgenode: "true"

反复查找原因,发现在其中的一台边缘节点上访问很快,基本0.0ms级的响应,其他节点均在10s左右。
网上搜了好久,终于发现有一外国哥们说是去掉对traefik-ingress-lb的资源限制看看

未分类

于是将资源的限制部分去掉(注释掉),delete 后重新 create,在任意一个节点上测试都会发现快多了。

将:
        resources:
          limits:
            cpu: 200m
            memory: 30Mi
          requests:
            cpu: 100m
            memory: 20Mi
注释掉:
        #resources:
        #  limits:
        #    cpu: 200m
        #    memory: 30Mi
        #  requests:
        #    cpu: 100m
        #    memory: 20Mi

重新创建 traefik-ingress
测试结果:
# time curl -H Host:rolling-update-test.sudops.in http://172.30.0.251/
This is version 3.
real    0m0.015s
user    0m0.006s
sys 0m0.004s
#

未分类
traefik-ingress-health

在此Mark一下。

使用Helm 在容器服务k8s集群一键部署wordpress

摘要: Helm 是啥? 微服务和容器化给复杂应用部署与管理带来了极大的挑战。Helm是目前Kubernetes服务编排领域的唯一开源子项目,做为Kubernetes应用的一个包管理工具,可理解为Kubernetes的apt-get / yum,由Deis 公司发起,该公司已经被微软收购。

Helm 是啥?

微服务和容器化给复杂应用部署与管理带来了极大的挑战。Helm是目前Kubernetes服务编排领域的唯一开源子项目,做为Kubernetes应用的一个包管理工具,可理解为Kubernetes的apt-get / yum,由Deis 公司发起,该公司已经被微软收购。Helm通过软件打包的形式,支持发布的版本管理和控制,很大程度上简化了Kubernetes应用部署和管理的复杂性。

Helm 架构

未分类

Helm 用途

做为Kubernetes的一个包管理工具,Helm具有如下功能:

  • 创建新的chart
  • chart打包成tgz格式
  • 上传chart到chart仓库或从仓库中下载chart
  • 在Kubernetes集群中安装或卸载chart
  • 管理用Helm安装的chart的发布周期

Helm有三个重要概念:

  • chart:包含了创建Kubernetes的一个应用实例的必要信息
  • config:包含了应用发布配置信息
  • release:是一个chart及其配置的一个运行实例

如何在阿里云容器服务使用Helm

阿里云容器服务的kubernets集群默认集成了helm并初始化提供了一些常用charts,下面我们就以安装wordpress示例来演示使用流程。

未分类

以上为容器服务默认提供的一些安装charts,下面我们来安装wordpress:

未分类

可以根据用户自身的需要,修改wordpress安装charts的一些默认配置,当然使用默认配置安装也是没问题的,输入本次安装release的名字,点击部署后就完成了一键部署。
我们使用控制台查看一下部署资源的情况:

未分类

可以看到wordpress的依赖资源都已经安装完毕,访问图中圈出来的地址就可以打开wordpress界面:

未分类

可以看到wordpress已经可以正常访问。如果使用传统方式,你可能需要创建一堆deployment + service + pvc等集合体,现在只需要一键部署,等待片刻,一个wordpress应用就可以展现在你面前。

aspnetcore.webapi实践k8s健康探测机制 – kubernetes

1、浅析k8s两种健康检查机制

  • Liveness
    k8s通过liveness来探测微服务的存活性,判断什么时候该重启容器实现自愈。比如访问 Web 服务器时显示 500 内部错误,可能是系统超载,也可能是资源死锁,此时 httpd 进程并没有异常退出,在这种情况下重启容器可能是最直接最有效的解决方案。

  • Readiness
    k8s通过readiness来探测微服务的什么时候准备就绪(例如初始化时,连接数据库,加载缓存数据等等,可能需要一段时间),然后将容器加入到server的负载均衡池中,对外提供服务。

1.1、k8s默认的健康检查机制

  每个容器启动时都会执行一个进程,此进程由 Dockerfile 的 CMD 或 ENTRYPOINT 指定。如果进程退出时返回码非零,则认为容器发生故障,Kubernetes 就会根据 restartPolicy 重启容器。如果不特意配置,Kubernetes 将对两种探测采取相同的默认行为。

2、通过微服务自定义两种机制

存活10分钟:如果当前时间超过服务启动时间10分钟,则探测失败,否则探测成功。Kubernetes 如果连续执行 3 次 Liveness 探测均失败,就会杀掉并重启容器。

未分类

准备就绪30秒,30秒后,如果连续 3 次 Readiness 探测均失败后,容器将被重置为不可用,不接收 Service 转发的请求。

未分类

从上面可以看到,我们可以根据自身的需求来实现这两种机制,然后,提供给k8s进行探测。

3、编写k8s资源配置文件(yml)

k8s默认是根据命令进行探测的,由于我们需要与微服务结合,所以需要在yml文件中指定为http方式,k8s对于http方式探测成功的判断条件是请求的返回代码在 200-400 之间。

health-checks-deployment.yml 如下:

未分类

从上面可以看到,一共部署了3个pod副本,而每个pod副本里面部署一个容器,即为同一个微服务部署了3个实例进行集群。

4、在k8s集群的master机器上,创建部署对象

未分类

从上面可以看到,刚开始创建时,READY 状态为不可用,等待一段时间

未分类

现在全部可用了

5、通过dashboard查看集群概况

未分类

6、剖析k8s集群自愈(self-healing)过程

未分类

未分类

未分类

从上面可以看到,大约1分钟(dashboard统计信息有一定的延迟)左右,第一次进行 Readiness 探测并成功返回,此时准备就绪,可以对外提供服务了。在10分钟内,探测Liveness也成功返回。

继续等待一段时间,查询其中一个pod详细信息:

未分类

未分类

从上面可以看到,超过10分钟存活期后,liveness探测失败,容器被 killed and recreated。探测Readiness未成功返回时,整个容器处于不健康的状态,并不会被负载均衡请求。

此时通过dashboard查看集群概况:

未分类

继续等待一段时间:

未分类

现在,整个集群已经自愈完成了!!!

7、总结

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

源码参考:https://github.com/justmine66/k8s.ecoysystem.apps