当前位置:首页 >> 服务器

构建一个简单的CaaS系统

在CaaS系统出现前企业应用架构基本被IaaS/SaaS/PaaS等模式垄断,直到Docker的出现为我们打开了另一个扇大门,废话不说了,我们直奔主题。

我们先了解下一个简单的CaaS系统是如何为用户提供服务的:

  1. 企业用户上传它的应用代码或其他代码托管方式,我们生成用户应用的镜像,或者用户直接上传镜像,或者用户直接使用我们提供的基础服务镜像
  2. 用户部署他的镜像应用,启动它的镜像容器
  3. 用户访问他的应用服务

OK,需求确定了,该搬砖了。

用户镜像制作

既然是一个简单的CaaS系统,我们就不让用户上传代码或者使用第三方代码托管了,直接让他们制作镜像后提交给我们,为此我们需要搭建一个Docker私服来让用户上传镜像,假设用户上传的镜像遵循这种格式 :docker 私服地址 /{appId}:{version} ,这对用户有一定要求,毕竟一些用户可能连Docker是啥都不知道就更别奢望让他们编写Dockerfile制作镜像交付给我们了。当然如果我们提供一些基础服务镜像(比如MySQL服务,Redis服务等)给用户那最好了。

启动用户镜像

有了用户制作的镜像,该是启动它的时候了。

docker pull docker私服地址/{appId}:{version}

docker run -d docker私服地址/{appId}:{version} 

启动方式很简单,但这并不是我们想要的,毕竟我们是要让用户能够访问到他部署的服务的,假如用户的服务是一个Web服务,那你得暴露出用户的Web服务端口,这需要我们确定容器的通信方案:

  1. 跟宿主机共用一个网络空间
  2. 发布一个容器端口,让Docker随机选择一个未使用的高位端口
  3. 发布一个容器端口,并映射到宿主机上指定端口为外部路由服务
  4. 采用Docker的'links'来允许容器间通信。 如果一个新容器链接到一个已有容器,新容器将会通过环境变量获得已有容器的链接信息,一个关联的容器将会获得它的对应连接信息,在它处理了那些变量后允许它自动连接。这样就使得同一个宿主机上的容器不需要知道对应服务的端口和地址,就可以直接进行通信

我们简单的CaaS系统暂时还用不到容器间通信,如果跟宿主机共用一个网络空间即 --net="host" 模式启动的话,那么如果有多个用户上传了镜像,他们的WEB服务端口都是8080,显然宿主机上只能启动一个8080端口,只能有一个用户的容器启动成功,其他的因为端口已经被占用导致启动失败,在这里我们选择第三种模式,选择指定的端口映射来发布容器,这也方便我们后面管理宿主机上的端口资源。OK,启动方式改成下面:

docker run -d -p 25701:8080 docker私服地址/{appId}:{version} 

为了不让某个用户的应用占用过多资源导致影响到整个宿主机上其他的应用,我们稍微对用户的资源进行下限制,比如限制用户应用容器的使用内存和CPU权重:

docker run -d -p 25701:8080 -m 512M -c 1024 docker私服地址/{appId}:{version} 

为了能做到水平扩展,容器服务最好是无状态的的,这样能更好的实现负载均衡和水平扩容。

应用启动成功,我们可以通过在宿主机上访问25701即可访问容器的8080端口服务。

在写代码的时候我们通过 Docker Remote API client libraries 来启动卸载容器,具体代码实现就不多说了。

服务发现

容器启动成功后,用户该如何访问到他的容器服务呢,总不能提供宿主机IP给用户直接访问吧,这就需要我们构建一个服务发现组件了。

服务发现的工作方式

当每一个服务启动上线之后,他们通过发现工具来注册自身信息
服务的消费者能够在预设的终端查询该服务的相关信息,然后它就可以基于查到的信息与其需要的组件进行交互
为了简便,我们使用ZooKeeper来作为我们的服务发现工具。

首先在容器启动成功后我们将服务注册到zookeeper中,存储的path路径如下:/caas/service/address/{appId}/{version},存储的服务子节点为{containerId}->{宿主机IP}:{服务端口}。

例如用户appId01和appId02分别部署了各自的应用版本容器containerId01和containerId02,对应的服务端口分别为25701和25702,那么zk里存储的注册表信息为下:

/caas/service/address/appId01/app01Version/containerId01 -> {宿主机IP}:25701

/caas/service/address/appId02/app02Version/containerId02 -> {宿主机IP}:25702 

如果一个用户部署了多个容器实例,对应的zk注册表信息类似下面:

/caas/service/address/{appId}/{version}/containerId01 -> {宿主机IP}:25701

/caas/service/address/{appId}/{version}/containerId02 -> {宿主机IP}:25702

/caas/service/address/{appId}/{version}/containerId03 -> {宿主机IP}:25703

/caas/service/address/{appId}/{version}/containerId04 -> {宿主机IP}:25704 

故障检测

以上我们完成了服务的注册,注册完服务后为了实现应用的高可用,我们应该还需要对容器进行故障检测,故障检测的方案通常有下面2种:

  • 组件主动请求服务发现心跳方式:组件可以设置一个超时时间,并能定期去请求服务发现来重置超时时间,超时时间达到阀值更新注册表
  • 服务发现主动请求组件心跳方式:服务发现定期的健康检查组件以及当组件出现故障时更新注册表

