转载

不可变层,不(仅仅)是基础设施

原文由Kevin Fishner发布于2015年12月31日

不可变的基础设施是一个有效的应用交付流程 ,但部署的过程会非常缓慢,且往往不能够充分利用资源。每次应用更新就构建镜像并创建新主机会导致每次部署都需要花费10分钟甚至更多。如果主机上只有一个服务,通常不能完全利用处理器、内存和磁盘资源。如果只有20%的资源使用率,将会造成严重的资源浪费。像 Nomad 、 Mesos 和 Kubernetes 这些调度器可以将应用和基础设施细分成不可变层,这将能够加快部署时间和增加资源密度,同时能够享受到不可变架构的益处。

调度器能够打包应用,例如Docker容器、JAR、binary等等,然后发布到主机上。多个应用可以被放置到同一个主机上以增加资源密度,从而节约成本。例如,如果之前以20%的利用率使用了100个主机,可以换成以80%的资源利用率使用25个主机,这样就能减少75%的基础设施费用。由于部署过程仅仅是将打包好的应用放置到已经存在的主机上,部署的过程将会非常快。如果还要构建机器镜像,那就可能需要10分钟以上,构建应用的时间通常少于5分钟。相较于花费数分钟才能配置一台新机器,调度应用的时间是秒级的。完整的部署过程能够从10分钟以上减少到2至3分钟。一旦应用打包完成,部署过程仅需数秒钟。这就意味着开发团队可以更快地应对变化和迭代应用。更重要的是,应用程序仍然是不可变对象,即它是可以进行版本控制和审计的。

基础设施和应用不变性

一个调度器的工作流程能够在两个层级促进不可变基础设施。第一个是机器级,那就是使用不可变的机器镜像。第二个是应用级,即配置不可变的应用程序包。HashiCorp工具的生态系统就是关于如何管理一个应用交付流程并同时在应用级和机器级促进不变性的例子。

首先,使用 Pacher 和 Terraform 来配置基础环境。使用Packer为Consul和Nomad服务器构建镜像,然后通过Terraform创建Consul和Nomad集群。这样环境已经就绪,应用就能被Nomad调度并被Consol发现。如果没有像Consul这样的服务发现工具,又没有其他的发现方式,那么数以千计的应用程序只能被调度,而他们的位置将是未知的。

然后,需要配置服务器池,以给应用提供资源。使用Pacher配置含有通用包、一个Nomad代理和一个Consul代理的基础镜像。Terraform利用先前的基础镜像配置好服务器池,同时创建好网络规则。工作流通常不被定义为有状态的服务,但许多公司在机器级管理存储,而不是应用级。Terraform同样在机器级配置存储。

一旦主机配置好后,每台主机中的Nomad代理将会报告该主机中的资源总量和容量给中央Nomad服务器。Nomad服务器保存服务器集群的总状态——可用资源和每台主机的容量,这些信息将被用来做调度决策。现在环境和Nomad服务器已经准备好接受工作和调度应用了。

为了充分利用应用开发和部署工作流,开发人员首先在本地环境中工作,一旦提交的修改被合并到主分支,Pacher将会创建应用包,然后将自动被Nomad部署。当Nomnad将Pacher构建好的应用部署到主机上(详细信息请看下一节调度策略),它将把应用注册到本地的Consul代理中。由于应用程序是动态部署的,所以由Consul来处理服务发现是必不可少的。否则数以千计的应用被部署后就无法得知其IP和端口。Vault能够安全地存储Packer、Terraform和Nomad配置环境和部署应用时使用的密钥。

从开发人员的角度来看,他们主要关注的是在本地测试后使用CI工具集成。部署过程能够完全自动化。操作人员通常负责建立自动化过程——构建、配置、部署、维护。

使用调度器声明应用依赖和部署策略:

# Define a job called my-service job "my-service" {     # Job should run in the US region region = "us"      # Spread tasks between us-west-1 and us-east-1 datacenters = ["us-west-1", "us-east-1"]   # Rolling updates should be sequential     update {                 stagger = "30s"         max_parallel = 1     }      group "webs" {        # Create 5 web groups         count = 5         # Create a web frontend using a docker image         task "frontend" {             driver = "docker"             config {                 image = "hashicorp/web-frontend"             }             restart {                 interval = "1m"                 attempts = 2                 delay = "15s"             }              # Register the task with Consul for service discovery             service {                 tags = ["prod"]                 port = "http"                 check {                     type = "http"                     path = "/health"                     interval = "10s"                     timeout = "2s"                 }            }            env{                 DB_HOST = "db01.example.com"                 DB_USER = "web"                 DB_PASSWORD = "loremipsum"             }              # Define the resources needed to run this task             resources {                 cpu = 500                 memory = 128                 network {                     mbits = 100                     dynamic_ports = [                     "http",                     "https",                     ]                 }             }         }     } }

这个工作文件声明了五个web前端实例,Docker容器应使用“us-west-1”和“us-west-2”数据中心来运行实例。每个任务需要500MHz的CPU、128MB内存和100MBit带宽。另外,Nomad动态为每个任务分配一个端口,使得同一个任务的多个实例可运行于同一个主机上。如果上述的前端任务有一个静态分配的80端口,那么一台主机上将只能运行一个此任务的实例,因为每台主机中只有一个80端口。通过动态分配端口,一个前端任务实例可运行于20100端口另一个实例可运行于20101端口。

