怎样构建一个容器集群?

【编者的话】本文是Google容器技术系列博客的第二篇,第一篇中大致介绍了容器,Docker,以及Kubernetes的基本概念,这篇文章中对Kubernetes进行了相对深入的介绍,作者从Kubernetes中的一些核心概念入手,介绍了Google在构建容器集群管理系统中的一些核心要素。

在上周,来自“Google 云平台全球解决方案小组”的专家MilesWard为我们做了关于容器技术的系列博客的开篇,在前一篇博客中MilesWard大体介绍了关于容器,Docker以及Kubernetes的一些基本概念。如果你还没有阅读先前的文章,建议你先进行一些了解,这样而可以补充一些相关的基础知识,也将会帮助你更好的理解本文中介绍的内容。

这周,我们请来了Google的高级工程师,同时也是Kubernetes项目的核心成员Joe Beda。他将从更深的层面上为我们介绍Google在容器技术使用过程中的一些核心技术概念。这些概念也是Kubernetes创建基的础,理解这些概念也可以帮助我们更好地理解这一系列博客的后续文章。

怎样构建一个容器集群?

最近一段时间,容器系统相关技术迅速崛起并受到了广泛关注(比如Docker)。容器技术已经带给我们很多令人兴奋的实践。容器的打包,迁移,并且在不同的环境中运行服务的能力,可以方便地让我们管理自己的服务,从另一个角度上,这也帮助我们提高了服务的“流动性”。但是,随着用户不断将他们的服务迁移到生产环境中,新的问题也随之出现,比如具体哪个容器运行在哪台服务器上,怎样同时运行大量数目的容器,容器之间如何方便地进行跨主机通信等等,正是这些问题的出现,促使我们构建了Kubernetes。Kubernetes是一个来自Google的开源的工具包,可以帮助我们解决这些新出现的问题。

正像我们在上一篇文章中所讨论的那样,我们认为Kubernetes是一个“容器集群管理器”。许多技术人员习惯把这个领域的项目称之为“编排系统(orchestration systems)”,他们或许是想将集群的管理工作比作是交响乐的编曲。但我从不这样理解,交响乐(Orchestral music)编曲工作通常是提前根据旋律和配乐被细致地编排好,并且在表演之前,每个表演者的任务已经被明确指定好。而Kubernetes集群的管理过程,更像是一个升级版的爵士乐即兴表演。它是一个动态的系统,可以根据输入的信息和当前系统的运行环境实时做出反应。

所以,我们不禁要问,到底是哪些因素帮助我们构建了一个容器集群? 是否可以这样描述一个集群系统:这是一个动态的系统,在系统中可以放置多个容器,这些容器的状态以及容器之间的通信都可以被系统所监控。事实的确如此,一个容器集群正是由一个监控系统和一系列计算结点(不论是物理服务器或者是虚拟机)所组成的。在这篇文章的剩余部分,我们会着重探讨三方面的话题:容器集群由什么组成,容器集群应该怎样应用到我们的实际工作中,以及组成容器集群的各个要素又是怎样在一起发生作用的。此外,基于我们已有的经验,一个容器集群还应该包含一个管理层,我们将继续探索这个管理层是如何实现的。

为何要以集群的方式运行容器?

在Google,我们所构建的容器集群需要符合一系列常见的要求:集群总是可用的,可以被打补丁并且被升级,集群按需扩展,集群相关指标容易被测量(easily instrumented)和监控等等。根据容器本身的特性,服务可以通过快速、容易地方式进行部署,并且还可以将整个服务分成许多小的部分,以进行更加细粒度的操作。虽然容器化的操作一定程度上为我们提供了方便,但是为了满足我们提出的这些目标,我们仍然需要一个系统的解决方案来管理容器集群。

