关于Kubernetes Master高可用的一些策略

Kubernetes高可用也许是完成了初步的技术评估,打算将生产环境迁移进Kubernetes集群之前普遍面临的问题。 为了减少因为服务器当机引起的业务中断,生产环境中的业务系统往往已经做好了高可用,而当引入Kubernetes这一套新的集群管理系统之后, 服务器不再是单一的个体,位于中央位置的Kubernetes Master一旦中断服务,将导致所有Node节点均不可控,有可能造成严重的事故。

总体来讲这是一个被多次讨论,但暂时没有形成统一解决方案的话题。今天主要介绍一些Kubernetes Master高可用的策略,供大家参考。

一个小目标

高可用是复杂的系统工程。出于篇幅的考虑以及能力的限制,今天我们先关注一个小目标:所有的Kubernetes Master服务器没有单点故障,任何一台服务器当机均不影响Kubernetes的正常工作。

实现这一目标带来的直接收益是我们可以在不影响业务正常运行的前提下实现所有服务器的滚动升级,有助于完成系统组件升级以及安全补丁的下发。

为了实现没有单点故障的目标,需要为以下几个组件建立高可用方案:

这些组件的关系可参考下面这张集群架构示意图。

下面为大家逐个详细介绍各个组件的高可用策略。

etcd高可用

etcd是Kubernetes当中唯一带状态的服务,也是高可用的难点。Kubernetes选用etcd作为它的后端数据存储仓库正是看重了其使用分布式架构,没有单点故障的特性。

虽然单节点的etcd也可以正常运行。但是推荐的部署方案均是采用3个或者5个节点组成etcd集群,供Kubernetes使用。

大家常使用的kubeadm工具默认是在一个单节点上启动etcd以及所有的Master组件。虽然使用起来非常方便,但是要用到生产环境还是要注意这个节点当机的风险。

etcd的高可用基本有三种思路:

一是使用独立的etcd集群,使用3台或者5台服务器只运行etcd,独立维护和升级。甚至可以使用CoreOS的update-enginelocksmith,让服务器完全自主的完成升级。这个etcd集群将作为基石用于构建整个集群。 采用这项策略的主要动机是etcd集群的节点增减都需要显式的通知集群,保证etcd集群节点稳定可以更方便的用程序完成集群滚动升级,减轻维护负担。

二是在Kubernetes Master上用static pod的形式来运行etcd,并将多台Kubernetes Master上的etcd组成集群。 在这一模式下,各个服务器的etcd实例被注册进了Kubernetes当中,虽然无法直接使用kubectl来管理这部分实例,但是监控以及日志搜集组件均可正常工作。在这一模式运行下的etcd可管理性更强。

三是使用CoreOS提出的self-hosted etcd方案,将本应在底层为Kubernetes提供服务的etcd运行在Kubernetes之上。 实现Kubernetes对自身依赖组件的管理。在这一模式下的etcd集群可以直接使用etcd-operator来自动化运维,最符合Kubernetes的使用习惯。

这三种思路均可以实现etcd高可用的目标,但是在选择过程中却要根据实际情况做出一些判断。简单来讲预算充足但保守的项目选方案一, 想一步到位并愿意承担一定风险的项目选方案三。折中一点选方案二。各个方案的优劣以及做选择过程中的取舍在这里就不详细展开了,对这块有疑问的朋友可以私下联系交流。

kube-apiserver高可用

apiserver本身是一个无状态服务,要实现其高可用相对要容易一些,难点在于如何将运行在多台服务器上的apiserver用一个统一的外部入口暴露给所有Node节点。

说是难点,其实对于这种无状态服务的高可用,我们在设计业务系统的高可用方案时已经有了相当多的经验积累。需要注意的是apiserver所使用的SSL证书要包含外部入口的地址,不然Node节点无法正常访问apiserver。

apiserver的高可用也有三种基本思路:

一是使用外部负载均衡器,不管是使用公有云提供的负载均衡器服务或是在私有云中使用LVS或者HaProxy自建负载均衡器都可以归到这一类。 负载均衡器是非常成熟的方案,在这里略过不做过多介绍。如何保证负载均衡器的高可用,则是选择这一方案需要考虑的新问题。

二是在网络层做负载均衡。比如在Master节点上用BGPECMP,或者在Node节点上用iptables做NAT都可以实现。采用这一方案不需要额外的外部服务,但是对网络配置有一定的要求。

三是在Node节点上使用反向代理对多个Master做负载均衡。这一方案同样不需要依赖外部的组件,但是当Master节点有增减时,如何动态配置Node节点上的负载均衡器成为了另外一个需要解决的问题。

从目前各个集群管理工具的选择来看,这三种模式都有被使用,目前还没有明确的推荐方案产生。建议在公有云上的集群多考虑第一种模式,在私有云环境中由于维护额外的负载均衡器也是一项负担,建议考虑第二种或是第三种方案。

kube-controller-manager与kube-scheduler高可用

这两项服务是Master节点的一部分,他们的高可用相对容易,仅需要运行多份实例即可。这些实例会通过向apiserver中的Endpoint加锁的方式来进行leader election, 当目前拿到leader的实例无法正常工作时,别的实例会拿到锁,变为新的leader。

目前在多个Master节点上采用static pod模式部署这两项服务的方案比较常见,激进一点也可以采用self-hosted的模式,在Kubernetes之上用DaemonSet或者Deployment来部署。

Kube-dns高可用

严格来说kube-dns并不算是Master组件的一部分,因为它是可以跑在Node节点上,并用Service向集群内部提供服务的。但在实际环境中, 由于默认配置只运行了一份kube-dns实例,在其升级或是所在节点当机时,会出现集群内部dns服务不可用的情况,严重时会影响到线上服务的正常运行。

为了避免故障,请将kube-dns的replicas值设为2或者更多,并用anti-affinity将他们部署在不同的Node节点上。这项操作比较容易被疏忽,直到出现故障时才发现原来是kube-dns只运行了一份实例导致的故障。

总结

上面介绍了Kubernetes Master各个组件高可用可以采用的策略。其中etcd和kube-apiserver的高可用是整个方案的重点。由于存在多种高可用方案,集群管理员应当根据集群所处环境以及其他限制条件选择适合的方案。

这种没有绝对的通用方案,需要集群建设者根据不同的现状在多个方案中做选择的情况在Kubernetes集群建设过程中频频出现, 也是整个建设过程中最有挑战的一部分。容器网络方案的选型作为Kubernetes建设过程中需要面对的另外一个大问题也属于这种情况,今后有机会再来分享这个话题。

在实际建设过程中,在完成了上述四个组件的高可用之后,最好采取实际关机检验的方式来验证高可用方案的可靠性,并根据检验的结果不断调整和优化整个方案。

此外将高可用方案与系统自动化升级方案结合在一起考虑,实现高可用下的系统自动升级,将大大减轻集群的日常运维负担,值得投入精力去研究。

虽然本篇主要在讲Kubernetes Master高可用的方案,但需要指出的是,高可用也并不是必须的,为了实现高可用所付出的代价并不低, 需要有相应的收益来平衡。对于大量的小规模集群来说,业务系统并没有实现高可用,贸然去做集群的高可用收益有限。这时采用单Master节点的方案,做好etcd的数据备份,不失为理性的选择。

再见ELK,您好fluent-bit-aliyun

ELKElasticsearchLogstashKibana的缩写,是我们在处理日志时最常用到的方案。其中Logstash负责日志采集, Elasticsearch负责日志存储,Kibana负责日志展示。三款开源项目分工合作,提供了完整的解决方案。 此外也有使用Fluentd替换Logstash组成的EFK方案,同样也非常受欢迎。

针对不同的环境,已经有大量的文档详细介绍了安装和配置的方法。在Kubernetes环境中,管理员甚至可以使用一键部署脚本完成安装。 这些总结下来的经验极大的降低了ELK的上手门槛,运维人员可以很方便的开始将所有服务器产出日志统一的搜集起来。

但在使用了一段时间之后,随着数据量的增加以及集群规模的扩大,维护一套高效运转的ELK系统所需要付出的运维成本在逐渐增大。 管理员将面临以下几个挑战:

  • 多种不同应用的日志格式不同,需要为不同的应用配置专门的日志解析器
  • 在所有服务器上更新组件版本以及配置带来的运维工作量的增加
  • 单机版本的Elasticsearch的性能跟不上日志产出的速度,需要集群化部署ES
  • ES集群的搭建和管理过程中的复杂度对运维人员的能力要求较高。过度依赖脚本和教程的工程师可能无法顺利完成
  • ES消耗的IO,CPU,内存资源均较高。为了能够提供足够的日志处理能力,ELK所需要的计算资源投入对于小型团队来说是不小的负担
  • ELK方案中缺少日志归档,持久保存的功能。而ES的存储能力受集群规模的限制无法无限扩张。管理员需要面临删除老数据或是研发数据导出存档功能的选择

在Kubernetes环境中,使用k8s所提供的调度功能和ConfigMap所提倡的配置管理最佳实践,再配合上elasticsearch-operator这样的工具, 可以大大降低日常的运维负担,但在算力消耗以及成本增加的问题上,能够带来的改善有限。

对于小型项目,我们需要更加轻量更加经济的解决方案,将日志管理SaaS化,交给合适的供应商来提供,用户按需付费可能是更适合的解决方案。

阿里云日志服务

阿里云提供的日志服务是一套完整的日志管理解决方案。它提供的搜集、消费、存储、查询、归档等功能基本覆盖了日志管理绝大部分的需求。 具体的功能清单如下图所示,在阿里云的网站上有更加详细的介绍,这里就不进一步展开了。

阿里云日志服务对运行在阿里云上的服务器有原生的支持,但是对于Kubernetes下的容器环境的支持有限,此外对于非阿里云服务器, 用户需要自己完成配置和对接。为了解决容器环境的日志搜集以及方便大量的非阿里云用户使用阿里云日志服务, 我们为fluent-bit开发了插件来支持向阿里云日志服务输出日志。

fluent-bit-aliyun

fluent-bitfluentd来自同一家公司。fluent-bit使用C语言开发,比使用Ruby开发的fluentd性能更好,资源占用更低。 作为一个新项目,虽然目前支持的插件还没有fluentd丰富,但已经有不少团队开始在生产环境中使用它。

fluent-bit-aliyun是使用Go语言开发的fluent-bit插件,通过API调用将日志输出到阿里云日志服务。 项目地址在https://github.com/kubeup/fluent-bit-aliyun

为了方便使用,我们提供了打包好的Docker镜像,在https://hub.docker.com/r/kubeup/fluent-bit-aliyun/

在Docker环境中安装

Docker原生支持fluentd格式日志输出。我们可以在容器中运行fluent-bit-aliyun,然后在启动新容器时进行配置将日志发送给它即可。

$ docker run -d --network host -e ALIYUN_ACCESS_KEY=YOUR_ACCESS_KEY -e ALIYUN_ACCESS_KEY_SECRET=YOUR_ACCESS_KEY_SECRET -e ALIYUN_SLS_PROJECT=YOUR_PROJECT -e ALIYUN_SLS_LOGSTORE=YOUR_LOGSTORE -e ALIYUN_SLS_ENDPOINT=cn-hangzhou.log.aliyuncs.com kubeup/fluent-bit-aliyun:master /fluent-bit/bin/fluent-bit -c /fluent-bit/etc/fluent-bit-forwarder.conf -e /fluent-bit/out_sls.so
$ docker run --log-driver=fluentd -d nginx

如果在启动Docker Daemon时进行配置,还可以默认将所有日志发送到阿里云日志服务。

在Kubernetes环境中安装

