FreeWheel基于Kubernetes容器云构建与实践

【编者的话】随着公司业务不断发展以及逐渐向微服务的转变,我们借助于Kubernetes容器化解决方案来标准化和简化应用发布的整个流程,使原来需要大量人工维护和干预的工作变为自动化。本次内容主要是FreeWheel现阶段基于Kubernetes容器化经验和实践的总结,目标是提供一个持续、稳定、高效的容器云平台。

服务健康检查与自我恢复

对线上业务来说,保证服务的正常稳定是重中之重,对故障服务的及时处理避免影响业务以及快速恢复一直是开发运维的难点。Kubernetes提供了健康检查服务,对于检测到故障服务会被及时自动下线,以及通过重启服务的方式使服务自动恢复。

主要分享内容:

  1. 如何判断Container和Service的健康状态。
  2. 健康检查失败的Container和Service,如何自我恢复。
  3. 使用建议。

 

健康检查

使用Liveness及Readness探针
  • Liveness探针主要用于判断Container是否处于运行状态,比如当服务crash或者死锁等情况发生时,kubelet会kill掉Container,然后根据其设置的restart policy进行相应操作(可能会在本机重新启动Container,或者因为设置Kubernetes QoS,本机没有资源情况下会被分发的其他机器上重新启动)。
  • Readness探针主要用于判断服务是否已经正常工作,如果服务没有加载完成或工作异常,服务所在的Pod的IP地址会从服务的Endpoints中被移除,也就是说,当服务没有ready时,会将其从服务的load balancer中移除,不会再接受或响应任何请求。

探针处理Handler类型

无论对于Readness或Liveness探针,Handler均支持以下3种类型:ExecAction、TCPSocketAction、HTTPGetAction。每种类型说明与举例如下:

  • ExecAction:Container内部执行某个具体的命令,例子。
  • TCPSocketAction:通过Container的IP、port执行tcp进行检查, 例子。
  • HTTPGetAction:通过Container的IP、port、path,用HTTP Get请求进行检查,例子。

 

探针检查结果

探针检查结果分为3种情况:

  1. 成功(Success):通过检查。
  2. 失败(Failure):检查失败。
  3. 未知(Unknown):检查未知,需要人工干预。

 

健康检查总结
探针类型          说明                                 通过健康检查标准 
ExecAction       Container内部执行shell命令            shell命令返回0
TCPSocketAction  通过ContainerIPport执行tcp进行检查  port是否打开
HTTPGetAction    通过ContainerIPportpath       200<=返回值<400
                 HTTP Get请求进行检查

 

服务可用性与自动恢复

  1. 如果服务的健康检查(readiness)失败,故障的服务实例从service endpoint中下线,外部请求将不会再转发到该服务上,一定程度上保证正在提供的服务的正确性,如果服务自我恢复了(比如网络问题),会自动重新加入service endpoint对外提供服务。
  2. 另外,如果设置了Container(liveness)的探针,对故障服务的Container(liveness)的探针同样会失败,container会被kill掉,并根据原设置的container重启策略,系统倾向于在其原所在的机器上重启该container、或其他机器重新创建一个pod。
  3. 由于上面的机制,整个服务实现了自身可用与自动恢复。

 

使用建议:

  1. 建议对全部服务同时设置服务(readiness)和Container(liveness)的健康检查。
  2. 通过TCP对端口检查形式(TCPSocketAction),仅适用于端口已关闭或进程停止情况。因为即使服务异常,只要端口是打开状态,健康检查仍然是通过的。
  3. 基于第二点,一般建议用ExecAction自定义健康检查逻辑,或采用HTTP Get请求进行检查(HTTPGetAction)。
  4. 无论采用哪种类型的探针,一般建议设置检查服务(readiness)的时间短于检查Container(liveness)的时间,也可以将检查服务(readiness)的探针与Container(liveness)的探针设置为一致。目的是故障服务先下线,如果过一段时间还无法自动恢复,那么根据重启策略,重启该Container、或其他机器重新创建一个Pod恢复故障服务。

 

应用编排与配置管理

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

随着业务容器化与向微服务架构转变,通过分解巨大的单体应用为多个服务的方式,分解了单体应用的复杂性,使每个微服务都可以独立部署和扩展,实现了敏捷开发和快速迭代和部署。但任何事情都有两面性,虽然微服务给我们带来了很多便利,但由于应用被拆分成多个组件,导致服务数量大幅增加,对于Kubernetest编排来说,每个组件有自己的资源文件,并且可以独立的部署与伸缩,这给采用Kubernetes做应用编排带来了诸多挑战:

  1. 管理、编辑与更新大量的Kubernetest配置文件
  2. 部署一个含有大量配置文件的复杂Kubernetest应用
  3. 分享和复用Kubernetest配置和应用
  4. 参数化配置模板支持多个环境
  5. 管理应用的发布:回滚、diff和查看发布历史
  6. 控制一个部署周期中的某一些环节
  7. 发布后的验证

而Helm恰好可以帮助我们解决上面问题。

