支撑多样化数据服务的K8s容器化部署实践

需求背景

SDMK(Smart Data Market)是TalkingData的数据智能市场,该平台提供了多样化的数据服务,包含API服务、SaaS服务以及Lookalike、预测引擎等人工智能算法模型服务,目地在于降低数据应用场景的难度,帮助更多的企业发现数据的深层价值。

SDMK数据平台是采用微服务架构来设计实现的,主要APP模块近二十几个,再加上第三方数据服务的适配器以及其它APP模块近七十多个,提供的REST API近成百上千个。微服务架构的应用使得每个服务都可以有专门的开发团队(或个人)来开发,开发者可以自由选择开发技术,并且每个微服务都可以独立开发、升级、拓展。因此系统具备很高的稳定性和快速迭代能力,但同时也会遇到一些问题,如负载均衡、故障重启、弹性伸缩、资源隔离等等。

因为一个微服务背后可能有多个副本实例在支撑,如何做到自动负载均衡?当其中一个副本实例宕机了,是否需要人工重启?当流量增加的时候,如何方便或者自动的增加节点,以满足相应的SLA?面对以上等等的这些问题,Kubernetes(K8s)能够很好解决,从而也使其成为企业微服务容器化的首选解决方案。

本文将简单介绍K8s核心概念,并重点介绍TalkingData SDMK采用K8s的容器化部署实践。

K8s核心概念

首先,Kubernetes(K8s)是自动化容器操作的开源平台,这些操作包括部署、调度和节点集群间扩展。如果你曾经用过Docker容器技术部署容器,那么可以将Docker看成Kubernetes内部使用的低级别组件。Kubernetes不仅仅支持Docker,还支持另一种容器技术Rocket。

使用Kubernetes可以:

  • 自动化容器的部署和复制
  • 随时扩展或收缩容器规模
  • 将容器组织成组,并且提供容器间的负载均衡
  • 很容易地升级应用程序容器的新版本
  • 提供容器弹性,如果容器失效就替换它,等等……

Master

集群拥有一个K8s Master(上图中紫色方框)。K8s Master提供集群的独特视角,并且拥有一系列组件,比如K8s API Server。API Server提供可以用来和集群交互的REST 接口。Master节点包括用来创建和复制Pod的Replication Controller。

  • K8s API Server,提供了HTTP
    Rest接口的关键服务进程,是集群中所有资源增、删、改操作的唯一入口,也是控制集群的唯一入口。
  • K8s Controller Manager,是集群中所有资源对象的运行指挥中心。
  • K8s Scheduler,是负责调度Pod资源的进程。
  • Etcd服务,提供集群中所有资源对象配置的持久化。

Node

Node作为集群中的工作节点,运行真正的应用程序,在Node上K8s管理的最小运行单元是Pod。Node上运行着K8s的Kubelet、kube-proxy服务进程,这些服务进程负责Pod的创建、启动、监控、重启、销毁、以及实现软件模式的负载均衡。节点(上图橘色方框)是物理或者虚拟机器,作为K8s worker,通常称为Minion。每个节点都运行如下K8s关键组件:

  • Kubelet:是主节点代理。
  • Kube-proxy:Service使用其将链接路由到Pod。
  • Docker或Rocket:K8s使用的容器技术来创建容器。

Pod

Pod是K8s最基本的操作单元,包含一个或多个紧密相关的容器,一个Pod可以被一个容器化的环境看作应用层的“逻辑宿主机”;一个Pod中的多个容器应用通常是紧密耦合的,Pod在Node上被创建、启动或者销毁;每个Pod里运行着一个特殊的被称之为Pause的容器,其他容器则为业务容器,这些业务容器共享Pause容器的网络栈和Volume挂载卷,因此他们之间通信和数据交换更为高效,在设计时我们可以充分利用这一特性将一组密切相关的服务进程放入同一个Pod中。

Pod的生命周期通过Replication Controller来管理,通过模板进行定义,然后分配到一个Node上运行,在Pod所包含容器运行结束后,Pod结束。K8s为Pod设计了一套独特的网络配置,包括:为每个Pod分配一个IP地址,使用Pod名作为容器间通信的主机名等。