在Kubernetes环境中,我们使用DaemonSet在集群中的所有Node上部署fluent-bit-aliyun,它将搜集每台服务器上所有Pod所输出的日志。 fluent-bit内置的kubernetes过滤器会将Pod的元数据附加到日志上。

首先,我们创建一个新的Secret来保存所有的配置信息:

$ kubectl create secret generic fluent-bit-config --namespace=kube-system --from-literal=ALIYUN_ACCESS_KEY=YOUR_ACCESS_KEY --from-literal=ALIYUN_ACCESS_KEY_SECRET=YOUR_ACCESS_KEY_SECRET --from-literal=ALIYUN_SLS_PROJECT=YOUR_PROJECT --from-literal=ALIYUN_SLS_LOGSTORE=YOUR_LOGSTORE --from-literal=ALIYUN_SLS_ENDPOINT=cn-hangzhou.log.aliyuncs.com

接下来部署DaemonSet:

$ kubectl create -f https://raw.githubusercontent.com/kubeup/fluent-bit-aliyun/master/fluent-bit-daemonset.yaml

我们可以使用kubectl来检查部署情况:

$ kubectl get pods --namespace=kube-system

在阿里云中查看日志

当日志发送到阿里云之后,可以通过管理界面的日志预览功能确认日志搜集和发送的正确性。

在日志查询中开启索引后,可以进行复杂的过滤和查询。

配置日志归档

阿里云还提供了在ELK方案中缺失的归档功能,只需要简单配置即可开通。

具体的设置方案以及其他相关功能,在阿里云有详细的文档说明,这里就不过多展开了。

总结

本文介绍了使用ELK管理日志可能遇到的挑战,同时提出了新的基于阿里云日志服务以及fluent-bit-aliyun管理日志的办法。 新的办法有如下特点:

  • 不再依赖Elasticsearch,减少大量计算资源消耗。fluent-bit的资源占用也远远低于fluentd,可以随着任务Pod部署
  • 当负载增加时,仅需要在阿里云添加更多的Shard即可,伸缩性更好
  • SaaS模式的计费方式,根据使用量计费,大部分情况下可以降低运行成本
  • 依赖阿里云日志服务的扩展功能,可以实现基于日志的消息处理总线,架构上更加灵活
  • 基于开源系统搭建,仅替换了fluent-bit的输出插件,可以复用input和filter插件。当需要切换后端时,前端无需修改
  • 内置归档功能。只需要简单配置,即可将日志输出到OSS长期保存
  • 同时支持阿里云ECS以及阿里云以外的服务器,对于阿里云以外的服务器,可以将阿里云日志服务作为SaaS来使用

fluent-bit-aliyun的项目地址在https://github.com/kubeup/fluent-bit-aliyun,欢迎大家试用和反馈。

基于Kubernetes的分布式压力测试方案

压力测试是用来检测系统承载能力的有效手段。在系统规模较小的时候,在一台空闲的服务器上使用abwrksiege 等工具发起一定量的并发请求即可得到一个初步的测试结果。但在系统复杂度逐步提高,特别是引入了负载均衡, 微服务等架构后,单机的压力测试方案不再可用,企业需要搭建分布式测试集群或者付费使用外部供应商提供的压力测试服务。

不管是采取自主搭建或是采用外购的手段,都会面临系统使用率不高以及成本的问题。基于Kubernetes的动态资源调度功能, 以及Kubernetes集群的动态伸缩特性,我们可以充分利用集群内的闲置计算资源,在需要进行压力测试时启动测试节点, 在测试结束后释放资源给其他业务,甚至通过集群扩容和缩容临时为压力测试提供更多的计算资源。

支持分布式部署的压力测试工具有多款,今天我们将介绍在Kubernetes集群中使用Tsung进行压力测试的方法。

Tsung

Tsung是一款使用Erlang开发的分布式压力测试系统,它支持HTTP,Jabber,MySQL等多种协议,可以用于不同场景的压力测试。 与传统的针对单一测试目标重复请求的压测系统不同,Tsung更侧重于模拟真实使用场景。测试人员指定新用户到访频率, 并设定一系列的模拟操作请求。所有的Slave节点将在Master节点的统一调度下,按照到访频率创建虚拟用户,并发送操作请求。 所有请求的耗时以及错误信息将传回Master节点用于统计和报表。

选择Tsung主要有三方面的考虑:

  • 性能优越。Erlang语言天生就是为高并发网络系统设计的。合理配置的Tsung集群可以实现100W以上的并发流量。
  • 描述式的配置方法。不论简单还是复杂,Tsung均统一使用XML文件描述整个测试步骤以及各种参数。这样可以在集群架构保持不变时完成各种测试。
  • 模拟真实用户的测试理念。在真实场景中,用户会访问系统的各项功能。只有支持模拟真实用户的压力测试系统才能比较准确的反应系统各个部分在压力下的状态,找到瓶颈环节。

由于Tsung采取的工作模式是在配置中注明Slave地址,然后由Master连上Slave完成测试,传统的部署方法是启动多台物理机或者虚拟机, 分别配置它们。在这种工作模式下,会产生大量的运维工作,同时这些计算资源在不进行测试时处于闲置状态,降低了硬件使用率。

在Kubernetes中使用容器运行Tsung

利用Kubernetes强大的调度能力,我们可以将Tsung运行在容器当中,动态的启动和删除。当需要提高测试规模时, 我们仅需要使用Archon等已有的工具对集群进行扩容,就可以很方便的一键扩容Slave的数量,几乎没有带来任何的运维负担。

以下是具体的操作流程:

创建Namespace

$ kubectl create namespace tsung

使用StatefulSet部署Tsung Slave

这里不能使用Deployment,只有使用StatefulSet才能在为每一个Pod分配独立的内部域名,供Master连接。

将以下文件保存为tsung-slave-svc.yaml

apiVersion: v1
kind: Service
metadata:
  labels:
    run: tsung-slave
  name: tsung-slave
spec:
  clusterIP: None
  selector:
    run: tsung-slave
  ports:
  - port: 22
  type: ClusterIP

将以下文件保存为tsung-slave.yaml

apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: tsung-slave
spec:
  serviceName: "tsung-slave"
  replicas: 1
  template:
    metadata:
      labels:
        run: tsung-slave
    spec:
      containers:
      - name: tsung
        image: ddragosd/tsung-docker:1.6.0
        env:
        - name: SLAVE
          value: "true"

在Kubernetes中创建相应的资源

$ kubectl create -f tsung-slave-svc.yaml --namespace tsung
$ kubectl create -f tsung-slave.yaml --namespace tsung

这里我们设置了StatefulSetserviceName字段,这样启动的Pod在集群内部就可以通过tsung-slave-0.tsung-slave.tsung.svc.cluster.local 这个域名访问到。

使用StatefulSet部署Tsung Master

与Slave类似,Master节点也要求可以在集群内部通过域名访问。所以我们依然需要使用StatefulSet来运行。

将以下文件保存为tsung-config.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: tsung-config
data:
  config.xml: |
    <?xml version="1.0" encoding="utf-8"?>
    <!DOCTYPE tsung SYSTEM "/usr/share/tsung/tsung-1.0.dtd" []>
    <tsung loglevel="warning">
      <clients>
        <client host="tsung-slave-0.tsung-slave.tsung.svc.cluster.local" />
      </clients>
      <servers>
        <server host="target" port="8000" type="tcp"/>
      </servers>
      <load>
        <arrivalphase phase="1" duration="1" unit="minute">
          <users arrivalrate="100" unit="second"/>
        </arrivalphase>
      </load>
    <sessions>
      <session name="es_load" weight="1" type="ts_http">
        <for from="1" to="10" incr="1" var="counter">
          <request> <http url="/" method="GET" version="1.1"></http> </request>
        </for>
      </session>
    </sessions>
    </tsung>

将以下文件保存为tsung-master-svc.yaml

apiVersion: v1
kind: Service
metadata:
  labels:
    run: tsung-master
  name: tsung-master
spec:
  clusterIP: None
  selector:
    run: tsung-master
  ports:
  - port: 8091
  sessionAffinity: None
  type: ClusterIP

将以下文件保存为tsung-master.yaml

apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: tsung-master
spec:
  serviceName: "tsung-master"
  replicas: 1
  template:
    metadata:
      labels:
        run: tsung-master
    spec:
      containers:
      - name: tsung
        image: ddragosd/tsung-docker:1.6.0
        env:
        - name: ERL_SSH_PORT
          value: "22"
        args:
        - -k
        - -f
        - /tsung/config.xml
        - -F
        - start
        volumeMounts:
        - mountPath: /tsung
          name: config-volume
      volumes:
      - configMap:
          name: tsung-config
        name: config-volume

在Kubernetes中创建相应的资源

$ kubectl create -f tsung-config.yaml --namespace tsung
$ kubectl create -f tsung-master-svc.yaml --namespace tsung
$ kubectl create -f tsung-master.yaml --namespace tsung

当Tsung Master的容器被启动后,它会自动开始运行压力测试。在上面的列子中,Tsung将向http://target:8000发起为期1分钟的压力测试, 在测试期间,每秒钟产生100个模拟用户,每个用户访问10次目标地址。

我们将Tsung的配置文件用ConfigMap注入到了Master容器当中,这样用户仅需要修改tsung-config.yaml的内容, 就可以方便的定义符合自己要求的测试。在实际使用过程中,用户可以自主调整测试持续时间,虚拟用户产生速度, 目标地址等参数。用户还可以通过修改tsung-slave.yamlreplicas的数值,并将更多的Slave地址加入到tsung-config.yaml当中, 来获得更多的测试资源,进一步增加负载量。

在Master的运行参数中,我们使用的-k参数将使得Master在测试完成后仍处于运行状态,这样用户可以通过8091端口访问到测试结果。

$ kubectl port-forward tsung-master-0 -n tsung 8091:8091

之后在本地通过浏览器访问http://localhost:8091即可打开Tsung内置的报表界面。如下图所示:

另外-F参数让Master使用FQDN地址访问Slave节点,这项参数非常关键,缺少它将导致Master无法正常连接上Slave。

资源回收

测试结束后,用户可以使用报表界面查看和保存结果。当所有结果被保存下来之后,可以直接删除Namespace完成资源回收。

$ kubectl delete namespace tsung

这样所有的Tsung相关配置和容器均会被删除。当下次需要测试时,可以从一个全新的状态开始新一次测试。

总结

本文主要介绍了在Kubernetes中部署Tsung这款分布式压力测试系统的方法。其中使用StatefulSet配合-F参数的方法, 使得Master和Slave可以顺利的使用域名找到对方,成功的解决了在容器中运行Tsung会遇到的访问问题。

原本需要专业的运维工程师投入不少时间才能搭建起来的Tsung测试集群,在Kubernetes中几乎可以毫不费力的启动起来, 完成测试。这种使用调度器充分利用集群空闲资源,使用后及时释放供其他系统使用的方法,也充分体现了Kubernetes的优越性。

在下一篇分享中,我们将使用本文所描述的测试系统,对主流的Python WSGI服务器进行压力测试,用以对比各个服务器的性能指标。 希望通过这种实战演示的方式,帮助大家深入了解Tsung以及Kubernetes。敬请期待。

使用IPVS实现Kubernetes入口流量负载均衡

新搭建的Kubernetes集群如何承接外部访问的流量,是刚上手Kubernetes时常常会遇到的问题。 在公有云上,官方给出了比较直接的答案,使用LoadBalancer类型的Service,利用公有云提供的负载均衡服务来承接流量, 同时在多台服务器之间进行负载均衡。而在私有环境中,如何正确的将外部流量引入到集群内部,却暂时没有标准的做法。 本文将介绍一种基于IPVS来承接流量并实现负载均衡的方法,供大家参考。