主要分享内容:

  1. Helm的简介,包括Helm术语、架构、用途等。
  2. 展示Helm做一次release的过程,包括release创建、更新、回滚以及删除等。
  3. Helm的对release的强大版本管理功能。
  4. 利用Helm/init container处理服务启动顺序依赖。

 

Helm架构

Helm基本架构如下:

Helm有三个重要概念:

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

 

Helm组件

Helm有以下两个组成部分:

Helm Client是用户命令行工具,其主要负责如下:

  • 本地chart开发
  • 仓库管理
  • 与Tiller sever交互
  • 发送预安装的chart
  • 查询release信息
  • 要求升级或卸载已存在的release
  • Tiller Server是一个部署在Kubernetes集群内部的server,其与Helm client、Kubernetes API server进行交互。

Tiller server主要负责如下:

  • 监听来自Helm client的请求
  • 通过chart及其配置构建一次发布
  • 安装chart到Kubernetes集群,并跟踪随后的发布
  • 通过与Kubernetes交互升级或卸载chart

简单的说,client管理charts,而server管理发布release。

Helm实现

Helm client

  • Helm client采用Go语言编写,采用gRPC协议与Tiller server交互。

Helm server

  • Tiller server也同样采用Go语言编写,提供了gRPC server与client进行交互,利用Kubernetes client 库与Kubernetes进行通信,当前库使用了REST+JSON格式。
  • Tiller server 没有自己的数据库,目前使用Kubernetes的ConfigMaps存储相关信息

说明:配置文件尽可能使用YAM格式

Helm部署Kubernetes应用

获取chart

获取版本为0.2.8的mysql并解压缩包:

$ helm fetch stable/mysql --version 0.2.8 --untar

利用helm lint命令检查下载的chart是否存在问题:

$ helm lint mysql
==> Linting mysql
Lint OK
1 chart(s) linted, no failures

 

创建chart

利用helm create mychart命令创建一个mychart目录:

$ helm create mychart

生成的mychart的文件结构如下:

mychart/
|-- charts
|-- Chart.yaml
|-- templates
|   |-- deployment.yaml
|   |-- _helpers.tpl
|   |-- ingress.yaml
|   |-- NOTES.txt
|   `-- service.yaml
`-- values.yaml

2 directories, 7 files

生成chart目录里有Chart.yaml,values.yaml 与 NOTES.txt等文件,下面分别对chart中几个重要文件解释:

Chart.yaml 包含了chart的meta

  • data,描述了Chart名称、描述信息与版本。
  • values.yaml:存储了模板文件变量。
  • templates/:记录了全部模板文件。
  • charts/:依赖chart存储路径。
  • NOTES.txt:给出了部署后的信息,例如如何使用chart、列出默认的设置等等。

chart安装有以下几种方式:

  • 指定chart:helm install stable/mariadb
  • 指定打包的chart:helm install ./nginx-1.2.3.tgz
  • 指定打包目录:helm install ./nginx
  • 指定chart包URL:helm install https://example.com/charts/nginx-1.2.3.tgz

覆盖chart中的默认值,通过指定配置文件方式:

helm install -f myvalues.yaml ./redis

或者通过–set key=value形式:

$ helm install --set name=prod ./redis

安装release名称为mysql例子如下:

$ helm install -n mysql -f mysql/values.yaml --set resources.requests.memory=512Mi mysql

通过helm status查看release状态:

$ helm status mysql
LAST DEPLOYED: Tue Sep 12 07:31:49 2017
NAMESPACE: default
STATUS: DEPLOYED

或通过helm list -a查看全部的release,tag “-a”是查看全部的release,包括已部署、部署失败、正在删除、已删除release等。

$ helm list -| grep mysqlmysql           1       Tue Sep 12 07:31:49 2017 DEPLOYED mysql-0.2.8         default

 

更新release

Helm使用helm upgrade更新已安装的release:

$ helm upgrade mysql -f mysql/values.yaml --set resources.requests.memory=1024Mi mysql

查看指定release的历史部署版本信息:

$ helm hist  mysql
REVISION    UPDATED                        STATUS           CHART          DESCRIPTION
1           Tue Sep 12 07:31:49 2017       SUPERSEDED       mysql-0.2.8    Install complete
2           Tue Sep 12 07:44:00 2017       DEPLOYED         mysql-0.2.8    Upgrade complete

查看指定release的历史版本部署时部分配置信息,以resources.requests.memory为例,符合查看部署符合预期:即第一次部署resources.requests.memory设置为512Mi,第二次的升级resources.requests.memory设置为1024Mi:

$ helm get --revision 1 mysql
resources:
requests:
cpu: 100m
memory: 512Mi

$ helm get --revision 2 mysql
resources:
requests:
cpu: 100mw'w
memory: 1024Mi

 

版本回滚

回滚到第一次的版本:

helm rollback --debug mysql 1
[debug] Created tunnel using local port: '60303'
[debug] SERVER: "localhost:60303"
Rollback was a success! Happy Helming!

查看mysql release的版本信息,当前已经回滚到REVISION为1的版本:

$ helm hist mysql
REVISION           UPDATED                    STATUS        CHART          DESCRIPTION
1                  Tue Sep 12 07:31:49 2017   SUPERSEDED    mysql-0.2.8    Install complete
2                  Tue Sep 12 07:44:00 2017   SUPERSEDED    mysql-0.2.8    Upgrade complete
3                  Tue Sep 12 08:50:48 2017   DEPLOYED      mysql-0.2.8    Rollback to 1

 

删除chart

利用helm delete命令删除一个chart:

$ helm delete mysql

即使删除的chart,其发布的历史信息还是继续被保存。

$ helm hist mysql
REVISION      UPDATED                     STATUS         CHART          DESCRIPTION
1             Tue Sep 12 07:31:49 2017    SUPERSEDED     mysql-0.2.8    Install complete
2             Tue Sep 12 07:44:00 2017    SUPERSEDED     mysql-0.2.8    Upgrade complete
3             Tue Sep 12 08:50:48 2017    DELETED        mysql-0.2.8    Deletion complete

可以恢复一个已经删除的release:

$ helm rollback --debug mysql 2
[debug] Created tunnel using local port: '37413'
[debug] SERVER: "localhost:37413"
Rollback was a success! Happy Helming!

如果希望彻底删除一个release,可以用如下命令:

$ helm delete --purge mysql
release "mysql" deleted

 

Helm对release的版本管理

在上面例子中,已经展示了Helm对release的非常强大的版本管理功能,比如通过”helm list -a”查看有哪些release,通过” helm hist“查看某一个具体的release发布过的历史版本,以及通过” helm get –revision”,查看某个release的一次历史版本对应的具体应用配置信息等。即使已经被删除的release仍然有记录,并且通过Helm能够快速回滚到已删除release的某个发布过的历史版本。Helm的这些版本管理功能,Kubernetes原生并不支持。

使用Helm对Release配置

上面展示了一次Helm发布的生命周期,包含了chart创建、更新、回滚、删除等。但这并不是我们使用Helm的唯一原因,我们还需要一个管理配置发布的工具。

Helm Chart模板采用go模板语言编写 Go template language,模板的值记录在values.yaml文件中。values.yaml文件中的值可以在安装chart过程中,通过参数–values YAML_FILE_PATH或–set key1=value1, key2=value2替换。

注意:模板语言中的引用数值型需要注意,不加引号,会被解释为数值。

查看上文中创建mychart/templates/configmap.yaml文件,内容如下:

apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"

一个模板指令由{{}}标记。{{ .Release.Name }}会把release name插入模板。传入模板的值可被认作是namespaces对象,通过”.”来分隔namespaces元素。

内置对象

对象通过模板引擎传给模板,有几种方式在模板内创建新的对象,比如后面要讲到的tuple。

模板内置对象请参见:https://docs.helm.sh/chart_tem … jects

Values文件

上面章节提到Helm模板提供了内置的对象,而Values做为4个内置对象之一,有4种来源:

  • 可通过chart中的values.yaml文件
  • 对于subchart,父chart中values.yaml文件
  • helm install 或 helm update 指定 -f flag(helm install -f myvals.yaml ./mychart)
  • 通过–set 参数传值(例如 helm install –set foo=bar ./mychart)

说明:values.yaml默认会被引用,但其会被父chart中的values.yaml文件内容覆盖(overridden),而父chart中的values.yaml文件会被户用-f指定的文件内容覆盖,而用户用-f指定的文件会被–set参数传的值覆盖。简单的说,也就是上面4种Values来源的重要性由上到下的顺序,优先级由低到高。

删除默认键值

如需要删除键值对中删除默认key,需要将key的值设置为null,helm也会将从覆盖的键值对中将其剔除。

模板功能和Pipeline

前面展示的模板例子中,模板内容基本不去修改,但有一些情况下,我们希望提供给模板的数据对我们更加简单易用。例如对于 .Values对象注入模板的键值可利用模板提供的quote功能引用。

apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ quote .Values.favorite.drink }}
food: {{ quote .Values.favorite.food }}  

Helm 提供了超过60功能函数,其中一些定义在Go模板语言自身: Go template language . 这些函数是 Sprig template library的一部分。

PIPELINES

Pipelines是模板语言的非常强大的特性之一,与Linux/Unix的pipeline类似,pipelines是把模板语言的多个命令通过 (|)的分隔形式,以描述的顺序的实现命令聚合功能。

apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
drink: {{ .Values.favorite.drink | repeat 5 | quote }}
food: {{ .Values.favorite.food | upper | quote }}

上面例子中,对.Values.favorite.drink | repeat 5 | quote的值coffee重复了5次后引用,而对.Values.favorite.food先做了一次大写转换然后加以引用。类似结果如下:

data:
myvalue: "Hello World"
drink: "coffeecoffeecoffeecoffeecoffee"
food: "PIZZA"

使用 DEFAULT功能

default是模板中经常使用一个功能,该功能是在模板中指定一个默认值。例如下面例子中,如果.Values.favorite.drink没有被赋值,那么模板被引用时,tea便为其默认值。

drink: {{ .Values.favorite.drink | default "tea" | quote }} 