在Google过去的10年间,我们发现一个容器集群管理器就可以满足席上这些需求,并且这个集群管理器还可以为我们提供许多其他的好处:


  • 通过微服务的模式进行开发,可以使整个开发过程变得更容易管理。集群管理器可以使我们把一个完整的服务分成许多小的部分,这些小的部分可以互相分开,分别进行管理和扩展。这可以使我们在软件开发阶段,按照服务的复杂程度来组织我们的开发团队,通过指定好清晰的接口让不同的小的开发团队来协同开发。

  • 面对故障时候的系统自我修复。当某个服务器发生故障的时候,集群管理器可以自动地在健康的服务器上重启那些之前在发生故障的服务器上运行的任务。

  • 水平扩展变得更容易。一个容器集群可以为水平扩展提供工具,例如如果想要添加更多的计算能力,仅需要通过修改设置(重复记数)就能实现。

  • 高利用率和高效率。Google在将服务迁移到容器上之后,极大程度地增加资源的利用率和使用效率。

  • 集群和服务的运维团队的角色发生了改变。开发者可以将更多的精力集中在他们所提供的服务上,而并非集中在底层的基础设施的支持上。例如,Gmail的运维和开发团队(operations and development teams)几乎不用和集群的操作运维团队直接交流就可以完成他们工作,这种关注点的分离可以使运维团队发挥更大的作用。

现在,我们明白了,当前我们所做的事情还是很有意义的,所以让我们一起探索构成一个优秀的集群管理系统到底需要哪些要素,以及如果你希望认识到以集群的方式运行容器的优势,应该对哪些方面进行特别关注。

要素一:动态容器分配

想要构建一个成功的容器集群,你需要一点点“jazz即兴表演技巧”。你需要将你的工作任务任务打包成一个容器镜像并且明确地说明你的意图,说明要如何运行容器以及将要在哪里运行容器。集群管理系统最后会决定到底你的工作任务在哪里运行,我们把这个过程称为“集群调度”。

这并不是意味着工作任务会被随机地分配在计算结点上。正相反,在工作量被分配的时候,需要遵循一系列严格的限制,从计算机科学的角度来将,这会使得集群调度变成一个有趣而又困难的问题(注释1)。当需要调度的时候,调度器确定要把你的工作量放到一个有足够剩余空间(例如CPU,RAM,I/O,存储)的虚拟机或者是物理服务器上。但是,为了满足可靠性的目标,调度器可能需要把一系列的任务以跨主机的形式进行分配或者按一定的顺序来排列(racks in order),以此来减少相关运行时发生故障的可能性。或者一些特殊的任务会被分配在一些有某些特殊的硬件(比如GPU,本地的SSD等等)的机器上。调度器也会根据不断变化的运行环境作出反应。并且应该在任务运行失败的时候重新对任务进行调度,增加/缩小集群规模以提高效率。为了实现这个目的,我们鼓励用户避免将一个容器固定在一个服务器上。有些时候你可能需要指定“我想让某个容器在某个机器上运行”但这种情况应该比较少见。

下一个问题是:我们进行调度操作的具体对象是什么?最简单的答案就是使用单独的容器。但是在某些时候,你希望有一系列的容器在一个主机上以合作的方式在运行。例如一个数据加载器,需要一个数据库服务一起运行或者是一个log compressor/saver进程同样需要与一个服务来搭配运行。运行这些服务的容器通常需要被放在一起,并且你需要确保它们在动态配置的过程中并没有被分离开。为了实现这个目的,我们在Kubernetes中引入个一个概念:pod。一个pod是一系列容器的集合,这些容器在一起构成一个单元在服务器(也可以被称作Kubernetes结点)上被配置和调度。为了使得每次可以配置多个pod,Kubernetes采用一种可靠的方式将许多工作打包在一个结点上。

怎样构建一个容器集群?

要素二:按照集合的方式进行思考