IPVS

IPVSLVS项目的一部分,是一款运行在Linux kernel当中的4层负载均衡器,性能异常优秀。 根据这篇文章的介绍,使用调优后的内核,可以轻松处理每秒10万次以上的转发请求。目前在中大型互联网项目中, IPVS被广泛的使用,用于承接网站入口处的流量。

Kubernetes Service

Service是Kubernetes的基础概念之一,它将一组Pod抽象成为一项服务,统一的对外提供服务,在各个Pod之间实现负载均衡。 Service有多种类型,最基本的ClusterIP类型解决了集群内部访问服务的需求,NodePort类型通过Node节点的端口暴露服务, 再配合上LoadBalancer类型所定义的负载均衡器,实现了流量经过前端负载均衡器分发到各个Node节点暴露出的端口, 再通过iptables进行一次负载均衡,最终分发到实际的Pod上这个过程。

在Service的Spec中,externalIPs字段平常鲜有人提到,当把IP地址填入这个字段后,kube-proxy会增加对应的iptables规则, 当有以对应IP为目标的流量发送到Node节点时,iptables将进行NAT,将流量转发到对应的服务上。一般情况下, 很少会遇到服务器接受非自身绑定IP流量的情况,所以externalIPs不常被使用,但配合网络层的其他工具,它可以实现给Service绑定外部IP的效果。

今天我们将使用externalIPs配合IPVS的DR(Direct Routing)模式实现将外部流量引入到集群内部,同时实现负载均衡。

环境搭建

为了演示,我们搭建了4台服务器组成的集群。一台服务器运行IPVS,扮演负载均衡器的作用,一台服务器运行Kubernetes Master组件, 其他两台服务器作为Node加入到Kubernetes集群当中。搭建过程这里不详细介绍,大家可以参考相关的文档。

所有服务器在172.17.8.0/24这个网段中。服务的VIP我们设定为172.17.8.201。整体架构如下图所示:

接下来让我们来配置IPVS和Kubernetes。

使用externalIPs暴露Kubernetes Service

首先在集群内部运行2个nginx Pod用作演示。

$ kubectl run nginx --image=nginx --replicas=2

再将它暴露为Service,同时设定externalIPs字段

$ kubectl expose deployment nginx --port 80 --external-ip 172.17.8.201

查看iptables配置,确认对应的iptables规则已经被加入。

$ sudo iptables -t nat -L KUBE-SERVICES -n
Chain KUBE-SERVICES (2 references)
target     prot opt source               destination
KUBE-SVC-4N57TFCL4MD7ZTDA  tcp  --  0.0.0.0/0            10.3.0.156           /* default/nginx: cluster IP */ tcp dpt:80
KUBE-MARK-MASQ  tcp  --  0.0.0.0/0            172.17.8.201         /* default/nginx: external IP */ tcp dpt:80
KUBE-SVC-4N57TFCL4MD7ZTDA  tcp  --  0.0.0.0/0            172.17.8.201         /* default/nginx: external IP */ tcp dpt:80 PHYSDEV match ! --physdev-is-in ADDRTYPE match src-type !LOCAL
KUBE-SVC-4N57TFCL4MD7ZTDA  tcp  --  0.0.0.0/0            172.17.8.201         /* default/nginx: external IP */ tcp dpt:80 ADDRTYPE match dst-type LOCAL
KUBE-SVC-NPX46M4PTMTKRN6Y  tcp  --  0.0.0.0/0            10.3.0.1             /* default/kubernetes:https cluster IP */ tcp dpt:443
KUBE-NODEPORTS  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL

配置IPVS实现流量转发

首先在IPVS服务器上,打开ipv4_forward

$ sudo sysctl -w net.ipv4.ip_forward=1

接下来加载IPVS内核模块。

$ sudo modprobe ip_vs

将VIP绑定在网卡上。

$ sudo ifconfig eth0:0 172.17.8.201 netmask 255.255.255.0 broadcast 172.17.8.255

再使用ipvsadm来配置IPVS,这里我们直接使用Docker镜像,避免和特定发行版绑定。

$ docker run --privileged -it --rm --net host luizbafilho/ipvsadm
/ # ipvsadm
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
/ # ipvsadm -A -t 172.17.8.201:80
/ # ipvsadm -a -t 172.17.8.201:80 -r 172.17.8.11:80 -g
/ # ipvsadm -a -t 172.17.8.201:80 -r 172.17.8.12:80 -g
/ # ipvsadm
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  172.17.8.201:http wlc
  -> 172.17.8.11:http             Route   1      0          0
  -> 172.17.8.12:http             Route   1      0          0

可以看到,我们成功建立了从VIP到后端服务器的转发。

验证转发效果

首先使用curl来测试是否能够正常访问nginx服务。

$ curl http://172.17.8.201
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

接下来在172.17.8.11上抓包来确认IPVS的工作情况。

$ sudo tcpdump -i any port 80
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
04:09:07.503858 IP 172.17.8.1.51921 > 172.17.8.201.http: Flags [S], seq 2747628840, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 1332071005 ecr 0,sackOK,eol], length 0
04:09:07.504241 IP 10.2.0.1.51921 > 10.2.0.3.http: Flags [S], seq 2747628840, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 1332071005 ecr 0,sackOK,eol], length 0
04:09:07.504498 IP 10.2.0.1.51921 > 10.2.0.3.http: Flags [S], seq 2747628840, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 1332071005 ecr 0,sackOK,eol], length 0
04:09:07.504827 IP 10.2.0.3.http > 10.2.0.1.51921: Flags [S.], seq 3762638044, ack 2747628841, win 28960, options [mss 1460,sackOK,TS val 153786592 ecr 1332071005,nop,wscale 7], length 0
04:09:07.504827 IP 10.2.0.3.http > 172.17.8.1.51921: Flags [S.], seq 3762638044, ack 2747628841, win 28960, options [mss 1460,sackOK,TS val 153786592 ecr 1332071005,nop,wscale 7], length 0
04:09:07.504888 IP 172.17.8.201.http > 172.17.8.1.51921: Flags [S.], seq 3762638044, ack 2747628841, win 28960, options [mss 1460,sackOK,TS val 153786592 ecr 1332071005,nop,wscale 7], length 0
04:09:07.505599 IP 172.17.8.1.51921 > 172.17.8.201.http: Flags [.], ack 1, win 4117, options [nop,nop,TS val 1332071007 ecr 153786592], length 0

可以看到,由客户端172.17.8.1发送给172.17.8.201的封包,经过IPVS的中转发送给了172.17.8.11这台服务器, 并经过NAT后发送给了10.2.0.3这个Pod。返回的封包不经过IPVS服务器直接从172.17.8.11发送给了172.17.8.1。 说明IPVS的DR模式工作正常。重复多次测试可以看到流量分别从172.17.8.11172.17.8.12进入,再分发给不同的Pod, 说明负载均衡工作正常。

与传统的IPVS DR模式配置不同的是,我们并未在承接流量的服务器上执行绑定VIP,再关闭ARP的操作。 那是因为对VIP的处理直接发生在iptables上,我们无需在服务器上运行程序来承接流量,iptables会将流量转发到对应的Pod上。

使用这种方法来承接流量,仅需要配置externalIPs为VIP即可,无需对服务器做任何特殊的设置,使用起来相当方便。

总结

在本文中演示了使用IPVS配合externalIPs实现将外部流量导入到Kubernetes集群中,并实现负载均衡的方法。 希望可以帮助大家理解IPVS和externalIPs的工作原理,以便在恰当的场景下合理使用这两项技术解决问题。 实际部署时,还需要考虑后台服务器可用性检查,IPVS节点主从备份,水平扩展等问题。在这里就不详细介绍了。

在Kubernetes中还有许多与externalIPs类似的非常用功能,有些甚至是使用Annotation来进行配置,将来有机会再进一步分享。

最后插播下广告,为了实现私有环境下的Kubernetes集群自动化部署和运维,我们为Archon系统增加了PXE管理物理机的支持, 相应的配置案例在这里。如果使用过程中有任何问题,欢迎跟我们联系。

In or Out? Kubernetes一统江湖的野心 - 写在Kubernetes 1.6即将发布之际

如一切顺利的话,Kubernetes 1.6将于3月29日发布。虽然比预期延迟了一周,但是赶在了KubeCon之前, 对Kubernetes这个规模的项目来说已经实属不易。为了庆祝1.6版本的发布,撰文一篇讲讲目前Kubernetes生态圈的现状。

自2014年发布以来,Kubernetes发展迅速,从最开始以源自Google最佳实践的容器管理平台亮相, 再与Docker SwarmMesos一起争夺容器编排领域的主导位置,到最近开始整合整个容器生态的上下游。 Kubernetes始终保持着小步快跑的节奏,在每个Release当中不断推出新的Feature。 同时Kubernetes背后的组织CNCF还在不断吸收Kubernetes生态圈中的优秀开源项目,解决最终用户在生产部署中所存在的监控、 日志搜集等需求。

如今,Kubernetes已经超越了单纯的容器编排工具,企业选择Kubernetes本质上是拥抱以Kubernetes为核心的云原生最佳实践。 其中包含了网络、存储、计算等运行资源的调度,还涵盖了监控、日志搜集、应用分发、系统架构等研发和运维的操作流程。

容器引擎接口(Container Runtime Interface)

众所周知,KubernetesDocker是既合作又竞争的关系。Kubernetes使用Docker Engine作为底层容器引擎,在容器编排领域与Docker Swarm展开竞争。为了减少对Docker的依赖,同时满足生态中其他容器引擎与Kubernetes集成的需要,Kubernetes制定了容器引擎接口CRI。 随后Kubernetes发布了cri-o项目,开始研发自己的Docker兼容容器引擎。目前已经有Docker,rkt,cri-o三款容器引擎支持CRI接口。 此外支持CRI的还有Hyper.sh主导的frakti项目以及Mirantis主导的virtlet项目, 它们为Kubernetes增加了直接管理虚拟机的能力。

CRI的发布将Docker推到了一个非常难受的位置,如果不支持CRI,面临着在Kubernetes体系当中被其他容器引擎所替换的风险。 如果支持CRI,则意味着容器引擎的接口定义被竞争对手所主导,其他容器引擎也可以通过支持CRI来挑战Docker在容器引擎领域的事实标准地位。 最终,为了不被边缘化,Docker只能妥协,选择将containerd项目捐献给CNCF。在同一天,CoreOS也宣布将rkt项目捐献给CNCF。 至此CRI成为了容器引擎接口的统一标准,今后如果有新的容器引擎推出,将首先支持CRI。

容器网络接口(Container Network Interface)

因为Kubernetes没有内置容器网络组件,所以每一个Kubernetes用户都需要进行容器网络的选型,给新用户带来了不小的挑战。 从现状来看,不内置网络组件的策略虽然增加了部署的复杂度,但给众多SDN厂商留下了足够的公平竞争空间,从中长期来讲是有利于容器网络领域的良性发展的。

1.0版本的Kubernetes没有设计专门的网络接口,依赖Docker来实现每个Pod拥有独立IP、Pod之间可以不经过NAT互访的网络需要。 随着与Docker的竞争加剧以及Docker主导的CNM接口的推出,Kubernetes也推出了自己的容器网络接口CNI

随着CNI的推出,各家SDN解决方案厂商纷纷表示支持。目前FlannelCalicoWeaveContiv这几款热门项目均已支持CNI, 用户可以根据需要为自己的Kubernetes集群选择适合的网络方案。面对CNI和CNM,主流厂商目前的选择是同时支持,但从中长期来看, 厂商一定会根据各个生态的发展进度来动态配置资源,这时Docker内置的原生网络组件有可能反而会影响和其他网络厂商的协作。