操作符和函数

对于Helm模板,类似eq,ne,lt,gt,and,or等操作符已经实现为函数,

控制流(Flow Control)

Helm模板语言提供了如下控制结构:

  • if/else 条件语句
  • with 指定范围
  • range,提供了“for each”-形式的循环

除此之外,Helm还提供了一些命名模板的行为:

  • define 模板内部定义一个命名模板
  • template 导入一个命名模板
  • block declares a special kind of fillable template area

IF/ELSE

条件基本的控制结构如下:

{{ if PIPELINE }}
# Do something
{{ else if OTHER PIPELINE }}
# Do something else
{{ else }}
# Default case
{{ end }}   

pipeline会被认为是false 如果值是下面几种类型:

  • 布尔类型 false
  • 数字 0
  • 空串
  • nil(empty or null)
  • 空集合(map,slice,tuple,dict,array)

空格敏感

Helm模板语言对空格非常敏感,左侧有{{-(破折号后面有一个空格)表明空格需要被移除,而右侧 -}}表示空格会被消费。

使用with限制引用范围

通过with与(.) 指定当前范围到某一个指定的对象,例如下面例子中 .Values.favorites,后面再引用drink与food时,无需再指定Values.favorite。

apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
data:
myvalue: "Hello World"
{{- with .Values.favorite }}
drink: {{ .drink | default "tea" | quote }}
food: {{ .food | upper | quote }}
{{- end }} 

RANGE循环

像多数编程语言支持for与foreach等循环一样,Helm模板语言通过支持range操作符遍历一个集合。

模板变量

模板变量在range的循环中非常有用,变量的定义格式为$name ,可通过 :=赋值。举例如下:

toppings: |-
{{- range $index, $topping := .Values.pizzaToppings }}
  {{ $index }}: {{ $topping }}
{{- end }}  

 

命名模板

Helm允许我们创建一个命名模板,通过模板名称,可以在任何地方引用它。一般情况下命名模板会写在_helpers.tpl 中,并以形式 ({{/* … */}})加上模板说明。比如下面例子中定义了一个my_labels命名模板,并且用 include通过 {{- include “my_labels” }}嵌入到configmap的metadata中。通常 include也可以用 template代替,但是 include在处理YAML文件的格式输出上会更胜一筹,另外 include可以包含一个变量的模板{{ include $mytemplate }}。

{{/* Generate basic labels */}}
{{- define "my_labels" }}
labels:
generator: helm
date: {{ now | htmlDate }}
{{- end }}  

在configmap.yaml文件中引用该命名模板:

apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
{{- include "my_labels" . }}
data:
myvalue: "Hello World"
{{- range $key, $val := .Values.favorite }}
{{ $key }}: {{ $val | quote }}
{{- end }}  

需要注意的是,命名模板名称是全局的,如果存在多个同名的模板,最后被load到Tiller中的模板会被最终使用。另外模板的命名通常以chart的名字做为前缀,比如 {{ define “mychart.labels” }} 或 {{ define “mychart_labels” }}。

设置模板使用范围

前面的命名模板中,并没有使用模板的内置对象。下面例子中在命名模板中使用了内置对象:

{{/* Generate basic labels */}}
{{- define "my_labels" }}
labels:
generator: helm
date: {{ now | htmlDate }}
chart: {{ .Chart.Name }}
version: {{ .Chart.Version }}
{{- end }}  

在configmap中消费这个命名模板,注意在命名模板调用的末尾加上” .”, indent 2表示引用的模板内容向右缩进2格。

apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-configmap
labels:
{{ include "mychart_app" . | indent 4 }}
data:
myvalue: "Hello World"
{{- range $key, $val := .Values.favorite }}
{{ $key }}: {{ $val | quote }}
{{- end }}
{{ include "mychart_app" . | indent 2 }}  

 

模板访问文件

上个章节中展示了几种创建和访问命名模板的方式,通过命名模板,可以很容易在一个模板中引用另外一个模板。但有的时候,我们还有导入一个普通文件内容来渲染模板的需求,而不仅仅是以模板的形式。Helm提供了一个内置 .Files对象。

Helm支持chart中存在一个普通类型的文件,这些文件也会被与其他模板文件一同打包发给给Tiller,但文件大小目前被限制在1M以内。由于安全原因,一般情况下 templates/的.Files对象不可访问,另外,chart并不会保留 UNIX mode的信息,文件层面的权限并不会影响对.Files对象的访问控制。

NOTES.txt文件

在 chart install 或 chart upgrade的尾部,会打印出用户帮助信息,该信息在 templates/NOTES.txt中定制。Helm并不强制提供用户帮助信息,但为了用户更加方便了解和使用所安装的应用,强烈建议在templates/NOTES.txt加上用户帮助信息。举例如下:

Thank you for installing {{ .Chart.Name }}.

Your release is named {{ .Release.Name }}.

To learn more about the release, try:

$ helm status {{ .Release.Name }}
$ helm get {{ .Release.Name }}  

 

SubChart和全局变量