当在一个单独的物理结点上工作时,一般的工具通常不会以批量的方式对容器进行操作。但是在容器集群上进行工作的时候,你可能希望很轻易地就能实现服务的跨结点扩展。为了实现这一目标,你需要以集合的方式进行思考,而并非像之前一样按照单例模式考虑。并且你还希望这些容器集合都可以通过很容易地方式进行配置。在Kubernets中,我们引入了两个额外的概念来管理一系列pod:label以及replication controllers。

Kubernets中的每一个pod都有一套key-value键值对与其相绑定,我们把这个键值对称为labels。你可以通过构建一个基于这些labels查询,来筛选出一系列pods。Kubernets没有一个所谓的组织pod的“正确的方式”。这完全取决于用户,只要是适合用户的组织方式就是合适的。用户可以根据应用程序的层来结构来组织,也可以根据地理位置来组织,或者是部署环境等等。实际上,因为labels是非层次结构的(non-hierarchical),你可以同时以多种方式组织你的pod。

举例来说:比如你有一个简单的服务,这个服务同时包含前端和后端两个层次。同时你还有不同的环境:测试环境,交付环境(staging environment)以及生产环境。你可以同时利用多个标签来标记你pod,比如用于生产环境的前端pod可以标记为:env=prod、tier=fe 同时,用于生产环境后端pod可以标记为env=prod、tier=be。同理,你也可以按照类似方法来标记你在测试和交付环境中使用的pod。接下来,当用户需要对集群进行操作或者检查的时候,就可以将操作范围限制在标记为env=prod的pod中,这样就可以同时看到在生产环境中的前端和后端的pod。或者你只想查看你的前端环境,此时只需要查找标记为tier=fe的pods,这样就可以查看跨越了测试、交付和生产三个不同的环境的前端pods。随着你添加更多的层次和不同的运行环境,你也可以按照自己的方式来设想和规划,按照自己的方式定义这个系统将,使其更好地满足你的需求。

怎样构建一个容器集群?

扩展

既然我们之前已经可以对拥有类似配置的物理服务器资源池进行识别和维护。我们可以参考这个功能来对容器集群进行水平扩展(即“scaling out”)。为了使这个步骤更加容易,我们在Kubernets中维护了一个helper对象,我们称其为replication controller 。它维护着一个存有pods的资源池,还有一些属性用于描述这个资源池,包括期望进行扩展的数目 replication count ,还有一个pod 模板以及一个用于进行选择/查询的label。 实际上这个对象的原理理解起来也并不困难,下面是伪代码:

object replication_controller {

property num_replicas

property template

property label_selector


runReplicationController(num_desired_pods, template, label_selector) {

loop forever {

  num_pods = length(query(label_selector))

  if num_pods > num_desired_pods {

    kill_pods(num_pods - num_desired_pods)

  } else if num_pods < num_desired_pods {

    create_pods(template, num_desired_pods - num_pods)

  }

}

}

}



对以上代码进行分析,比如,你想要使用三个pod来运行一个php前端,你可能会使用一个合适的pod模板(指向你的php容器镜像)创建一个replication controller。其中的 num_replicas的值为3。你可能会通过一个label查询 env=prod、tier=fe 来定位到一系列pod集合,之后这个replication controller对象就会对你找到的这些pod集合进行操作。通过这种方式replication controller将会很容易理解集群进行收缩/扩展之后预期的状态,它会不断对集群进行调整直至实现最后的状态。如果你希望缩小或者扩大你的服务规模,所有你需要做的仅仅是改变预期的replicaiton count,replication controller将会处理其余的问题。通过将注意力集中在系统的预期状态,我们使这个问题变得易于处理。

怎样构建一个容器集群?

要素三:集群内部服务之间的连接与通信

你已经可用上面列出的几个特性做一些很有趣的事了。任何高度并行的任务分发系统(持续集成系统,视频解码等等)在工作的时候,它们的pod之间不需要做很多的交互。然而,大多数复杂的服务更多的是小型(微型)的网络服务构成的,它们的pod之间需要进行很多的交互,按照传统的应用层级划分,每一层就像是中的一个结点。