容器存储接口(Container Storage Interface)

在统一了容器引擎和容器网络之后,Kubernetes又将触角伸到了存储领域。目前还在制定过程当中的容器存储接口CSI有望复制CRI和CNI的成功, 为Kubernetes集群提供可替换的存储解决方案。不论是硬件存储厂商或是软件定义存储解决方案厂商,预计都将积极拥抱CSI。 因为不支持CSI就意味着放弃整个Kubernetes生态圈。

软件打包与分发(Packaging and Distribution)

在使用CRI,CNI,CSI解决底层运行环境的抽象以外,Kubernetes还在试图通过Helm项目以及Helm Charts来统一软件打包与分发的环节。 由于Kubernetes提供了底层的抽象,应用开发者可以利用Kubernetes内置的基础元素将上层应用打包为Chart,用户这时就能使用Helm完成一键安装以及一键升级的操作。

在系统架构越来越复杂的今天,能够方便的将复杂的分布式系统运行起来,无疑为Kubernetes的推广增加了不少亮点。 目前一些常见的开源系统,比如Redis,ElasticSearch等已经可以通过使用官方的Charts进行部署。相信未来会有更多的开源项目加入这个清单。

看到这一块商机的公司,比如CoreOS,已经推出了自己的软件仓库服务。由于这块离最终用户最近,相信未来在这一领域的竞争将会非常激烈。

云原生计算基金会(Cloud Native Computing Foundation)

前面列举的案例主要偏重技术解决方案,Kubernetes最有潜力的其实是在幕后团结容器生态中各方力量的CNCF组织。 与同期建立的Docker主导的OCI组织相比,当前CNCF不论是在项目数量,会员数量,会员质量等多个方面都明显领先。 可以说CNCF是事实上在推动整个容器生态向前发展的核心力量。

人的力量是最根本的也是最强大的,只有团结到尽可能多的玩家,才能制定出各方都能接受的标准。面对这么多的会员企业,要平衡各方的诉求实在不是容易的事情。 目前CNCF做的还不错,中立的基金会形式似乎更加容易被各方所接受。最近正在进行决策小组选举的讨论,有兴趣的朋友可以自行围观。

总结

有两句经常听到的话在Kubernetes身上得到了很好的体现,一是没有什么是不能通过增加一个抽象层解决的,二是一流的企业做标准,二流的企业做品牌,三流的企业做产品。 Kubernetes通过在具体实现上增加抽象层,试图为整个容器生态圈建立统一的标准。当标准逐步建立,用户开始依照标准选择解决方案, 将进一步强化Kubernetes位于整个容器生态核心的地位。这时容器生态的上下游将不得不面对,要么选择In拥抱Kubernetes所提出的标准, 要么选择Out被整个生态圈孤立的情况。面对这种选择,想必大部分厂商都将选择In,而更多的厂商加入将进一步强化标准的力量。

可以预见Kubernetes构建的组织、标准、开源项目三层体系,将有望统一容器生态圈的各方力量,而这种统一对最终用户是有益的。 在容器生态中的各个领域,开源的解决方案将与商业解决方案直接竞争,甚至开源解决方案之间也将展开竞争。这种竞争将促进整个容器生态的发展, 由于大家都遵守相同的标准,不论你在最初建设时选择的是哪一套解决方案,将来也可以用更新更好的方案来替换, 规避了商家绑定的风险。希望捐献给CNCF的项目将会越来越多,因为进入CNCF就意味着比其他相同功能的开源项目更加容易获得Kubernetes生态圈的认可。

最后插播一条小广告,为了解决Kubernetes与各个云平台之间的对接问题,我们开源了一款基于Kubernetes对底层云平台进行自动化运维的系统。项目叫做Archon,地址在 https://github.com/kubeup/archon 。希望Archon可以帮助Kubernetes统一对底层云平台的管理和操作方法,使得用户不论使用哪一家云平台均可以使用相同的方法进行运维和管理, 以便用户可以在多个云平台之间自由的迁移。有兴趣的朋友可以试用并给我们反馈,帮助我们完善。

3月29日,将在德国柏林举办CloudNativeCon + KubeCon Europe,届时会带来更多关于Kubernetes 1.6的介绍, 对Kubernetes感兴趣的同学可以关注,更多激动人心的消息在等着大家。

在阿里云上部署生产级别Kubernetes集群

阿里云是国内非常受欢迎的基础云平台,随着Kubernetes的普及,越来越多的企业开始筹划在阿里云上部署自己的Kubernetes集群。 本文将结合实战中总结的经验,分析和归纳一套在阿里云上部署生产级别Kubernetes集群的方法。 文中所采取的技术方案具有一定的主观性,供各位读者参考。在实践中可以根据具体使用场景进行优化。

目标

当我们刚接触Kubernetes进行测试集群的搭建时,往往会选择一篇已有的教程,照着教程完成集群搭建。我们很少去质疑教程作者每一步操作的合理性, 只想快点把集群搭建起来,早点开始实际的上手体验。

与测试集群不同,对于生产级别的部署,我们会有更加严格的要求,更加强调合理的规划以及每个步骤的合理性以及可管理性。以下是我们设定的目标:

  • 没有单点故障。任何一台服务器离线不会影响到整个集群的正常运转
  • 充分利用阿里云原生提供的SLB,云盘等工具,支持Volume挂载,LoadBalancer类型的Service等Kubernetes基础功能。 获得完整的Kubernetes使用体验。
  • 在不牺牲安全性和稳定性的前提下,尽可能降低日常运维所需要的投入,将可以由程序完成的重复性工作自动化
  • 支持随着业务规模扩展的动态集群规模扩展

因为篇幅的原因,以下内容将不作为本文的目标,留待日后再做分享:

  • 集群运行成本控制
  • 监控、日志等运维系统的搭建
  • 安全防护以及权限设计

现状

目前Kubernetes主要支持的云平台还是海外的几大主流平台,但是对比阿里云提供的基础设施,我们已经具有了基本相同的底层环境。 阿里云提供的VPC,云盘,SLB,NAS等组件,都为搭建生成级别Kubernetes集群提供了很好的支持。充分利用这些组件, 我们应该可以搭建出完整可用的Kubernetes集群。但是仔细研究Kubernetes的代码我们会发现,阿里云的CloudProvider暂时没有被合并到上游当中, 所以我们需要设计方案来解决Kubernetes暂时还没有原生阿里云支持的问题。

Kubernetes生态圈发展迅速,目前已经有了像kops这种集群自动创建工具帮助运维工程师创建集群。但是目前已有的集群创建工具均只支持国外的云平台, 使得国内云平台的用户只能采取手动搭建的办法。像kubeadm这种半自动工具还可以使用,也算是减轻了不少负担。从目前状况来看, 运维的负担依然很严重,为我们实现生产级别部署所追求的自动化、规模化的目标带来了不小的障碍。

由于网络原因造成的镜像拉取困难也给我们创建Kubernetes集群制造了不小的麻烦,好在阿里云一直在致力于解决这个问题,为用户提供了镜像拉取加速服务以及重要镜像的Mirror

另一个问题是操作系统镜像的问题,在后面分析操作系统的时候再详细展开。

架构

基于消除单点故障以及降低复杂度的考虑,我们设计了由5台服务器,分两个服务器组构成的Kubernetes集群的控制节点,并视业务需求情况由N台服务器, 分多个服务器组,构成集群的运行节点。如下图所示:

在设计这一架构时,我们考虑了以下几点:

  • 整个集群的主要构成元素为服务器组。一组服务器具有相同的硬件配置,服务相同的功能,在软件配置上也基本相同。这样为服务器的自动化管理打下了很好的基础。
  • 由3台服务器组成的etcd集群,在其中任何一台服务器离线时,均可以正常工作。为整个Kubernetes集群的数据持久化保存提供了稳定可靠的基础
  • 2台同时运行着Kubernetes核心组件kube-apiserverkube-controller-managerkube-scheduler的服务器,为整个集群的控制平面提供了高可用性。
  • 多个运行节点服务器组,有着不同的CPU,内存,磁盘配置。让我们可以灵活的根据业务对运行环境的要求来选择不同的服务器组。

集群搭建

在有了架构蓝图后,接下来让我们来实际搭建这个集群。

操作系统选型

搭建集群首先会面临的问题是,选什么配置的服务器,用什么操作系统。服务器硬件配置相对好解决,控制节点在业务量不大的时候选择入门级别的配置再随着业务增长不断提升即可, 运行节点应当根据业务需要来选择,可能要做一些尝试才能定下来最适合的硬件配置。比较困难的选择是操作系统的选型。

只要是使用较新的Kernel的Linux主机,均可以用来运行Kubernetes集群,但是发行版的选择却需要从多个方面来考虑。在这里我们选择了CoreOS作为最基础的操作系统。 做出这一选择是基于以下这些因素:

  • CoreOS是专门为运行容器设计的操作系统,非常适合用来运行Kubernetes集群
  • CoreOS去除了包管理,使用镜像升级的方式,大大简化了运维的复杂度
  • CoreOS可以使用cloud-init,方便的对服务器进行初始化
  • 阿里云提供了CoreOS的支持

CoreOS的详细介绍,大家可以参考官方的文档,在这里就不展开了。需要指出的是阿里云提供的CoreOS镜像版本较低,需要先进行升级才能正常使用,增加了不少麻烦。 希望以后阿里云能够提供最新版本的CoreOS镜像,改善这一问题。

CoreOS版本升级

由于网络的原因,CoreOS在国内不能正常进行升级。我们需要在国内搭建升级服务器。CoreRoller是一个可选项。具体的搭建可以参考相关文档,在这里就略过了。

在顺利搭建好升级服务器之后,可以修改/etc/coreos/update.conf,添加SERVER=https://YOUR_SERVER/v1/update/这一条配置,然后使用以下指令来升级服务器:

sudo systemctl restart update-engine
update_engine_client -update

我们搭建了自己的升级服务器,如果有需要的朋友可以联系我们获得服务器地址。后面所有启动的CoreOS服务器,我们均假设管理员已经提前完成了版本升级的工作, 在流程中不再重复。如果阿里云开始提供最新的CoreOS镜像,那这一步可以省略掉。

引入kube-aliyun解决兼容问题

在前面分析现状时,我们提到了阿里云的CloudProvider暂时还未被并入Kubernetes,所以我们需要额外的工具来解决原生Kubernetes与阿里云之间的兼容问题。

针对这一问题,我们开发了一款名为kube-aliyun的工具。kube-aliyuncontroller的形式运行在集群内部,提供以下的功能:

  • 配置VPC路由,使得集群内的Pod网络互通
  • 使用SLB支持LoadBalancer类型的Service
  • 使用flexv支持云盘类型的Volume的动态挂载以及解除挂载

容器网络方案选型

Kubernetes要求所有集群内部的Pod可以不经过NAT互访,所以我们需要在服务器网络之上再搭建一层容器网络。容器网络的实现方案有多种,比如常见的flannelcalico等。 在这里我们选择了更加简单的kubenet + hostroutes方案。hostroutes是我们专门配合kubenet开发的路由配置工具, 详细的信息可以参考它的Github主页,以及这篇文档

如果集群规模较小,我们还可以使用kube-aliyun的VPC路由配置功能。这样主机上不用对路由做任何的配置,所有的网络路由交给了VPC来完成,也不失为一种简单易用的方案。

SSL证书管理

SSL证书和配置是使用Kubernetes过程中非常容易出问题的点。这里推荐使用cfssl来做证书的生成和管理,主要看重了cfssl简单易用的特点,比起openssl更加容易操作和自动化。 cfssl的使用方法请参考官方的文档,这里不再重复。