RC(Replication Controller)

RC就是复制控制器,新一代的RC叫RS(Replication Set)其主要功能如下:

  • 确保Pod数量:RC用来管理正常运行Pod数量,一个RC可以由一个或多个Pod组成,在RC被创建后,系统会根据定义好的副本数来创建Pod数量。在运行过程中,如果Pod数量小于定义的,就会重启停止的或重新分配Pod,反之则杀死多余的。
  • 确保Pod健康:当Pod不健康,运行出错或者无法提供服务时,RC也会杀死不健康的Pod,重新创建新的。
  • 弹性伸缩
    :在业务高峰或者低峰期的时候,可以通过RC动态的调整Pod的数量来提高资源的利用率。同时,配置相应的监控功能(Hroizontal Pod
    Autoscaler),会定时自动从监控平台获取RC关联Pod的整体资源使用情况,做到自动伸缩。
  • 滚动升级:滚动升级为一种平滑的升级方式,通过逐步替换的策略,保证整体系统的稳定,在初始化升级的时候就可以及时发现和解决问题,避免问题不断扩大。

Labels

Labels以key/value的形式附加到各种对象上,如Pod、Service、RC、Node等,以识别这些对象,管理关联关系等,如Service和Pod的关联关系,有了这种关联关系,就可以通过选择器(Selector)来进行筛选那个服务和那个Pod进行关联。

Label selectors

通过label selectors,用户可以识别一些对象,它是K8s的核心Grouping Primitive。其选择器主要有以下两种:

  • equality-based
  • Set-based requirement

Deployment

部署表示用户对K8s集群的一次更新操作。部署是一个比RS应用模式更广的API对象,可以是创建一个新的服务,更新一个新的服务,也可以是滚动升级一个服务。滚动升级一个服务,实际是创建一个新的RS,然后逐渐将新RS中副本数增加到理想状态,将旧RS中的副本数减小到0的复合操作;这样一个复合操作用一个RS是不太好描述的,所以用一个更通用的Deployment来描述。以K8s的发展方向,未来对所有长期伺服型的的业务的管理,都会通过Deployment来管理。

Service

RC、RS和Deployment只是保证了支撑服务的Pod的数量,但是没有解决如何访问这些服务的问题。一个Pod只是一个运行服务的实例,随时可能在一个节点上停止,在另一个节点以一个新的IP启动一个新的Pod,因此不能以确定的IP和端口号来提供服务。要稳定地提供服务需要服务发现和负载均衡能力。服务发现完成的工作,是针对客户端访问的服务,找到对应的后端服务实例。在K8s集群中,客户端需要访问的服务就是Service对象。每个Service会对应一个集群内部有效的虚拟IP,集群内部通过虚拟IP访问一个服务。在K8s集群中服务的负载均衡是由Kube-proxy实现的。Kube-proxy是K8s集群内部的负载均衡器。它是一个分布式代理服务器,在K8s的每个节点上都有一个;这一设计体现了它的伸缩性优势,需要访问服务的节点越多,提供负载均衡能力的Kube-proxy就越多,高可用节点也随之增多。

SDMK容器化部署实践

一、服务暴露实践

上文概念中所说Service通常只是集群内部有效,在K8s的世界里,服务要对集群外暴露,通常有这么几种方式:

1. Cluseter IP

集群内的私有IP,这是默认值。

2. NodePort Service

其实质上是通过在集群的每个node上暴露一个端口,然后将这个端口映射到某个具体的service来实现的,虽然每个node的端口很多,但由于安全性和易用性(服务多了就乱了,易产生端口冲突),实际使用的并不多。

3. LoadBalancer Service

是K8s深度结合云平台的一个组件,当使用LoadBalancer service暴露服务时,实际上是通过底层平台申请创建一个负载均衡器来向外暴露服务的。这种方式多数应用于云平台上来使用。

4. Ingress Service