所谓subchart,指的是一个chart依赖其他chart,被依赖的chart即被称为subchart。

关于subchart的几点说明:

  • subchart无需依赖父chart,其是一个完全独立操作的chart,拥有自己values和模板。
  • subchart没权限访问父chart的values,但父chart可以覆盖subchart的values。
  • 无论subchart或父chart都可以访问helm全局values。

 

操作subchart

创建subchart

创建subchart的过程与普通chart基本一致,唯一需要注意的是,subchart需要创建在父chart的charts文件夹内,举例如下:

$ cd mychart/charts
$ helm create mysubchart
Creating mysubchart

为sub chart添加模板和values:

添加 values.yaml 到 mychart/charts/mysubchart :

dessert: cake

在 mychart/charts/mysubchart/templates/configmap.yaml 中创建ConfigMap模板:

apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-cfgmap2
data:
dessert: {{ .Values.dessert }}

subchart可以单独安装:

$ helm install --dry-run --debug mychart/charts/mysubchart

父chart的values可以覆盖子chart,在mychart/values.yaml添加:

mysubchart:
dessert: ice cream

然后执行 helm install –dry-run –debug mychart命令:

# Source: mychart/charts/mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: unhinged-bee-cfgmap2
data:
dessert: ice cream

从结果可知,子chart的dessert的已由cake被替换为ice cream。

全局chart values

全部chart,包括子chart都可以访问全局chart values。一般全局chart values记录在父chart Values.global中。以mychart/values.yaml为例:

mysubchart:
dessert: ice cream

global:
salad: caesar

那么在mysubchart/templates/configmap.yaml 中,即可以通过 .Values.global.salad 访问:

apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-cfgmap2
data:
dessert: {{ .Values.dessert }}
salad: {{ .Values.global.salad }} 

父子chart共享模板

Helm中父子chart之间可以共享模板,在任何一个chart中定义的块,对其他chart均可访问。

调试模板

模板在Tiller server端渲染,而非Helm client端。经过Tiller server渲染的模板,最后发送给Kubernetes API server,而YAML文件除了格式问题外,可能还有其他多种因为会被Kubernetes API server拒绝。

  • helm lint 验证chart是否遵循了一些好的实践
  • helm install –dry-run –debug:chart发送给Tiller server并用value.yaml中的值来渲染相关模板。但实际上chart并没有安装, 只是输出了渲染后的模板。
  • helm get manifest:查看Tiller server端已安装的模板。

 

Helm Hook

前面展示了采用Helm的一次应用release的生命周期过程,涵盖了发布(helm install)、更新(helm upgrade)、回滚(helm rollback)与删除(helm delete)等部分。为了允许chart开发者在应用release的生命周期中某些关键的时间点上,执行一些操作来更好的服务于release的需求,为此Helm提供了hook机制。

举例说明什么是一个Helm hook,比如ConfigMap或Secret要先于其他chart被加载,或希望在更新/回滚/删除一个release之前能够安全的关闭服务,都可以通过Helm hook实现。

Hook和普通模板文件基本相同,但其可以通过加入一些特殊的注释(annotation)与普通模板文件加以区分,下文会介绍hook的基本格式和用法。

支持Hook类型

Helm提供了如下hook供chart开发者使用:

  • pre-install:在模板文件被渲染之后、而在Kubernetes创建任何资源创建之前执行。
  • post-install:在Kubernetes加载全部的资源之后执行。
  • pre-delete:在Kubernetes删除任何resource之前执行。
  • post-delete:在一个release的全部资源被删除之后执行。
  • pre-upgrade:在模板渲染之后,而在Kubernetes加载任何资源之前执行。
  • post-upgrade:在Kubernetes更新完全部resource之后执行。
  • pre-rollback:在模板被渲染之后、而在Kubernetes执行对全部resource的回滚之前。
  • post-rollback:在Kubernetes的全部resource已经被修改之后执行。

 

Hook与Release生命周期

上面已经提到,Hook允许chart开发者在release生命周期的一些时间点上可以执行一些操作。以helm install为例,默认情况下其基本生命周期如下:

  1. 用户执行helm install foo
  2. Chart被加载到Helm server-Tiller
  3. 经过一些用户自定义case验证,Tiller用chart中的Values.yaml等渲染foo模板
  4. Tiller加载渲染好的Kubernetes resource到Kubernetes集群
  5. Tiller返回release名称等数据给Helm client
  6. Helm Client退出

如果在install的生命周期内定义pre-install与post-install 2个hook,那么install的生命周期会变成如下序列:

  1. 用户执行helm install foo
  2. Chart被加载到Helm server-Tiller
  3. 经过一些用户自定义case验证,Tiller用chart中的Values.yaml等渲染foo模板
  4. Tiller准备执行pre-install hook(加载hook resource到Kubernetes)
  5. Tiller按hook的权重对hook进行排序,对相同权重的hook按照字母从a-z顺序排序
  6. Tiller按照权重由低到高顺序加载hook
  7. Tiller等待hook状态变为就绪(”Ready”)
  8. Tiller加载resource到Kubernetes
  9. Tiller执行post-install hook(加载hook resource)
  10. Tiller等待hook状态变为就绪
  11. Tiller返回release名称等数据给Helm client
  12. Helm Client退出