由于Nomad将不同类型的工作负载放置到了一个通用主机集群中,如果没有服务发现工具这些放置的工作负载的地址和端口将是未知的。使用像Consul、 etcd 或者 Zookeeper 这样的服务发现工具允许在一个集群中动态放置工作负载同时被相关的依赖服务发现。“服务”模块通过在Consul将任务注册为名为 $(job-name)-$(task-group)-$(task-name) 的服务来完成它们。另外,它还为Nomad放置时分配的任务注册了动态端口,包括任何用于服务的健康检查任务。上述例子中的任务在Consul将被注册为以 my-service-webs-frontend 命名的服务。当然,服务发现集成需要Consul代理运行在Nomad调度的任务所运行的主机中。目前Nomad只有与Consul集成的API,但Nomad的未来版本将会为集成定制服务发现解决方案暴露相应的API。

使用调度器部署应用

可以使用如下命令将 my-service 任务提交至Nomad服务器并部署:

nomad run my-service.hcl

当将一个任务提交至Nomad服务器后,它会开始调度决策过程。这个过程通过相关约束规则来寻找可用资源,然后使用bin-packing算法决定最优部署。

一起来看上述例子如何运行。 前端 任务的驱动类型为Docker容器,这意味着运行它需要主机上有Docker引擎。如果Nomad集群中有100个节点,其中有40个已经安装了Docker引擎,那么其余60个将会直接从放置选项中被去除。下一个过滤步骤是确定哪些主机有运行此任务的可用资源(CPU、内存、网络)。最终,Nomad会生成分配可能性列表,然后基于最大化资源密度的原则做出最优决策。如果一个主机有20%的利用率而另一个有60%的利用率,Nomad将会选择后一个来最大化密度。因为所有的这些信息存储在每个Nomad服务器中,调度过程将会非常迅速。不需要网络通信,它会极大地降低调度速度。

另外,Nomad服务器可以通过并行运行的方式来增加调度吞吐量和提供高可用性。当Nomad运行在高可用性模式下时,Nomad服务器会复制状态、参与调度决策和执行首领选举。首领负责运行所有的查询和事务。Nomad是乐观并发的,意味着所有的服务器并行地参与作出调度决策。首领会提供额外必要的协调来确保安全性,同时保证客户端不被过度调度。由于Nomad是乐观并发的,同时运行三个Nomad服务器能够将吞吐量提升至三倍左右。Nomad服务器将会找到主机中的资源并乐观地假设这些资源仍然可用。如果两个Nomad服务器尝试将冲突的工作负载放置到同一个主机中,第二个放置任务将会失败,任务将会被放回到调度队列中。由于Nomad能够在秒级作出调度决策,被放回到调度队列中的任务不会导致严重的调度延迟。

如果任务被放置成功,但由于其他原因失败了(例如Docker容器处于不可用状态),本地的Nomad客户端将需要重启任务。在上述的例子中, 前端 任务会试图每分钟进行两次重启,然后等待15秒钟直到另一个重启周期开始。Nomad客户端会把它运行的每个任务的健康状态报告给中心Nomad服务器,这样服务器就能够知晓集群中所有任务的健康状态。如果本地的Nomad客户端不能重启任务,中心Nomad服务器将会把此任务放置到其他主机中。

现代化操作和正确的应用交付之路

正确地交付应用是一个复杂的过程。将工作流分为两个不可变层,应用和基础设置,能够在减少复杂性的同时加快部署速度和增加资源密度。通过构建机器镜像和使用这些镜像配置服务器来定义基础设施层。所有的这些组件可以通过代码进行配置,例如使用Packer模板和Terraform配置。应用层通过像Docker容器和静态二进制的应用包来进行定义,然后通过调度器或者静态分配的方式进行调度。这些组件也可以被定义为代码,例如使用Packer模板和Nomad任务文件。

可以使用像Consul这样的服务发现工具合并应用层和基础设施层。每个主机中都在基础设施层运行一个Consul代理。被调度的应用将被注册在本地代理中,所以应用可以被发现。例如,如果200个 api 任务被动态放置了,能够发现每个任务实例是非常关键的。任务所属主机中的IP地址和被动态分配的端口会被注册到北本地Consul代理中。为了负载均衡200个任务实例的流量,可以使用Consul注册数据填充一个 HA代理 配置:

以下是一个使用Consul注册数据来填充一个HA代理配置的 Consul模板 的例子:

backend webs             balance roundrobin             mode http{{range service "api"}} server {{.Node}} {{.Address}}:{{.Port}}{{end}}

以下代码将会渲染HA代理配置:

backend webs             balance roundrobin             mode http             104.131.109.224:6379             104.131.109.224:7812             104.131.59.59:6379

正如以上所展示的,由于Nomad能够探测端口冲突和动态分配端口来防止冲突,相同任务的多实例可被放置到同一个主机中(相同IP)。服务发现使得调度器能够真正地最大化资源利用率,同时维护服务之间的连接。

将问题域分为两个层级,应用层和基础设施层,同时更进一步将他们分解为不同的组件:准备、配置、服务发现、安全和调度,这样就能允许组织将复杂的过程化解为期望的应用交付流程。

关于作者

Keven Fishner 是HashiCorp的客户部门主管。在HashiCorp他与开源和商业产品客户有丰富的工作经验。他有哲学学位(杜克大学)和工程贸易学学位。

查看英文原文: Immutable Layers, Not (Just) Infrastructure

正文到此结束
Loading...