通过Ingress用户可以实现使用nginx等开源的反向代理负载均衡器实现对外暴露服务。它包含有三个组件:反向代理负载均衡器、Ingress Controller、Ingress。

  • 反向负载均衡器,简单来说就是nginx、apache等,部署方式也较为自由,可以RC、Deployments、DaemonSet。
  • Ingress Controller:可以简单理解为是个监视器,Ingress Crontroller通过不断地跟Kubernetes
    API打交道,实时感知后端service、pod的变化,比如增加或者减少pod,service的增加或者减少等,当这些信息发生变化时,Ingress
    Controller再结合上下文,生成配置,然后更新到反向代理负载均衡器,并重新reload配置,达到服务发现的作用。
  • Ingress:简单的说就是个规则定义;比如哪个域名对应哪个service,即哪个域名请求进来,转发给哪个service。

SDMK部署就是采用的Ingress这种方式。在K8s集群中,Ingress 是授权入站连接到达集群服务的规则集合,为其提供七层负载均衡能力,从而可以通过 Ingress 配置提供外部可访问的 URL、负载均衡、SSL、基于名称的虚拟主机等。

SDMK在部署的时候主要考虑了以下两种部署方式:

  • 共享Ingress
  • 独占Ingress

01共享Ingress

如上述部署架构图,所有的服务共享一个Ingress实例,该实例作为集群所有服务的流量入口,但问题就是,一旦流量入口出现问题,例如性能问题、可用性问题等,就会影响集群内所有服务的可用性。

02独占Ingress

如上述部署架构图,由多个独占 Ingress 实例组成统一接入层承载集群入口流量,每个微服务模块独占一个Ingress,同时也可依据后端业务流量水平扩缩容 Ingress 节点。当然如果前期的集群规模并不大,也可以采用将 Ingress 服务与业务应用混部的方式,但建议进行资源限制和隔离。

作为集群流量入口,Ingress的高可靠性显得尤为重要,出于这样的考虑,SDMK采用独占Ingress的同时,并将nginx-ingress-controller采用多副本部署的方式以及将其与后端service进行资源隔离,这样可以避免业务应用与 Ingress 服务发生资源争抢,以及单点的问题。

二、弹性伸缩实践

在K8s中,Pod是最基础的调度单位,多个Pod 可以组成一个集合,这个集合向外提供服务。这时候,有以下两种情形需要关注:

  1. 集合中的Pod可能会由于某种原因fail,这时候需要某种机制能够创建新的Pod以确保有足够数量的Pod在运行。
  2. Pod 的个数由访问请求决定。即当前实例个数不足以满足访问请求时,需要增加实例个数,反之,需要通过某种策略减少实例数。

如果人肉来实时监控实例的运行状态,手动启动新的Pod以替代fail的Pod,监控实例的负载情况,手动创建或者删除Pod,这个工作繁琐且工作量大,好在K8s已经有相应的机制来应对这种变化,这个机制就是弹性伸缩。

弹性伸缩式一种根据资源使用情况自动伸缩工作负载的方法。弹性伸缩在K8s中有两个维度:Cluster Autoscaler 和 Horizontal Pod Autoscaler ,二者一起可用于动态调整计算能力以及并行性的水平扩展能力,从而满足系统的SLA。Cluster Autaoscaler 依赖于托管集群的云提供商的底层功能,HPA就可以独立于Iaas/Paas提供商进行操作了。

简单的来讲,就是HPA实现了一个控制环,可以周期性的通过资源指标API查询特定应用的CPU、MEM等信息,而Metrics server 负责收集各个应用Pod的指标信息,再通过控制器的计算,从而实现弹性伸缩。当前主要的指标分为以下三种:

  • Resource:Pod的资源指标,例如cpu、mem等信息
  • Custom: 自定义的指标信息,例如:ingress的QPS、应用在线活跃人数等
  • External :集群指标的监控指标,通常由云厂商提供

计算的算法:

desiredReplicas = ceil[currentReplicas * ( currentMetricValue / desiredMetricValue )]

例如:

currentMetricValue 为 200m, desiredMetricValue 为100m ,那么副本的个数就会翻倍,前提是不超过你设置的最大值,如果超过,就把最大值作为当前的目标副本数目。如果算出来的值为0.5,就大于等于该数的最小整数。

在这里要说明是注意你所采用的K8s版本:

在K8s v1.1中首次引入了HPA特性,其第一个版本基于观察到的CPU利用率,后续版不断发展,也支持了基于内存使用。在K8s 1.6中引入了一个新的API自定义指标API,它允许HPA访问任意指标。在K8s1.7引入了聚合层,允许第三方应用程序通过注册为API附加组件来扩展K8s API。自定义指标API以及聚合层使得像Prometheus这样的监控系统可以向HPA控制器公开特定于应用程序的指标。

实践部分

需要在Deployment 的yaml文件中定义每个APP使用的资源情况,至于资源的限制大小需要根据实际的业务需求而定,并添加上对应探针策略。

  • Resoureces:用于限制容器的资源使用情况,主要是CPU和内存资源。
  • readnessProbe:来确定容器及服务是否已经就绪,从而可以接受流量。只有当Pod的状态,被kubelet认定为就绪状态时,才会接受流量。也即控制哪些pod应该作为service的后端。如果Pod处于非就绪状态,那么它们将会被从service得load
    balancer中移除。
  • livenessProbe:来确定何时重启容器。例如:当应用程序处于运行状态但无法做进一步操作,liveness
    探针将捕获到deadlock,就会重启该状态下的容器,使应用程序存在bug的情况下,依然能够继续运行下去。
  • Env:该选项用于设置一些环境变量,这里的JAVA_OPTS用于开启CGroup资源感知,对于JDK8以下版本是需要单独设置,否则应用程序是无法感知到容器资源限制的,而在JAVASE8u131
    和JDK9已经支持了对容器资源限制的感知能力。可以参考这篇博客:https://blogs.oracle.com/java...
1apiVersion: autoscaling/v2beta1
 2kind: HorizontalPodAutoscaler
 3metadata:
 4  name: dm-sdmk-kafka-service
 5  namespace: dm
 6spec:
 7  scaleTargetRef:
 8    apiVersion: extensions/v1beta1
 9    kind: Deployment
10    name: dm-sdmk-kafka-service
11  minReplicas: 3
12  maxReplicas: 5
13  metrics:
14  - type: Resource
15    resource:
16      name: cpu
17      targetAverageUtilization: 50
18  - type: Resource
19    resource:
20      name: memory
21      targetAverageValue: 1536Mi

HPA的实现需要在HorizontalPodAutoscaler 的Yaml文件的定义具体弹性伸缩条件以及阈值,即最少的Pod个数、最多的Pod个数,根据什么条件来伸缩,例如CPU平均利用率达到50%,或者内存平均使用超过1536M等,当相应服务的监控条件达到对应的阈值时,就会伸缩Pod。具体的设置要根据相应业务模块类型来进行设置。SDMK在部署的时候多数根据内存条件来进行伸缩的,因为多数的APP应用在流量上来的时候消耗内存比较多。

三、引流实践

当K8s环境下的SDMK已经部署完毕,并经过简单测试,处于可用状态,现在想将特定用户的流量引入到K8s环境下的SDMK,从而使其能够承担起一定的业务流量,达到物理机环境下的SDMK与K8s环境下的SDMK混合部署效果,如上图所示。

01物理机部署

如上图所示,原理物理机上的部署,是通过物理机上nginx做负载均衡的,下面挂载多个物理机上Gateway(网关)节点,Gateway是SDMK的一个网关,用于接收并处理服务调用请求的。

01并行部署

并行部署就如上图所示,将K8s环境的入口域名以Location的方式也挂载在nginx下,在nginx处来将流量区分开来,哪些用户的流量打到K8s环境,哪些用户的流量打到物理机环境。SDMK是通过在nginx中添加lua脚本来实现的,其部署情况如下图所示:

实现步骤:

  • 在Nginx上添加特定的lua脚本,通过lua脚本连接redis
  • Lua脚本获取打在nginx上每个请求中的userId
  • 将拿到的userId再和Redis中配置的用户userId进行匹配,如果是,则进入K8s环境;如果不是,则进入到物理机环境,从而将流量部分引入到K8s环境
  • 并行运行一段时间后,如果K8s环境没有问题,再逐渐增加K8s环境的流量。

实践部分