所谓hook就绪状态,取决于hook中定义的Kubernetes resource的类型,比如,对于Job类型的resource,就绪状态是job成功构建完成,如果job构建失败,release也失败了。而对于其他类型的resource,一旦resource加载(添加或更新)到Kubernetes,其状态被认为是就绪状态。

Hook权重

前面提到,一个hook可以对应多个resource,例如, secret与config map做为一个pre-install hook。

同时一个resource也可以有多个hook。

annotations:
"helm.sh/hook": post-install,post-upgrade

Helm支持一个hook中的resource设置权重,Helm推荐通过对resource设置权重的方式,按权重大小加载resource。对于权重数值可以设为负数,权重的大小由负数到正数顺序。如果未设置权重,resource的加载是顺序执行的,但执行顺序并不会保证(Helm 2.3.0开始,相同权重的执行顺序按照字母a-z顺序执行,但未来版本可能会对相同权重的资源执行顺序有所变化)。

写一个Hook

Hook与普通的Kubernetes声明文件一样,只是在文件metadata中加了特殊的注释(annotations)。但由于Hook文件是模板文件,所以其也具备全部的模板文件特性,包括从内置对象 .Values,.Release,and .Template获取渲染值等。

Hook资源删除

Helm支持2种删除hook resource策略:

  • hook-succeeded
  • hook-failed

当使用”helm.sh/hook-delete-policy” 注释(annoation)时,删除hook resource支持2种策略:”hook-succeeded” 与 “hook-failed”。当删除策略为 “hook-succeeded”时,hook执行成功后,该hook会被Tiller删除。而删除策略为 “hook-failed”时,hook在执行过程中失败后,该hook会被Tiller删除。

Hook resource删除策略举例:

annotations:
"helm.sh/hook-delete-policy": hook-succeeded

 

利用Hook处理服务启动顺序依赖

所谓服务依赖指的是启动一个服务,依赖于另外服务。这个时候我们就需要设置服务依赖关系来处理了。

对于服务启动的依赖,可以采用2种方式:

  • Init Container
  • Helm hook

理解Init Container

一个Pod中可以有一或多个Init Container。Pod的中多个Init Container启动顺序为yaml文件中的描述顺序,且串行方式启动,下一个Init/app Container必须等待上一个Init Container完成后方可启动。例如,Init Container1-> … -> Init Containern -> app Container[1-n]。Init Container1成功启动并且完成后,后续的Init Container和app Container才可以启动,如Init Container启动或执行相关检查失败,后续的init Container和应用Container将不会被执行启动命令。

因此可利用Init Container来判断app Container中被依赖的服务是否成功启动。如被依赖的app Container服务启动失败,那么利用Init Container启动失败可以阻止后续app Container服务的启动。

由于Init Container必须要在Pod状态变为Ready之前完成,所以其不需要readiness探针。另外在资源的requests与limits上与普通Container有细微差别,详见 Resources,除以上2点外,Init Container与普通Container并无明显区别。

Init Containers用途

  1. 前文已经提及,由于Init Container必须在app Containers启动之前完成,所以可利用其特性,用作服务依赖处理。比如某一个服务A,需依赖DB或memcached,那么可以利用服务A Pod的Init Container判断db/memcached是否正常提供服务,如果启动服务失败或工作异常,设置Init Container启动失败,那么Pod中的服务A就不会被启动了。
  2. 应用镜像因安全原因等没办法安装或运行的工具,可放到Init Container中运行。另外,Init Container独立于业务服务,与业务无关的工具如SED,Awk,Python,Dig等也可以按需放到Init Container之中。最后,Init Container也可以被授权访问应用Container无权访问的内容。

Init Container处理服务依赖应用举例

serviceA服务依赖serviceB,而serviceB采用上文提及Readness探针的HTTPGetAction Handler。

spec:
initContainers:
- name: init-serviceA
image: registry.docker.dev.fwmrm.net/busybox:latest
command: ['sh', '-c', "curl --connect-timeout 3 --max-time 5 --retry 10 --retry-delay 5 --retry-max-time 60 serviceB:portB/pathB/"]
containers:

Helm hook处理服务依赖

也可以通过Helm hook来实现服务启动依赖的处理:

举例:serviceA服务依赖serviceB, serviceA的pre-install hook中实现:

apiVersion: batch/v1
kind: Job
metadata:
name: "{{.Release.Name}}"
labels:
chart: "{{.Chart.Name}}-{{.Chart.Version}}"
annotations:
"helm.sh/hook": pre-install
"helm.sh/hook-weight": "-5"
spec:
template:
metadata:
  name: "{{.Release.Name}}"
  labels:
    chart: "{{.Chart.Name}}-{{.Chart.Version}}"
spec:
  restartPolicy: Never
  containers:
  - name: pre-install-job
    image: "registry.docker.dev.fwmrm.net/busybox:latest"
    command: ['sh', '-c', "curl --connect-timeout 3 --max-time 5 --retry 10 --retry-delay 5 --retry-max-time 60 --retry-connrefused serviceB:portB/pathB/"]