在Kubernetes集群当中,我们一共需要生成4种类型的证书。另外etcd也可以通过证书进行验证和保护。出于复杂度考虑今天暂时不使用。

API Server证书

API Server证书主要用于客户端程序连接apiserver时进行加密和验证。可以使用以下模板作为CSR,填入相应的参数后生成:

{
    "CN": "${CLUSTER_NAME}",
    "hosts": [
        "kubernetes",
        "kubernetes.default",
        "kubernetes.default.svc",
        "kubernetes.default.svc.cluster.local",
        "10.3.0.1",
        "${SERVER_PRIVATE_IP}",
        "${SERVER_PUBLIC_IP}",
        "${LOAD_BALANCER_IP}"
    ],
    "key": {
        "algo": "ecdsa",
        "size": 256
    }
}

之后将以${APISERVER_PEM}${APISERVER_KEY}分别表示生成出的证书和私匙。

kubelet证书

kubelet证书用于系统组件访问kubelet内置的HTTP Server,获取运行状态或者调用kubelet提供的功能时进行加密和验证。CSR模板如下:

{
    "CN": "${SERVER_NAME}",
    "hosts": [
        "${SERVER_NAME}",
        "${SERVER_PRIVATE_IP}",
        "${SERVER_PUBLIC_IP}"
    ],
    "key": {
        "algo": "ecdsa",
        "size": 256
    }
}

之后将以${SERVER_PEM}${SERVER_KEY}分别表示生成出的证书和私匙。

Service Account证书

Service Account证书用于生成各个Namespace默认的token,以及进行token验证。在集群内部服务访问API Server时会使用这个token进行身份认证。CSR模板如下:

{
    "CN": "service-account",
    "hosts": [
        "service-account"
    ],
    "key": {
        "algo": "ecdsa",
        "size": 256
    }
}

之后将以${SERVICEACCOUNT_PEM}${SERVICEACCOUNT_KEY}分别表示生成出的证书和私匙。

kubectl证书

kubectl证书用于管理员或者用户远程访问集群,下达各种指令时的身份认证。CSR模板如下:

{
    "CN": "${USERNAME}",
    "hosts": [
        "${USERNAME}"
    ],
    "key": {
        "algo": "ecdsa",
        "size": 256
    }
}

在创建集群时并不需要这一证书,但是在集群创建完成后需要为所有用户生成证书,才能配置好本地的kubectl,获得访问集群的权限。

创建VPC

在阿里云控制台界面,可以很方便的创建VPC,选择目标可用区,并创建服务器所在网段即可。这里我们用10.99.0.0/24这个网段,读者可以根据自身业务设计选择适合的网段。 这里我们选择的网段,除去阿里云提供的服务以及内网SLB占掉的IP地址,至少有200个以上的空余地址,足以满足绝大部分场景下的规模需要。

搭建etcd集群

CoreOS对etcd有原生的支持,我们可以使用CoreOS官方提供的discovery服务快速的完成etcd集群的搭建。

首先访问https://discovery.etcd.io/new?size=3,将得到的地址以及服务器IP地址填入以下文件当中:

#cloud-config

coreos:
  etcd2:
    discovery: "https://discovery.etcd.io/<token>"
    advertise-client-urls: "http://${SERVER_PRIVATE_IP}:2379"
    initial-advertise-peer-urls: "http://${SERVER_PRIVATE_IP}:2380"
    listen-client-urls: "http://0.0.0.0:2379"
    listen-peer-urls: "http://${SERVER_PRIVATE_IP}:2380"
  units:
    - name: "start-etcd.service"
      command: "start"
      enable: true
      content: |-
        [Service]
        Type=oneshot
        ExecStart=/usr/bin/systemctl start etcd2
        [Install]
        WantedBy=multi-user.target

接下来在VPC中创建3台服务器,并在服务器上创建文件cloud-init.yaml包含上面的内容。再使用coreos-cloudinit -from-file cloud-init.yaml对服务器进行初始化。

一切顺利的话,各台服务器上的etcd服务将找到其他的节点,共同组成一个高可用的etcd集群。

搭建Master服务器组

在Master服务器组上,我们将在每台服务器上通过kubelet运行kube-aliyunkube-controller-managerkube-scheduler这几个组件。 为了简化配置流程,我们依然使用cloud-init来进行服务器初始化。将etcd服务器组的内网IP填入以下文件:

#cloud-config

coreos:
  units:
    - name: "docker.service"
      drop-ins:
        - name: "50-docker-opts.conf"
          content: |
            [Service]
            Environment=DOCKER_OPTS='--registry-mirror="https://${YOUR_MIRROR}.mirror.aliyuncs.com"'
    - name: "kubelet.service"
      command: "start"
      enable: true
      content: |-
        [Service]
        Environment=KUBELET_VERSION=v1.5.1_coreos.0
        Environment=KUBELET_ACI=kubeup.com/aci/coreos/hyperkube
        Environment="RKT_OPTS=--uuid-file-save=/var/run/kubelet-pod.uuid \
          --trust-keys-from-https \
          --volume dns,kind=host,source=/etc/resolv.conf \
          --mount volume=dns,target=/etc/resolv.conf \
          --volume var-log,kind=host,source=/var/log \
          --mount volume=var-log,target=/var/log \
          --volume lib-modules,kind=host,source=/lib/modules \
          --mount volume=lib-modules,target=/lib/modules"
        ExecStartPre=/usr/bin/systemctl stop update-engine
        ExecStartPre=/usr/bin/mkdir -p /etc/kubernetes/manifests
        ExecStartPre=/usr/bin/mkdir -p /var/log/containers
        ExecStartPre=-/usr/bin/rkt rm --uuid-file=/var/run/kubelet-pod.uuid
        ExecStart=/usr/lib/coreos/kubelet-wrapper \
          --api_servers=http://localhost:8080 \
          --register-schedulable=false \
          --allow-privileged=true \
          --config=/etc/kubernetes/manifests \
          --cluster-dns=10.3.0.10 \
          --node-ip=${SERVER_PRIVATE_IP} \
          --hostname-override=${SERVER_PRIVATE_IP} \
          --cluster-domain=cluster.local \
          --network-plugin=kubenet \
          --tls-cert-file=/etc/kubernetes/ssl/server.pem \
          --tls-private-key-file=/etc/kubernetes/ssl/server-key.pem \
          --pod-infra-container-image=registry.aliyuncs.com/archon/pause-amd64:3.0
        ExecStop=-/usr/bin/rkt stop --uuid-file=/var/run/kubelet-pod.uuid
        Restart=always
        RestartSec=10
        User=root
        [Install]
        WantedBy=multi-user.target
write_files:
  - path: "/etc/kubernetes/manifests/kube-apiserver.yaml"
    permissions: "0644"
    owner: "root"
    content: |
      apiVersion: v1
      kind: Pod
      metadata:
        name: kube-apiserver
        namespace: kube-system
      spec:
        hostNetwork: true
        containers:
        - name: kube-apiserver
          image: registry.aliyuncs.com/archon/hyperkube-amd64:v1.5.1
          command:
          - /hyperkube
          - apiserver
          - --bind-address=0.0.0.0
          - --etcd-servers=http://${ETCD_SERVER1_IP}:2379,http://${ETCD_SERVER2_IP}:2379,http://${ETCD_SERVER3_IP}:2379
          - --allow-privileged=true
          - --service-cluster-ip-range=10.3.0.0/24
          - --runtime-config=extensions/v1beta1=true,extensions/v1beta1/thirdpartyresources=true
          - --secure-port=443
          - --advertise-address=${LOAD_BALANCER_IP}
          - --admission-control=NamespaceLifecycle,NamespaceExists,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
          - --tls-cert-file=/etc/kubernetes/ssl/apiserver.pem
          - --tls-private-key-file=/etc/kubernetes/ssl/apiserver-key.pem
          - --service-account-key-file=/etc/kubernetes/ssl/serviceaccount-key.pem
          - --client-ca-file=/etc/kubernetes/ssl/ca.pem
          ports:
          - containerPort: 443
            hostPort: 443
            name: https
          - containerPort: 8080
            hostPort: 8080
            name: local
          volumeMounts:
          - mountPath: /etc/kubernetes/ssl
            name: ssl-certs-kubernetes
            readOnly: true
          - mountPath: /etc/ssl/certs
            name: ssl-certs-host
            readOnly: true
        volumes:
        - hostPath:
            path: /etc/kubernetes/ssl
          name: ssl-certs-kubernetes
        - hostPath:
            path: /usr/share/ca-certificates
          name: ssl-certs-host
  - path: "/etc/kubernetes/manifests/kube-proxy.yaml"
    permissions: "0644"
    owner: "root"
    content: |
      apiVersion: v1
      kind: Pod
      metadata:
        name: kube-proxy
        namespace: kube-system
      spec:
        hostNetwork: true
        containers:
        - name: kube-proxy
          image: registry.aliyuncs.com/archon/hyperkube-amd64:v1.5.1
          command:
          - /hyperkube
          - proxy
          - --master=http://127.0.0.1:8080
          - --proxy-mode=iptables
          securityContext:
            privileged: true
          volumeMounts:
          - mountPath: /etc/ssl/certs
            name: ssl-certs-host
            readOnly: true
        volumes:
        - hostPath:
            path: /usr/share/ca-certificates
          name: ssl-certs-host
  - path: "/etc/kubernetes/manifests/kube-controller-manager.yaml"
    permissions: "0644"
    owner: "root"
    content: |
      apiVersion: v1
      kind: Pod
      metadata:
        name: kube-controller-manager
        namespace: kube-system
      spec:
        hostNetwork: true
        containers:
        - name: kube-controller-manager
          image: registry.aliyuncs.com/archon/hyperkube-amd64:v1.5.1
          command:
          - /hyperkube
          - controller-manager
          - --master=http://127.0.0.1:8080
          - --leader-elect=true
          - --service-account-private-key-file=/etc/kubernetes/ssl/serviceaccount-key.pem
          - --root-ca-file=/etc/kubernetes/ssl/ca.pem
          - --allocate-node-cidrs=true
          - --cluster-cidr=10.2.0.0/16
          - --configure-cloud-routes=false
          livenessProbe:
            httpGet:
              host: 127.0.0.1
              path: /healthz
              port: 10252
            initialDelaySeconds: 15
            timeoutSeconds: 1
          volumeMounts:
          - mountPath: /etc/kubernetes/ssl
            name: ssl-certs-kubernetes
            readOnly: true
          - mountPath: /etc/ssl/certs
            name: ssl-certs-host
            readOnly: true
        volumes:
        - hostPath:
            path: /etc/kubernetes/ssl
          name: ssl-certs-kubernetes
        - hostPath:
            path: /usr/share/ca-certificates
          name: ssl-certs-host
  - path: "/etc/kubernetes/manifests/kube-scheduler.yaml"
    permissions: "0644"
    owner: "root"
    content: |
      apiVersion: v1
      kind: Pod
      metadata:
        name: kube-scheduler
        namespace: kube-system
      spec:
        hostNetwork: true
        containers:
        - name: kube-scheduler
          image: registry.aliyuncs.com/archon/hyperkube-amd64:v1.5.1
          command:
          - /hyperkube
          - scheduler
          - --master=http://127.0.0.1:8080
          - --leader-elect=true
          livenessProbe:
            httpGet:
              host: 127.0.0.1
              path: /healthz
              port: 10251
            initialDelaySeconds: 15
            timeoutSeconds: 1
  - path: "/etc/kubernetes/manifests/kube-aliyun.yaml"
    permissions: "0644"
    owner: "root"
    content: |
      apiVersion: v1
      kind: Pod
      metadata:
        name: aliyun-controller
        namespace: kube-system
      spec:
        hostNetwork: true
        containers:
        - name: aliyun-controller
          image: registry.aliyuncs.com/kubeup/kube-aliyun
          command:
          - /aliyun-controller
          - --server=http://127.0.0.1:8080
          - --leader-elect=true
          - --cluster-cidr=10.2.0.0/16
          env:
          - name: ALIYUN_ACCESS_KEY
            valueFrom:
              secretKeyRef:
                name: aliyun-creds
                key: accessKey
          - name: ALIYUN_ACCESS_KEY_SECRET
            valueFrom:
              secretKeyRef:
                name: aliyun-creds
                key: accessKeySecret
          - name: ALIYUN_REGION
            value: ${YOUR_VPC_REGION}
          - name: ALIYUN_VPC
            value: ${YOUR_VPC_ID}
          - name: ALIYUN_ROUTER
            value: ${YOUR_ROUTER_ID}
          - name: ALIYUN_ROUTE_TABLE
            value: ${YOUR_ROUTE_TABLE_ID}
          - name: ALIYUN_VSWITCH
            value: ${YOUR_VSWITCH_ID}
  - path: "/etc/kubernetes/ssl/ca.pem"
    permissions: "0644"
    owner: "root"
    content: |
      ${CA_PEM}
  - path: "/etc/kubernetes/ssl/apiserver.pem"
    permissions: "0644"
    owner: "root"
    content: |
      ${APISERVER_PEM}
  - path: "/etc/kubernetes/ssl/apiserver-key.pem"
    permissions: "0600"
    owner: "root"
    content: |
      ${APISERVER_KEY}
  - path: "/etc/kubernetes/ssl/serviceaccount.pem"
    permissions: "0644"
    owner: "root"
    content: |
      ${SERVICEACCOUNT_PEM}
  - path: "/etc/kubernetes/ssl/serviceaccount-key.pem"
    permissions: "0600"
    owner: "root"
    content: |
      ${SERVICEACCOUNT_KEY}
  - path: "/etc/kubernetes/ssl/server.pem"
    permissions: "0644"
    owner: "root"
    content: |
      ${SERVER_PEM}
  - path: "/etc/kubernetes/ssl/server-key.pem"
    permissions: "0600"
    owner: "root"
    content: |
      ${SERVER_KEY}