Lua脚本实现脚本:

 1local _M = {}
 2_M._VERSION = ‘0.01’
 3local function tryExec(prod,gray)
 4local redis = require ‘redis’
 5local red = redis.connect(‘172.20.33.5’, 6379, 0.2)
 6token = ngx.req.get_headers()["userId"]
 7if token == nil then
 8    return prod
 9end
10
11local temp , userId = token:match("([^,]+)-([^,]+)")
12local res = red:get(userId)
13if not res then
14    return prod
15end
16
17if res == ngx.null then
18    return prod
19else
20    return gray
21end
22end
23
24
25function _M.execByToken(prod,gray)
26local ok , msg = pcall(tryExec,prod,gray)
27if ok then
28ngx.exec(msg)
29else
30ngx.exec(prod)
31end
32end
33return _M

Nginx中添加配置:

 1location /data {
 2              set $target '';
 3              set $header_host $host;
 4              access_by_lua '
 5              package.path = "/usr/local/nginx/conf/lua/?.lua"
 6              local sdmk = require "sdmk"
 7              ngx.var.target=sdmk.tryExec("dmkdataProd","dmkdataK8s")
 8              if ngx.var.target == "dmkdataK8s" then
 9          ngx.var.header_host = "nginx-dm-talkingdata-datamarket-gateway-k8s.dm.svc.sc.tendcloud.com"
10              end
11          ';
12
13            proxy_http_version 1.1;
14            proxy_set_header   Connection          "";
15            proxy_set_header Host  $header_host;
16            proxy_set_header   X-Real-IP        $remote_addr;
17            proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
18            proxy_set_header   X-Forwarded-Server $host;
19            proxy_pass        http://$target;
20          }

这里需要注意的是需要在header_host中将域名带到K8s环境中。

四、注意事项

1.容器优雅关闭的问题

容器是否能够进行优雅的关闭,接收到SIGTERM信号,而非直接杀掉,从而使得应用程序中能够有效的处理数据、释放资源等,否则就有可能出现数据丢失等现象。

2.数据库访问限制

生产环境的数据库都有相应的访问限制,当前SDMK的数据库访问限制,是通过授权IP段形式来实现的。

3.Nginx-ingress-Controller的性能问题

无论采用共享Ingress的方式,还是采用独占Ingress方式,都需要在上生产环境之前进行压测一番,看是否能够满足系统的要求。

4.资源限制问题

  • 如果设置Pod的资源限制,应用程序能否识别容器的资源限制,即是否开启了CGroup资源感知,在JAVASE8u131
    和JDK9支持了对容器资源限制的感知能力。https://blogs.oracle.com/java...
  • 设置的资源限制是否满足应用程序的基本需要:如果不满足就会出现,容器起来,杀死,起来杀死的死循环。

5.HPA的设置条件,应当根据应用程序特定需求,来设置弹性伸缩条件

  • 计算密集型,那就以CPU使用量为条件
  • 内存密集型,那就以内存使用量为条件
  • 二者兼之的,都设置
  • 自定义metric:例如消息队列,处于等待状态的消息数量数

6.特殊镜像新需求

  • 可能运维同事,只提供标准的镜像,如果有特殊需求,就需要自己在Dockerfile中进行一定的处理。例如:
  • Springboot2 需要Jetty9.4 ,JDK1.8以上镜像
  • 生成文档应用程序,需要安装特殊的中文字体库

7.建议容器探针一定要加

  • Liveness probe: 来确定何时重启容器。例如:当应用程序处于运行状态但无法做进一步操作,liveness
    探针将捕获到deadlock,就会重启该状态下的容器,使应用程序存在bug得情况下,依然能够继续运行下去。
  • Readiness probe:
    来确定容器及服务是否已经就绪,从而可以接受流量。只有当pod的状态,被kubelet认定为就绪状态时,才会接受流量。也即控制哪些pod应该作为service的后端。如果pod处于非就绪状态,那么它们将会被从service得load
    balancer中移除。

8.Nginx引流访问K8s环境域名

  • 需要在header_host中将域名带过去
  • 并且添加DNS解析,因为K8s生成的域名,对应的IP是动态变化

本文作者:TalkingData 杨双亮

你可能感兴趣的:(容器技术)