Helm hook的是一种串行且阻塞式操作(blocking operation),所谓阻塞指的同一个chart,在同一个时刻只有一个hook执行,其他hook以及release生命周期的其他行为活动(helm install,helm upgrade,helm rollback等)都会被阻塞(block)。而串行指的是只有当一个hook成功执行完毕后,才会执行其他hook。

以上面的pre-install hook的例子说明,pre-install hook的job会先于helm install执行,且在pre-install hook的job执行过程中,其他的release周期活动已经其他hook都不会被执行。当且仅当serviceB:portB/pathB/在指定时间内服务可达时,pre-install hook才会成功执行,余下的hook以及release其他操作才可以继续执行。

简而言之,把被依赖的服务是否成功启动的逻辑判断,放在依赖服务的pre-install hook中,利用Helm hook的串行且阻塞式操作(blocking operation)的特性,如果hook执行失败,那么对应的一次release也就失败了,这就达到了服务启动依赖的处理效果。

服务质量与资源利用率的均衡

Kubernetes做为目前主流的容器集群管理平台,需要整体统筹平台资源使用情况、公平合理的将资源分配给相关Pod容器使用,并且要保证容器生命周期内有足够的资源来保证其运行。 与此同时,由于资源发放的独占性,即资源已经分配给了某容器,同样的资源不会在分配给其他容器,对于资源利用率相对较低的容器来说,占用资源却没有实际使用(比如CPU、内存)造成了严重的资源浪费,Kubernetes需从优先级与公平性等角度提高资源的利用率。

为了实现资源被有效调度和分配的同时提高资源利用率,Kubernetes针对不同服务质量的预期,通过QoS(Quality of Service)来对Pod进行服务质量管理,提供了个采用requestslimits两种类型对资源进行分配和使用限制。对于一个Pod来说,服务质量体现在两个为2个具体的指标: CPU与内存。实际过程中,当NODE节点上内存资源紧张时,kubernetes会根据预先设置的不同QoS类别进行相应处理。

本部分主要分享内容:

  1. 资源限制的原因。
  2. 资源需求(Requests)和限制( Limits)。
  3. QoS分类以及使用场景。
  4. 使用建议。

 

设置资源限制的原因

如果未做过节点nodeSelector,亲和性(node affinity)或Pod亲和、反亲和性(pod affinity/anti-affinity)等Pod高级调度策略设置,我们没有办法指定服务部署到指定机器上,如此可能会造成cpu或内存等密集型的Pod同时分配到相同Node,造成资源竞争。另一方面,如果未对资源进行限制,一些关键的服务可能会因为资源竞争因OOM(Out of Memory)等原因被kill掉,或者被限制CPU使用。

资源需求(Requests)和限制( Limits)

对于每一个资源,container可以指定具体的资源需求(requests)和限制(limits),requests申请范围是0到node节点的最大配置,而limits申请范围是requests到无限,即0 <= requests <=Node Allocatable,requests <= limits <= Infinity。

对于CPU,如果Pod中服务使用CPU超过设置的limits,Pod不会被kill掉但会被限制。如果没有设置limits,Pod可以使用全部空闲的cpu资源。
对于内存,当一个Pod使用内存超过了设置的limits,Pod中container的进程会被kernel因OOM kill掉。当Container因为OOM被kill掉时,系统倾向于在其原所在的机器上重启该Container或本机或其他重新创建一个Pod。

QoS分类

Kubelet提供QoS服务质量管理,支持系统级别的OOM控制。在Kubernetes中,Pod的QoS级别:GuaranteedBurstable与 Best-Effort。下面对各级别分别进行相应说明:

Guaranteed:Pod中所有容器都必须统一设置limits,并且设置参数都一致,如果有一个容器要设置requests,那么所有容器都要设置,并设置参数同limits一致,那么这个Pod的QoS就是Guaranteed级别。

注:如果一个容器只指明limit而未设定request,则request的值等于limit值。

Guaranteed举例:容器只指明了limits而未指明requests

containers:
name: foo
resources:
  limits:
    cpu: 10m
    memory: 1Gi
name: bar
resources:
  limits:
    cpu: 100m
    memory: 100Mi

Burstable:Pod中只要有一个容器的requestslimits的设置不相同,该Pod的QoS即为Burstable。举例如下:

Container bar没有指定resources

containers:
name: foo
resources:
  limits:
    cpu: 10m
    memory: 1Gi
  requests:
    cpu: 10m
    memory: 1Gi

name: bar

Best-Effort:如果对于全部的resources来说requestslimits均未设置,该Pod的QoS即为Best-Effort。举例如下:

containers:
name: foo
resources:
name: bar
resources:

 

可压缩资源与不可压缩资源

Kubernetes根据资源能否伸缩进行分类,划分为可压缩资源和不可以压缩资源2种。CPU资源是目前支持的一种可压缩资源,而内存资源和磁盘资源为目前所支持的不可压缩资源。

QoS优先级

3种QoS优先级从有低到高(从左向右):