接下来使用cloud-init在新创建的2台服务器上完成服务器初始化。经过一定时间的镜像拉取,所有组件将正常启动,这时可以在主机上用kubectl来验证服务器的启动状态。

创建LoadBalancer

如果直接使用Master服务器组当中的任何一台服务器,会存在单点故障。我们使用阿里云控制台创建一个内网的SLB服务,在两台服务器之上提供一个稳定的负载均衡的apiserver。 具体的操作流程请参考相关阿里云的文档,因为apiserver暴露在443端口,我们只需要配置443端口的负载均衡即可。

搭建Node服务器组

Node服务器组的初始化与Master服务器组的初始化类似。我们可以一次性启动N台服务器,然后在每台服务器上用以下配置进行初始化:

#cloud-config

coreos:
  units:
    - name: "docker.service"
      drop-ins:
        - name: "50-docker-opts.conf"
          content: |
            [Service]
            Environment=DOCKER_OPTS='--registry-mirror="https://${YOUR_MIRROR}.mirror.aliyuncs.com"'
    - name: "kubelet.service"
      command: "start"
      enable: true
      content: |-
        [Service]
        Environment=KUBELET_VERSION=v1.5.1_coreos.0
        Environment=KUBELET_ACI=kubeup.com/aci/coreos/hyperkube
        Environment="RKT_OPTS=--uuid-file-save=/var/run/kubelet-pod.uuid \
          --trust-keys-from-https \
          --volume dns,kind=host,source=/etc/resolv.conf \
          --mount volume=dns,target=/etc/resolv.conf \
          --volume var-log,kind=host,source=/var/log \
          --mount volume=var-log,target=/var/log \
          --volume lib-modules,kind=host,source=/lib/modules \
          --mount volume=lib-modules,target=/lib/modules"
        ExecStartPre=/usr/bin/systemctl stop update-engine
        ExecStartPre=/usr/bin/mkdir -p /etc/kubernetes/manifests
        ExecStartPre=/usr/bin/mkdir -p /var/log/containers
        ExecStartPre=-/usr/bin/rkt rm --uuid-file=/var/run/kubelet-pod.uuid
        ExecStart=/usr/lib/coreos/kubelet-wrapper \
          --api_servers=https://${APISERVER_ENDPOINT}:443 \
          --register-schedulable=true \
          --allow-privileged=true \
          --pod-manifest-path=/etc/kubernetes/manifests \
          --cluster-dns=10.3.0.10 \
          --node-ip=${SERVER_PRIVATE_IP} \
          --hostname-override=${SERVER_PRIVATE_IP} \
          --cluster-domain=cluster.local \
          --network-plugin=kubenet \
          --kubeconfig=/etc/kubernetes/node-kubeconfig.yaml \
          --tls-cert-file=/etc/kubernetes/ssl/server.pem \
          --tls-private-key-file=/etc/kubernetes/ssl/server-key.pem \
          --pod-infra-container-image=registry.aliyuncs.com/archon/pause-amd64:3.0
        ExecStop=-/usr/bin/rkt stop --uuid-file=/var/run/kubelet-pod.uuid
        Restart=always
        RestartSec=10
        User=root
        [Install]
        WantedBy=multi-user.target
write_files:
  - path: "/etc/kubernetes/manifests/kube-proxy.yaml"
    permissions: "0644"
    owner: "root"
    content: |
      apiVersion: v1
      kind: Pod
      metadata:
        name: kube-proxy
        namespace: kube-system
      spec:
        hostNetwork: true
        containers:
        - name: kube-proxy
          image: registry.aliyuncs.com/archon/hyperkube-amd64:v1.5.1
          command:
          - /hyperkube
          - proxy
          - --master=https://${APISERVER_ENDPOINT}:443
          - --kubeconfig=/etc/kubernetes/node-kubeconfig.yaml
          securityContext:
            privileged: true
          volumeMounts:
          - mountPath: /etc/ssl/certs
            name: ssl-certs-host
            readOnly: true
          - mountPath: /etc/kubernetes/node-kubeconfig.yaml
            name: kubeconfig
            readOnly: true
          - mountPath: /etc/kubernetes/ssl
            name: etc-kube-ssl
            readOnly: true
        volumes:
        - hostPath:
            path: /usr/share/ca-certificates
          name: ssl-certs-host
        - hostPath:
            path: /etc/kubernetes/node-kubeconfig.yaml
          name: kubeconfig
        - hostPath:
            path: /etc/kubernetes/ssl
          name: etc-kube-ssl
  - path: "/etc/kubernetes/node-kubeconfig.yaml"
    permissions: "0644"
    owner: "root"
    content: |
      apiVersion: v1
      kind: Config
      clusters:
      - name: local
        cluster:
          certificate-authority: /etc/kubernetes/ssl/ca.pem
      users:
      - name: kubelet
        user:
          client-certificate: /etc/kubernetes/ssl/server.pem
          client-key: /etc/kubernetes/ssl/server-key.pem
      contexts:
      - context:
          cluster: local
          user: kubelet
        name: kubelet-context
      current-context: kubelet-context
  - path: "/etc/kubernetes/ssl/ca.pem"
    permissions: "0644"
    owner: "root"
    content: |
      ${CA_PEM}
  - path: "/etc/kubernetes/ssl/server.pem"
    permissions: "0644"
    owner: "root"
    content: |
      ${SERVER_PEM}
  - path: "/etc/kubernetes/ssl/server-key.pem"
    permissions: "0600"
    owner: "root"
    content: |
      ${SERVER_KEY}

部署kube-aliyun与hostroutes

kube-aliyun的Pod已经在Master服务器组的描述文件中进行了定义,但是这个Pod会因为缺少必要的Secret无法启动。 我们创建这个Secret,来激活kube-aliyun

kubectl create secret generic aliyun-creds --namespace=kube-system --from-literal=accessKey=${YOUR_ACCESS_KEY} --from-literal=accessKeySecret=${YOUR_ACCESS_KEY_SECRET}

hostroutes是以DaemonSet的形式进行部署的。使用以下定义文件:

apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  name: hostrouts
  labels:
    app: hostroutes
spec:
  template:
    metadata:
      name: hostroutes
      labels:
        app: hostroutes
    spec:
      hostNetwork: true
      containers:
        - resources:
            requests:
              cpu: 0.20
          securityContext:
            privileged: true
          image: kubeup/hostroutes
          name: hostroutes
          command: [ "/hostroutes", "--in-cluster" ]

自动化部署和运维

经过前面的手工集群搭建,我们可以发现明显的重复模式。所有的服务器我们均是使用coreos-cloudinit -from-file cloud-init.yaml这个指令完成初始化, 唯一不同的就是不同的服务器组有不同的配置文件模板。这时动手能力强的同学已经可以自己进行简单的编程来简化服务器初始化流程了。

这种初始化模式是我们有意设计的,目的是为了使用程序来实现服务器的自动化运维。我们将以上的实践抽象成了一款叫做Archon的集群管理系统, 方便用户使用描述式的方法来创建和管理集群,这样一个指令就可以完成集群扩容、升级这些日常运维工作,大大降低了运维工程师的工作负担。

关于Archon系统,这里就不详细介绍了。有兴趣的朋友可以访问项目的Github地址 https://github.com/kubeup/archon 了解更多的信息。

总结

在本文中,我们首先分析了在阿里云上部署生产级别Kubernetes所具有的优势以及面临的挑战。接着演示了基于CoreOS,使用kube-aliyun以及hostroutes来解决目前存在问题的方法。 最后提出使用archon来进行自动化集群创建以及运维的可能性。整个集群设计充分考虑了生产环境的高可用性需求,同时也使得管理员在运维时可以将出故障的服务器离线进行维护, 或者使用RollingUpdate的方法删除老服务器创建新服务器来下发更新。

由于篇幅所限,部分步骤并没有进行详细的解说,读者在阅读时可以更加注重对原理和思路的理解。在理解了设计思路之后,大可采用自动化工具进行集群的创建和运维来提升工作效率。

本文并没有简单罗列创建集群过程所使用的全部指令,让读者可以一条一条剪切复制来照着操作,而是着重理清思路,帮助读者更好的理解集群搭建的工作细节。 在介绍集群创建流程环节,我们使用了更加底层的用cloud-init的定义文件进行服务器初始化这一演示方式,以期让读者能够清楚看到创建的所有配置文件的内容, 而不是简单依赖工具进行创建,导致出现故障时无法自主解决。文中使用的CoreOS,cfssl等工具可能对部分读者来说比较陌生,在使用过程中大可使用熟悉的工具进行替换,只要整体思路保持一致即可。

实际在生产环境中部署时,还需要部署日志和监控等系统组件,才能有效的对集群进行运维和管理。这些运行在集群之上的系统将留待以后的文章再做分享。

希望本文可以帮助更多的公司更好的在阿里云上利用Kubernetes构建自己的底层架构体系。并在Kubernetes上搭建起自己的DevOps体系,提高整体的研发和运行效率。

也许您的Kubernetes集群并不需要SDN

SDNSoftware-defined networking的缩写。在许多介绍Kubernetes的文档,特别是安装文档中, 当介绍到Kubernetes所需的容器网络时常常会提到这个缩写,告知用户需要使用某种SDN技术用以解决“每个Pod有独立IP, Pod之间可以不经过NAT直接互访”这一Kubernetes集群最基本的技术要求。