一个集群管理系统需要一个命名解析系统,这个解析系统可以与上面所描述的几个要素一同进行工作。就像DNS提供的域名到IP地址的解析一样,这个命名服务可以将服务名称解析成一个目标,以及一些额外的需求。特别地,系统的运行状态发生变化的时候,这种变化应该很快地被系统所捕获,一个“服务名称”应该能解析一系列的targets,可能还有额外的关于这些target的元信息(比如 碎片任务shard assignment)。对于Kubernets API ,这个工作通过 label selector 以及watch API(注释2)模式来完成。这为服务发现提供了一个很轻量级的形式。

大多数的客户端将不会因为仅仅想利用新的命名API的优势就立即重写(或者从来不会被重写)大多数项目希望有一个单独的地址以及一个端口以此可以和其他层的服务进行通信,为了弥补这个不足,Kubernetse引入了服务代理的理念。这是一个简单的网络负载均衡/代理,可以为你进行名字查询并且可以以一个单独的稳定的IP/端口的形式(通过DNS)在网络上暴露给用户。当前,这个代理做简单的轮询式负载均衡跨越所有的通过label selector识别出来的后端。按照计划,Kubernets希望允许custom proxies/ambassadors ,这样可以进行更灵活的指定域的决策(关注Kubernetes roadmap来了解更多的细节)。事实上,MySQL也开始意识到ambassador的作用,它可以知道如何发送写信息流到master结点,并且将信息流读入read slave结点。

总结

现在你已经了解了以上三个关于集群管理系统的关键的要素即:动态的容器配置,容器集合的方式进行思考,集群内部服务之间的连接,是如何在一起发挥作用的。

在这个文章的开始我们提出了这样一个问题:“到底如何构建一个容器集群?”希望通过我们在上面文章中提出的信息和相关细节,你已经有了答案:简而言之,一个容器集群是一个动态的系统,这个系统可以存放和管理容器,容器以pod的形式组合在一起,在结点上运行,同时还包括内部用于相互连接和通信的信道。

当我们开始构建Kubernetes 的时候,我们的目标是 :使得Google对于容器的使用经验具体化。我们最初仅仅关注容器的调度以及动态配置,然而,当我们彻底明白在构建一个真正的服务时,不同的系统是完全必要的。我们立即发现,把其他的额外的要素加入进来是完全有必要的,比如:pods,labels以及replication controller。在我看来,这些绝对是构建一个可用的容器集群管理系统的最少的需要的模块。

Kubernetes仍然在在不断发展,但目前的发展状态还算不错,我们刚刚推出了v0.8的版本,你可以从这里下载,我们仍然在添加新的功能并且重新构建那些我们已有的功能。我们还推出了roadmap v1.0, 这个项目已经开始启动,并且一个很大的正在不断成长的社区作为合作伙伴在做贡献(就像ReaHat 、VMWare、Microsoft、IBM、CoreOS等等)还有许多用户,他们在不同的环境中来使用Kubernetes。

虽然我们在这个领域有很多实践经验,但是又很多问题Google也没有答案,可能在集群使用过程中有一些特殊的要求和特别需要考虑的地方我们在目前还没有意识到,考虑到这一点,请参与到我们正在建设的一些项目中来:Try it out、file bug reportsask for help or send a pull request (PR)

-Posted by Joe Beda, Senior Staff Engineer and Kubernetes Cofounder


注释1:这是一个传统的背包问题,在通常情况下这是一个NP难问题
注释2:“Watch API 模式”是这样一种方法: 它可以从一个服务中来分发异步事件,在通常的锁服务系统中这个很常见(zookeeper等等),这个方法最初源于 Google Chubby 的论文。客户端本质上发送并且“挂起”一个请求,直到有变化发生。客户端的请求这通常会加上版本号信息,所以客户端会对任何变化保持最新的状态。

原文链接:What makes a container cluster? (翻译:王哲 )

相关推荐