Best-Effort pods -> Burstable pods -> Guaranteed pods

 

资源回收策略

当Kubernetes集群中某个节点上可用资源比较小时,Kubernetes提供了资源回收策略保证被调度到该节点Pod服务正常运行。当节点上的内存或者CPU资源耗尽时,可能会造成该节点上正在运行的Pod服务不稳定。Kubernetes通过kubelet来进行回收策略控制,保证节点上Pod在节点资源比较小时可以稳定运行。

可压缩资源:CPU

在压缩资源部分已经提到CPU属于可压缩资源,当Pod使用超过设置的limits值,Pod中进程使用cpu会被限制,但不会被kill。

不可压缩资源:内存

Kubernetes通过cgroup给Pod设置QoS级别,当资源不足时先kill优先级低的Pod,在实际使用过程中,通过OOM分数值来实现,OOM分数值从0-1000。

OOM分数值根据OOM_ADJ参数计算得出,对于Guaranteed级别的Pod,OOM_ADJ参数设置成了-998,对于BestEffort级别的Pod,OOM_ADJ参数设置成了1000,对于Burstable级别的Pod,OOM_ADJ参数取值从2到999。对于Kuberntes保留资源,比如kubelet,docker,OOM_ADJ参数设置成了-999,表示不会被OOM kill掉。OOM_ADJ参数设置的越大,通过OOM_ADJ参数计算出来OOM分数越高,表明该Pod优先级就越低,当出现资源竞争时会越早被kill掉,对于OOM_ADJ参数是-999的表示kubernetes永远不会因为OOM而被kill掉。

QoS Pods被kill掉场景与顺序

  • Best-Effort 类型的Pods:系统消耗完全部内存时,该类型Pods会最先被kill掉。
  • Burstable类型Pods:系统消耗完全部内存,且没有Best-Effort container可以被kill时,该类型Pods会被kill掉。
  • Guaranteed Pods:系统消耗完全部内存、且没有Burstable与Best-Effort container可以被kill,该类型的Pods会被kill掉。

注:如果Pod进程因使用超过预先设定的limites而非Node资源紧张情况,系统倾向于在其原所在的机器上重启该Container或本机或其他重新创建一个Pod。

使用建议
  • 如果资源充足,可将QoS Pods类型均设置为Guaranteed。用计算资源换业务性能和稳定性,减少排查问题时间和成本。
  • 如果想更好的提高资源利用率,业务服务可以设置为Guaranteed,而其他服务根据重要程度可分别设置为BurstableBest-Effort,例如filebeat。

 

已知问题

不支持swap,当前的QoS策略假设swap已被禁止。

Q&A

Q:这个东西的本质,是不是类似把kubectl的那一套指令做了封装呢,使操作简化?

A:不是的,Helm的定位是Kubernetes应用的包管理工具,是对Kubernetes的补充而不是代替。Helm对Release提供了非常强大的版本管理、配置以及Hook等功能,这些都是原生Kubernetes不具备的。

Q:Helm是一个cli client对吧?tiller有没有API可以调用?

A:是的。tiller目前暂时不提供API调用,以Pod形式安装的Tiller service,采用的是clusterIP,然后helm client使用kubectl proxy连接。

Q:请问生产环境负载均衡和服务发现有什么好的方案?

A:对于生产环境负载均衡,可以采用HAProxy/Nginx等负载均衡器代替kube-proxy以求更好的转发性能。
对于Kubernetes服务发现:有2种方式,第一种是环境变量,第二种Kubernetes DNS。推荐用Kubernetes DNS,因为环境变量方式对Pod启动顺序有非常强的依赖,即先启动的Pod看不到在其之后启动Pod服务的环境注入信息。

Q:Kubernetes的三种健康检查类型exec,tcp,http能在一个容器中同时使用吗?它们分别的应用场景是什么?

A:可以的,Kubernetes并没有对此限制。但一般情况下,一个容器服务不会同时提供TCP和HTTP服务。
ExecAction:Container内部执行某个具体的命令,适用于非tcp或者http服务。
TCPSocketActionp:适用于TCP服务的健康检查,但TCP探测有个限制是只要TPC端口是打开状态,即使服务存在故障,健康检查检查也会通过
HTTPGetAction:适用于HTTP服务的健康检查,但使用前提是服务本身需要提供健康检查路径。

Q:使用Helm是否可以不用kubectl了?另外是否支持Windows,支持的话如何配置呢?

A:不是的。Helm是Kubernetes应用的包管理工具,对Kubernetes来说,Helm是对其Release版本管理、配置等功能的补充。
支持Windows,安装可参考:https://github.com/kubernetes/helm/releases

以上内容根据2017年09月19日晚DockOne社区微信群分享内容整理。 分享人张夏,FreeWheel 主任工程师。研究生毕业于中国科学技术大学,近10年IT领域工作经验,曾先后供职于IBM与新浪微博等公司。目前主要负责公司基于Kubernetes容器云平台建设,致力于Kubernetes容器云产品化与平台化。

 

来源:  https://www.kubernetes.org.cn/2803.html