大多数非网络工程师背景的技术人员对SDN这个概念会比较陌生,当读到这个段落时,往往会选择把它当作Kubernetes的底层依赖, 照着文档所推荐的流程安装一款SDN工具,比如FlannelCalicoWeave等。由于不了解这些工具的原理,同时缺乏实际的使用经验, 当出现文档以外的异常情况时,整个安装流程就卡住了。SDN俨然成为了Kubernetes大规模普及的拦路虎。

那些按照文档顺利搭建起来的集群当中,还有不少使用了并不适合该集群所处环境的SDN技术,造成了额外的运维负担以及潜在的安全风险。 让我们不得不思考一个问题,怎样才是正确的在Kubernetes集群中使用SDN技术的方法?

今天我们来详细聊聊这个话题。

结论先行

在大多数的Kubernetes集群中,都不需要使用SDN技术,Kubernetes的容器网络要求可以使用更加简单易懂的技术来实现, 只有当企业有特定的安全或者配置要求时,才需要使用SDN技术。SDN应当作为一个附加选项,用以解决特定的技术问题。

理解Kubernetes的容器网络

下图是一张Kubernetes容器网络的示意图

可以看到在图中,每台服务器上的容器有自己独立的IP段,各个服务器之间的容器可以根据目标容器的IP地址进行访问。

为了实现这一目标,重点解决以下这两点:

  • 各台服务器上的容器IP段不能重叠,所以需要有某种IP段分配机制,为各台服务器分配独立的IP段
  • 从某个Pod发出的流量到达其所在服务器时,服务器网络层应当具备根据目标IP地址将流量转发到该IP所属IP段所对应的目标服务器的能力。

总结起来,实现Kubernetes的容器网络重点需要关注两方面,分配和路由。

Flannel的工作方式

这里我们以比较常见的Flannel为例子,看看SDN系统是如何解决分配和路由的问题的。

下图是Flannel的架构示意图

可以看到Flannel依赖etcd实现了统一的配置管理机制。当一台服务器上的Flannel启动时,它会连接所配置的etcd集群, 从中取到当前的网络配置以及其他已有服务器已经分配的IP段,并从未分配的IP段中选取其中之一作为自己的IP段。 当它将自己的分配记录写入etcd之后,其他的服务器会收到这条新记录,并更新本地的IP段映射表。

Flannel的IP段分配发生在各台服务器上,由flannel进程将结果写入到etcd中。路由也由Flannel完成,网络流量先进入Flannel控制的Tunnel中, 由Flannel根据当前的IP段映射表转发到对应的服务器上。

需要指出的是Flannel有多种backend,另外新增的kube-subnet-mgr参数会导致Flannel的工作方式有所不同,在这里就不详细展开了。 有兴趣的朋友可以去查阅Flannel的文档以及源代码了解更多的细节。

更见简化的网络配置方法

Flannel的工作方式有2点是需要注意的。一是所有服务器上运行的Flannel均需要etcd的读写权限,不利于权限的隔离和安全防护。 二是许多教程中所使用的默认backend类型为vxlan,虽然它使用了内核中的vxlan模块,造成的性能损失并不大, 但是在常见的二层网络的环境中,其实并不需要使用Tunnel技术,直接利用路由就可以实现流量的转发, 这时使用hostgw模式就可以达成目标。

大部分的Kubernetes集群服务器数量并不会超过100台,不论是在物理机房当中或是利用IaaS提供的VPC技术,我们会把这些服务器均放在同一个网段, 这时我们可以去掉Flannel这一层,直接使用Kubernetes内置的kubenet功能,配合上我们为Kubernetes定制的hostroutes工具, 即可实现容器网络的要求。

kubenet

kubenetkubelet内置的网络插件中的一个,它非常的简单,会根据当前服务器对应的Node资源上的PodCIDR字段所设的IP段,配置一个本地的网络接口cbr0, 在新的Pod启动时,从IP段中分配一个空闲的IP,用它创建容器的网络接口,再将控制权交还给kubelet,完成后续的Pod创建流程。

由于kubenet会自己管理容器网络接口,所以使用kubenet时,不需要修改任何的Docker配置,仅需要在启动kubelet时,传入--network-plugin=kubenet 参数即可。

allocate-node-cidrs

allocate-node-cidrscontroller-manager的一个参数,当它和cluster-cidr参数共同使用的时候,controller-manager会为所有的Node资源分配容器IP段, 并将结果写入到PodCIDR字段。

hostroutes

hostroutes是我们为kubenet开发的一个配套小工具,它也非常的简单,它会watch所有的Node资源的变化,用所有Node资源的PodCIDR字段来配置服务器本地路由表。 这时所有Pod发出的流量将通过Linux自带的路由功能进行转发,性能优异。Linux的路由功能也是大部分技术人员已经掌握的技能,理解维护起来没有任何负担。

在这一简化的模式下,controller-manager负责分配容器IP段,kubenet负责本地网络接口的控制,hostroutes负责路由。 我们最大程度使用了Kubernetes已有的功能,并且用hostroutes来解决kubenet只管网络接口不管路由的问题。整个方案中, 需要写入权限的仅有部署在master节点的controller-manager,运行在Node节点上的kubenethostroutes均只需要读取权限即可,增强了安全性。 另外此方案将Kubernetes作为唯一的配置来源,去除了对etcd的依赖,简化了配置,降低了运维负担和安全风险。

不同的技术方案虽说实现细节不同,但是只要围绕着分配和路由这两个关键点进行比较,我们就可以更加明确的在不同方案之间进行选择。

容器网络技术方案选型推荐

任何的技术方案都离不开场景,在这里我们根据不同的场景给大家推荐几种技术方案:

  • 单服务器:不需要网络组件,使用Docker自带的网络即可
  • 小规模集群:使用kubenet + hostroutes,简单、易配合管理
  • 云环境中的小规模集群:使用kubenet + master组件上运行的网络控制器,充分利用IaaS所提供的VPC环境中的路由功能,简化网络配置
  • 服务器不在一个网段的集群:使用Flannel提供的vxlan或者其他类似的Tunnel技术
  • 安全要求高的集群:使用Calico或者Open vSwitch等支持Policy的SDN技术

总结

在本篇文章中,我们探讨了Kubernetes的容器网络的工具方式,并以Flannel为案例分析了已有的SDN解决方案,并提出了适合小规模集群的kubenet + hostroutes的解决方案。希望可以帮助读者理清在Kubernetes集群搭建过程中容器网络这一部分的思路,不再因为容器网络影响了Kubernetes的整体使用。

在实际工作中,各个企业对集群的要求都有自己的特点,技术人员需要根据企业的需要,充分比较现有的各种方案的优劣,选择最适合的方案。 直接照抄教程的搭建方式会为将来的运行埋下隐患,应当尽可能的避免。

单节点Kubernetes集群

在传统的概念当中,Docker是简单易用的,Kubernetes是复杂强大的。 深入了解之后会发现Docker的简单是因为用户可以从基本功能开始用起, 只需要一台Linux主机,运行一下apt-get install docker-engine 或者yum install docker-engine,立马就可以用docker run启动一个新的容器, 整个过程与用户之前积累的Linux软件使用体验高度一致。 而Kubernetes则要求用户要分别配置SDN,ssl证书,etcdkubeletapiservercontroller-managerschedulerproxykubectl等多个组件, 刚刚接触对架构还不了解的新人一下就懵了。 过高的早期门槛把许多对Kubernetes感兴趣的用户挡在了外面,给人留下一种难以上手的感觉。

事实上,当整个系统扩展到多个节点,需要通盘考虑身份认证,高可用, 服务发现等高级功能后,Docker Swarm与Kubernetes的复杂度是接近的。 也许我们最初的比较出现了一点偏差, 将位于更高阶的集群管理和调度系统Kubernetes和位于底层的容器引擎Docker Engine直接比较并不恰当。

现在我们了解到Kubernetes的复杂是因为它提供了更多的功能, 但是如果我们无法解决Kubernetes的上手困难问题,始终会有推广上的障碍。 对此,Kubernetes社区做出了许多努力。比如:

  • minikube可以方便的在本机用虚拟机创建一个开箱即用的Kubernetes集群
  • kubeadm可以自动化的将多台Ubuntu或者CentOS主机组建成集群
  • nanokubekid等自动初始化脚本

充分利用已有的工具, 我们可以在单台服务器上把Kubernetes的上手体验简化到与Docker接近的程度, 新用户可以不再纠结于安装和配置,尽快开始使用Kubernetes完成工作, 在业务需求增长时,再扩展集群成为多节点高可用的完整集群。

下图是一张学习曲线的示意图,可以看到当引入单节点Kubernetes作为过渡之后, 整个学习曲线更加平滑,在早期简单环境时更接近Docker, 在后期环境完整时又能够充分利用Kubernetes的优势。

有多种方法可以创建单节点的Kubernetes集群,接下来分享其中一个比较简单方便的。

准备工作

首先准备一台Linux服务器,根据Docker官方的文档安装好Docker。

接着下载localkubekubectl:

$ curl -o localkube https://storage.googleapis.com/minikube/k8sReleases/v1.5.1/localkube-linux-amd64
$ chmod +x localkube
$ curl -O https://storage.googleapis.com/kubernetes-release/release/v1.5.2/bin/linux/amd64/kubectl
$ chmod +x kubectl

localkube将Kubernetes所有的依赖程序全部打包成为了一个独立的可执行文件, 使用它可以省略掉几乎所有的配置流程,直接将Kubernetes跑起来。

目前localkube已经被合并进了minikube,最新的版本需要从minikube中下载。

kubectl是Kubernetes的客户端程序,可以用它控制集群。

启动Kubernetes

使用localkube启动集群非常简单:

$ ./localkube

当不加任何参数时,localkube会使用默认参数启动, 它所监听的localhost:8080将被用于接受控制指令。

这里并没有在后台运行localkube,如果你需要后台运行, 可以自行使用Linux上已有的各种工具完成。

使用kubectl控制集群

接下来的操作与多节点的集群完全一样。我们可以用kubectl来控制集群,比如:

$ ./kubectl run nginx --image nginx

是不是和docker run nginx几乎一样?在这里我们不详细介绍Kubernetes的操作, 请参考官方文档学习Kubernetes的使用方法。

需要指出的是,由于是极简的配置,并没有配置远程控制所需要的证书, 所以不能在本地电脑上控制这个集群,而需要ssh到服务器上进行控制。 这和默认的Docker配置是一致的。

总结

从上面的流程可以看到,Kubernetes也可以变得很简单,仅仅需要将所有组件合并到一起就可以了。 而这也恰恰是Docker选择的策略,在Docker的二进制文件中,被打包进了Docker Engine, 分布式存储,Docker Swarm等功能,使用起来只需要一个docker指令就可以完成全部的操作。 接下来需要思考的问题是,既然合并到一起会更加简单,那为什么Kubernetes会把各个组件拆开呢? 今后在详细介绍Kubernetes架构的时候会再给大家做详细的分析,这里就暂时留给大家下来自己思考了。

在刚接触Kubernetes的时候,使用all in onelocalkube是有好处的。 可以把它整个看作Kubernetes,直接上手开始学习PodServiceReplicaSet这些抽象概念, 而不用特别去关注里面的组件划分。虽然暂时只能在一台服务器上运行, 不能完全的展现Kubernetes编排和调度的能力,但是用于学习和测试已经完全足够了。

待对Kubernetes建立了基本的概念之后,再进行多节点的集群部署, 在那时再来折腾SDN,ssl证书这些更偏重运维的组件时,才会有比较合理的投入产出预期。