通常内部自己的服务可以使用第一种方式让组件主动请求服务发现,用户自己写的服务一般不可能费劲的去实现心跳来访问服务发现组件,所以通常会要求用户实现一个服务发现组件能访问的心跳接口,让服务发现组件去主动请求用户的应用,一旦访问失败在重试一定次数后会认为该应用已经出现故障无法继续提供服务,这时可以根据策略来选择直接停止删除该用户容器或者重新启动。

比如服务发现的健康检查组件可以每隔一定时间来访问用户的心跳接口,类似{宿主机IP}:25701/_ping。

注册表安全访问

基于安全方面考虑,通常情况下我们需要对服务发现做相应的访问控制,以便对注册表中的存储信息实现安全访问,可能有以下几种方案可供参考:

  1. 服务发现工具可以采用SSL/TLS加密链接
  2. 对写入数据进行加密,使用者使用的信息必须用相应的密钥解码从服务发现中获取
  3. 服务发现实现访问控制,将不同的键值切分到不同的分组中,根据访问的需要来制定不同的秘钥从而访问相应的分组

这里我们就不说具体的安全方面的实现了,谁让我们是简易版CaaS系统呢。

分布式配置存储和负载均衡

其实服务发现的注册表存储访问地址只是其中的一个方面,你可以用它来存其他的信息,比如存应用的配置,你可以通过配置动态的调整应用,也可以存容器的相关指标,负载均衡就是一个很好的例子,它可以通过查询服务发现得到各个后端节点承受的流量数,然后根据这个信息来调整配置。具体的负载均衡算法可以根据需求来选择,我们就使用最简单的round bobin算法,即轮询方式访问。这方面的实现涉及到CaaS系统的另一个组件:路由网关,具体后面介绍。

上面我们一直都是使用了ZooKeeper来作为服务发现工具的,除了ZooKeeper,我们还可以使用其他的服务发现工具:etcd、Consul、crypt、Confd,大家有兴趣可以了解下,最重要的是能保证注册表信息的数据一致性。

调度编排

通过上面几步你的CaaS系统基本小有所成了,但这还不够。我们在生产环境里随着用户应用容器的数量增加需要增加宿主机来支撑避免资源不足,或者将某些用户的实例单独部署在指定的宿主机上,这就需要我们实现一个调度器组件。

宿主选择

CaaS系统是一个分布式系统,在多个宿主机的环境里,我们需要知道用户的应用该部署在哪台宿主机上,如果单机的话那就不需要选择了,直接指定就好了。具体该如何调度需要考虑以下几点:

  1. 需要一个默认的调度策略,比如选择可用内存最多的宿主机部署服务或选择CPU最空闲的宿主机部署服务
  2. 调度器需要提供覆盖机制,比如2个容器必须部署在同一个宿主机上作为一个单元来运行,比如同一个服务的2个实例容器必须部署在不同机器上来达到高可用
  3. 调度器需要满足限制条件,比如给特定的宿主机打标签,比如一些服务需要部署在集群中的每一台宿主机上
  4. 多容器部署调度

随着业务的扩展,我们可能需要提供分组容器管理,将一个集合的容器(通常是有相互依赖关系紧密关联的组件)作为一个单独应用来处理,比如一个Web服务容器再加上后端的数据库服务容器组合成一个project来发布。这里就不多做讨论了,我们的简易版系统还没考虑到这步。

供应

供应是指将一个新主机上线并完成基本配置使得它们能够工作的一个过程,通常在集群管理里用来自动扩展宿主机,管理工具来定义需求额外主机的过程以及自动触发的条件,例如,如果你的应用的负载很高,你可能希望让你的系统增加额外的机器并水平扩展容器以缓解负载,这里我们同样不做实现,简易版就直接手动增加宿主机就好了嘛。

我们在这里举个实现调度器的相对简陋的方案:

主要使用关系型数据库如MySQL来存储宿主机信息,调度器查询宿主机的相关指标信息根据调度算法选择相应的宿主机来部署,利用乐观锁来保证并发操作时的数据一致性,利用事务来保证部署和卸载等操作的原子性。这里面可能坑比较多,大家也可以使用现在比较流行的调度器,常用的调度器有:Fleet、Marathon、Swarm、Mesos、Kubernetes、Compose,大家有兴趣可以了解下。

网关

上面我们在服务发现的负载均衡方面介绍到了网关,我们把它作为CaaS系统中重要的一个组件,他主要是负责用户请求的转发,举个例子用户部署了容器想要访问它的容器服务,这个请求到达网关后网关根据策略选择相应的后端容器服务然后转发请求。根据用户的设定,动态路由请求到对应容器实例,这相当于一个代理服务器。具体如何选择容器实例服务转发就需要实现负载均衡器,我们可以通过查询服务发现组件来获取相应容器信息来完成。既然是代理服务,我们在中间可以对用户的请求做其他处理,比如做黑名单过滤,做流量统计,做CNames路由等等

假设我们的CaaS网关访问域名是 mycaas.gateway.cn ,用户在我们后台部署了一个WEB应用容器实例,调度器将他部署在了10.10.10.101宿主机上,容器服务端口映射为25701,用户请求mycaas.gateway.cn到达网关后,网关根据请求信息识别用户查询该用户所有的应用容器信息,得到所有的容器服务地址,根据负载均衡规则代理转发到目标容器服务上。这个查询服务发现的过程中最好实现本地缓存,比如使用zookeeper的缓存减少和避免每次请求都访问服务发现组件,同时代理转发中尽量使用连接池减少开销。

总结

至此我们简单的CaaS系统就架构设计好了,在整个系统中有服务发现/调度器/网关等多个组件协调配合。