还在等什么?快点把你的Docker主机升级为Kubernetes主机吧。就算没有联网形成集群, 使用更高的抽象来构建你的业务,也将为今后的发展打下良好的基础。

使用Node Exporter扩展Prometheus数据

前一篇文章当中,我们介绍了在Kubernetes中使用Prometheus进行集群监控的方法,并配置了服务发现,让Prometheus从Kubernetes集群的各个组件中采集运行数据。 在之前的例子中,我们主要是通过kubelet中自带的cadvisor采集容器的运行状态。今天我们来进一步完善监控系统,使用Node Exporter采集底层服务器的运行状态。

Node Exporter简介

Exporter是Prometheus的一类数据采集组件的总称。它负责从目标处搜集数据,并将其转化为Prometheus支持的格式。 与传统的数据采集组件不同的是,它并不向中央服务器发送数据,而是等待中央服务器主动前来抓取,默认的抓取地址为http://CURRENT_IP:9100/metrics

Prometheus提供多种类型的Exporter用于采集各种不同服务的运行状态。Node Exporter顾名思义,主要用于采集底层服务器的各种运行参数。

目前Node Exporter支持几乎所有常见的监控点,比如conntrackcpudiskstatsfilesystemloadavgmeminfonetstat等。 详细的监控点列表请参考其Github repo

部署Node Exporter

在Kubernetes中部署Node Exporter非常简单,我们使用DaemonSet功能,可以非常方便的在集群内的所有主机上启动Node Exporter。 在配合上Prometheus的服务发现功能,无需额外的设置,我们就可以把这些Node Exporter Pod加入到被采集的列表当中。

将以下配置文件保存为node-exporter.yaml, 并运行 kubectl create -f node-exporter.yaml

apiVersion: v1
kind: Service
metadata:
  annotations:
    prometheus.io/scrape: 'true'
  labels:
    app: node-exporter
    name: node-exporter
  name: node-exporter
spec:
  clusterIP: None
  ports:
  - name: scrape
    port: 9100
    protocol: TCP
  selector:
    app: node-exporter
  type: ClusterIP
----
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  name: node-exporter
spec:
  template:
    metadata:
      labels:
        app: node-exporter
      name: node-exporter
    spec:
      containers:
      - image: prom/node-exporter:latest
        name: node-exporter
        ports:
        - containerPort: 9100
          hostPort: 9100
          name: scrape
      hostNetwork: true
      hostPID: true

国内的Kubernetes集群可以使用registry.cn-hangzhou.aliyuncs.com/tryk8s/node-exporter:latest 替换prom/node-exporter:latest解决image拉取的问题。

在确保所有服务器上的Node Exporter pod启动之后,我们就可以用Prometheus的web界面查看各个服务的状态了

查询服务器状态

继续使用Prometheus的web界面来验证Node Exporter的数据已经被正确的采集。

首先查询node_load1node_load5node_load15这三项CPU使用情况。

接着查询rate(node_network_receive_bytes[1m])rate(node_network_transmit_bytes[1m])这两项网络使用情况。

以及node_memory_MemAvailable所代表的剩余内存情况。

总结

通过DaemonSetService,我们向Kubernetes集群中的所有机器部署了Node Exporter。 Prometheus会自动通过服务发现找到这些Node Exporter服务,并从中采集服务器状态。

但是从使用过程中我们会发现,Prometheus的web界面比较适合用来做测试,如果日常使用还需要手动输入查询参数会很不方便。 接下来我们将使用Grafana来对Prometheus采集到的数据进行可视化展示。这样就可以在一个页面中图形化展示多个预定义的参数。

使用Prometheus完成Kubernetes集群监控

当你完成了Kubernetes集群的最初搭建后,集群监控的需求随之而来。 集群内的N台服务器在Kubernetes的管理下自动的创建和销毁着Pod, 但所有Pod和服务器的运行状态以及消耗的资源却不能方便的获得和展示, 给人一种驾驶着一辆没有仪表板的跑车在高速公路飞驰的感觉。

对于单机的Linux服务器监控,已经有了NagiosZabbix这些成熟的方案。 在Kubernetes集群中,我们使用新一代的监控系统Prometheus来完成集群的监控。

Prometheus简介

Prometheus是SoundCloud开源的一款监控软件。它的实现参考了Google内部的监控实现, 与同样源自Google的Kubernetes项目搭配起来非常合拍。同时它也是继Kubernetes之后 第二款捐赠给CNCF的开源软件。相信有CNCF的推广,它将逐步成为集群时代的重要底层组件。

Prometheus集成了数据采集,存储,异常告警多项功能,是一款一体化的完整方案。 它针对大规模的集群环境设计了拉取式的数据采集方式、多维度数据存储格式以及服务发现等创新功能。

今后我们会进一步探讨Prometheus的特性以及使用技巧,在这里我们直接演示在Kubernetes集群中 使用Prometheus的方式。

使用服务发现简化监控系统配置

与传统的先启动监控系统,然后配置所有服务器将运行数据发往监控系统不同。Prometheus 可以通过服务发现掌握集群内部已经暴露的监控点,然后主动拉取所有监控数据。 通过这样的架构设计,我们仅仅只需要向Kubernetes集群中部署一份Prometheus实例, 它就可以通过向apiserver查询集群状态,然后向所有已经支持Prometheus metrics的kubelet 获取所有Pod的运行数据。如果我们想采集底层服务器运行状态,通过DaemonSet在所有服务器上运行 配套的node-exporter之后,Prometheus就可以自动采集到新的这部分数据。

这种动态发现的架构,非常适合服务器和程序都不固定的Kubernetes集群环境,同时 也大大降低了运维的负担。

启动Prometheus服务

首先将Prometheus的配置文件,存为ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-config
data:
  prometheus.yml: |
    global:
      scrape_interval: 30s
      scrape_timeout: 30s
    scrape_configs:
    - job_name: 'prometheus'
      static_configs:
        - targets: ['localhost:9090']
    - job_name: 'kubernetes-cluster'
      scheme: https
      tls_config:
        ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
      bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
      kubernetes_sd_configs:
      - api_servers:
        - 'https://kubernetes.default.svc'
        in_cluster: true
        role: apiserver
    - job_name: 'kubernetes-nodes'
      scheme: https
      tls_config:
        ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
        insecure_skip_verify: true
      bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
      kubernetes_sd_configs:
      - api_servers:
        - 'https://kubernetes.default.svc'
        in_cluster: true
        role: node
      relabel_configs:
      - action: labelmap
        regex: __meta_kubernetes_node_label_(.+)
    - job_name: 'kubernetes-service-endpoints'
      scheme: https
      kubernetes_sd_configs:
      - api_servers:
        - 'https://kubernetes.default.svc'
        in_cluster: true
        role: endpoint
      relabel_configs:
      - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
        action: keep
        regex: true
      - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme]
        action: replace
        target_label: __scheme__
        regex: (https?)
      - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path]
        action: replace
        target_label: __metrics_path__
        regex: (.+)
      - source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port]
        action: replace
        target_label: __address__
        regex: (.+)(?::\d+);(\d+)
        replacement: $1:$2
      - action: labelmap
        regex: __meta_kubernetes_service_label_(.+)
      - source_labels: [__meta_kubernetes_service_namespace]
        action: replace
        target_label: kubernetes_namespace
      - source_labels: [__meta_kubernetes_service_name]
        action: replace
        target_label: kubernetes_name
    - job_name: 'kubernetes-services'
      scheme: https
      metrics_path: /probe
      params:
        module: [http_2xx]
      kubernetes_sd_configs:
      - api_servers:
        - 'https://kubernetes.default.svc'
        in_cluster: true
        role: service
      relabel_configs:
      - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_probe]
        action: keep
        regex: true
      - source_labels: [__address__]
        target_label: __param_target
      - target_label: __address__
        replacement: blackbox
      - source_labels: [__param_target]
        target_label: instance
      - action: labelmap
        regex: __meta_kubernetes_service_label_(.+)
      - source_labels: [__meta_kubernetes_service_namespace]
        target_label: kubernetes_namespace
      - source_labels: [__meta_kubernetes_service_name]
        target_label: kubernetes_name
    - job_name: 'kubernetes-pods'
      scheme: https
      kubernetes_sd_configs:
      - api_servers:
        - 'https://kubernetes.default.svc'
        in_cluster: true
        role: pod
      relabel_configs:
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
        action: keep
        regex: true
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
        action: replace
        target_label: __metrics_path__
        regex: (.+)
      - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
        action: replace
        regex: (.+):(?:\d+);(\d+)
        replacement: ${1}:${2}
        target_label: __address__
      - action: labelmap
        regex: __meta_kubernetes_pod_label_(.+)
      - source_labels: [__meta_kubernetes_pod_namespace]
        action: replace
        target_label: kubernetes_namespace
      - source_labels: [__meta_kubernetes_pod_name]
        action: replace
        target_label: kubernetes_pod_name

将以上配置文件文件保存为prometheus-config.yaml,再执行

$ kubectl create -f prometheus-config.yaml

接下来通过Deployment部署Prometheus

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    name: prometheus-deployment
  name: prometheus
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: prometheus
    spec:
      containers:
      - image: prom/prometheus:v1.0.1
        name: prometheus
        command:
        - "/bin/prometheus"
        args:
        - "-config.file=/etc/prometheus/prometheus.yml"
        - "-storage.local.path=/prometheus"
        - "-storage.local.retention=24h"
        ports:
        - containerPort: 9090
          protocol: TCP
        volumeMounts:
        - mountPath: "/prometheus"
          name: data
        - mountPath: "/etc/prometheus"
          name: config-volume
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
          limits:
            cpu: 500m
            memory: 2500Mi
      volumes:
      - emptyDir: {}
        name: data
      - configMap:
          name: prometheus-config
        name: config-volume

将以上文件保存为prometheus-deployment.yaml,接着运行

$ kubectl create -f prometheus-deployment.yaml

如果是在国内环境,可以用registry.cn-hangzhou.aliyuncs.com/tryk8s/prometheus:v1.0.1 代替上面的image设置。

为了在本地访问Prometheus的web界面,我们利用kubectl port-forward将它暴露到本地

$ POD=`kubectl get pod -l app=prometheus -o go-template --template '{{range .items}}{{.metadata.name}}{{end}}'`
$ kubectl port-forward $POD 9090:9090

这时我们用浏览器访问http://127.0.0.1:9090来访问Prometheus的界面,查看已经搜集到的数据。

查询监控数据

Prometheus提供API方式的数据查询接口,用户可以使用query语言完成复杂的查询任务。 为了方便调试,web界面上提供基本的查询和图形化展示功能,我们用它做一些基本的查询。

首先查询每个容器的内存使用情况,查询container_memory_usage_bytes{image=~".+"}

接下来查询各个Pod的CPU使用情况,查询条件是sum(rate(container_cpu_usage_seconds_total{kubernetes_pod_name=~".+", job="kubernetes-nodes"}[1m])) by (kubernetes_pod_name, kubernetes_namespace)

更多的查询条件可以参考Prometheus的文档,将来也会逐步介绍,这里就不详细展开了。

总结

通过向Kubernetes集群内部署Prometheus,我们在不修改任何集群配置的状态下,利用Prometheus 的服务发现功能获得了基本的集群监控能力,并通过web界面对监控系统获取到的数据做了基本的查询。

未来我们将进一步完善Prometheus的使用:

  • 增加更多的监控数据源
  • 使用Grafana图形化的展示搜集到的监控数据
  • 使用AlertManager实现异常提醒

在本文的写作过程中,CoreOS在博客上发布了一篇相同主题的文章, 本文的Prometheus配置文件根据CoreOS的分享做了修改。