背景
为了集中精力在上层逻辑,我们计划将自己搭建的容器调度框架mesos+marathon逐步迁移至阿里云容器服务。经过一个月的测试、迁移和开发,我们已将测试环境所有服务迁移到容器服务,并针对容器服务的问题,做了很多workaround,最终在容器服务上搭建了一个高可用零宕机的容器环境。下面我们从《云计算十字真言及其在小博无线的实践》中提到的五个维度来谈一谈容器服务的亮点和它的不足,以及如何让其变得高可用。
冗余
服务无论是被内部还是外部请求调用,都需要通过冗余来避免单点,防止单点故障时造成的服务不可用。
四种互通机制
在marahon中,我们使用SLB来消除单点。容器服务还提供另外三种服务间互通机制:
- link
- hostname
- 服务发现
在《阿里云容器服务网络性能测试》一文中,我们测试了这四种互通机制的网络性能。文末提到了关于link和hostname负载均衡能力的问题,后来和阿里云确认,四种机制的负载均衡能力如下:
- link
没有负载均衡能力,仅link到单一容器,目标容器停止后原有链接不再可用
- hostname
可以解析到多个容器IP地址,负载均衡能力仅限于DNS轮询
- 服务发现
基于HAPrxoy,具备负载均衡能力,根据服务配置的健康检查地址实时检查容器状态
- SLB
阿里云的负载均衡服务
由此可见,只有服务发现和SLB满足高可用的需求。服务发现的局限是仅提供HTTP代理,且仅内网可见,好处是可以省去一个私网SLB,在编排模板中声明即可。
因此,如果需要提供具备负载均衡能力的HTTP服务给内网使用,推荐采用服务发现的方案,否则就使用SLB。
HTTP SLB的性能问题
在《阿里云容器服务网络性能测试》末尾提到,HTTP模式的SLB存在性能问题,应该尽量避免使用。如果要提供公网HTTP服务,不建议直接创建公网HTTP SLB。正确的做法是:
- 使用服务发现暴露服务
- 创建nginx容器,使用服务发现地址作为upstream
- 将nginx容器置于公网TCP SLB下
公网访问走SLB,内网间访问则走服务发现。这样既可避免HTTP SLB性能问题,而且内网服务间调用也不会遇到TCP环回问题。
自动绑定SLB
在编排模板中可以将服务与SLB关联,这样可以实现自动添加/移除SLB节点,无需人工干预。不过遗憾的是一个服务只能关联一个SLB,这种设计无法满足以下两种应用场景:
- 服务需要被内网和外网同时访问,所以需要加入私网SLB和公网SLB
- 服务需要绑定多个IP,所以需要加入多个SLB
跨可用区冗余
容器服务的高可用性调度还能做到根据节点可用区调度。
- availability:az==3
服务至少分布在三个可用区中;如果当前集群没有三个可用区,或机器资源不够导致无法分布在三个可用区,容器创建会失败。
- availability:az==~3
服务尽可能分布在三个可用区中;无法满足时依然可以成功创建。
由于阿里云目前主推的VPC并不能跨可用区,所以这个功能的作用并不大。
漂移
漂移是指当节点或容器不可用时,自动将容器迁移至其他节点上,以保证所有容器都是可用的。容器服务目前对漂移的支持,我们觉得还非常不满意,支持程度很低。既然容器已经封装了服务所有运行环境,那么对于容器集群来说,漂移应该是一个基本功能。但我们测试发现:
- 部署服务时,如果容器在某一个节点启动失败,容器和服务的状态显示为『失败』,之后不会在其他节点重试
- 如果节点宕机或者容器健康检查失败,这些服务不会被重新调度到其他节点上,仅仅在控制台显示状态为『失败』
- 服务不能被平滑地重启
根据我们使用云计算服务以来的经验,要使服务做到高可用,正确的做法是提高整个系统的容错性,而不是单点稳定性。外部环境非常复杂,尤其是云计算资源,有很多不可控的因素。所以不能认为容器正常运行是常态,应该容忍错误发生,出错就自动重启。然而容器服务并不会自动重启容器,其对『重启』的支持仅仅为docker的restart=always参数。如果在模板中加入了该参数,当容器异常退出,docker会自动重启容器。其他情况,部署失败、容器健康检查失败、节点宕机无响应,容器服务并不会自动干预。
第1点,阿里云客服反馈,后期可能会加入重试机制。在这一天到来之前,只能在部署程序中自行判断、重试。
第2点,容器服务不会自动漂移到其他节点上,这个问题可以用『重新部署』来解决,我们测试发现服务重新部署时,只会重新部署状态不正常的容器,健康容器不受影响。因此可以使用程序轮询容器状态,当检测到失败,自动调用重新部署API。
第3点,即使是健康的容器,也不免会有各种各样诡异的问题,其可能的原因非常多。对于偶发问题,深入调查非常浪费资源,有时候甚至是徒劳的。大多数时候,我们需要的只是一个重启按钮,然而,这样一个『包治百病』的按钮,容器服务是没有的。『重新部署』达不到这样的效果,因为它只会处理状态异常的容器。停止然后再启动容器,也不符合我们的期望,一是因为『停止』不会事先修改节点权重从而做不到平滑。二是因为停止和启动对应的其实是docker stop
和docker start
,启动之后还是同一个容器。要平滑地重启服务,目前只能通过发布一个新版本来实现,多少感觉有些别扭。
伸缩
容器服务支持容器级别的伸缩,伸缩的指标有内存和CPU两个尺度,在服务编排时设置上限和下限,当实际使用量超出限制时,会自动增加或减少容器数量。
这是容器服务优于marathon的地方。marathon不支持伸缩,我们为解决这个问题,使用运维机器人实时收集、分析容器负载,根据负载情况判断如何伸缩服务。
但容器服务只支持横向地伸缩服务,不能做到纵向地调节每个容器的资源配置,比如增加或减少内存配额。某些时候,容器内存负载升高,光是增加容器个数并不能解决问题。虽然增加容器使每个容器的请求压力降低,但对于内存已经居高的容器,其负载并不会立即下降,最终仍然有部分容器处于高负载状态。另外,如果不支持纵向伸缩,在操作性上也比较麻烦。一个服务究竟配额多少合适,多了浪费资源,少了又增加风险,而且一时合适的配额并不是任何时候都合适。因此,在伸缩上,依然需要运维机器人做额外的干预。
除了容器级别,还应该有节点级别的伸缩:当业务压力持续增加,已经没有足够的资源供容器扩容了,这时就应该增加节点。容器服务做不到节点级别的伸缩,而且只能在web控制台手动增加移除节点,没有提供API。不过说到节点的伸缩,这其实还有阿里云的ECS计价策略有关,并不完全是一个技术问题。
熔断
熔断用于控制错误的影响面,将故障范围降至最小。除了依赖于业务结构设计外,容器服务方面,需要提供相应的API:
- 停止服务的API
容器服务有提供停止服务的API,可以实现在发现上游服务有问题时关闭下游服务的需求。
- 移除节点的API
目前没有API可以将节点移出集群,而且现有的『增加节点』功能其成本非常高,要么重置系统盘,要么安装大量软件耗时很久。其实这里更合适的是一个类似白名单的机制,将有问题的节点移出白名单,容器不再调度至该节点上,问题修复后再加入白名单。容器服务在这个需求上做的还不够,以至于无法应对节点故障的场景。例如,节点磁盘写满,清理程序不能有效清理,由于无法移除节点,容器依旧会被调度到该节点上,导致问题持续发生。
扁平
为了更好的利用主机资源,将所有服务无差别的运行在所有节点上。而不是对节点进行分类,不同的类别运行不同的服务。这种说法我们称之为扁平。要做到扁平,最需要考虑的是如何避免TCP环回问题。
在之前的框架中,我们对所有HTTP服务使用7层负载均衡,而非4层,这样可以避免遇到TCP环回。对于TCP服务,我们使用marathon的服务约束将TCP服务约束到特定的机器上,再将调用方约束至其他机器。
容器服务文档《自定义路由-支持 TCP 协议》中特地列举了三种解法来解决TCP环回问题,其中解法三使用自定义路由算是正面解决问题而非绕开问题。我们测试自定义路由确实可以解决TCP环回问题,但其本质上是link一个proxy容器来代理请求,如前所述,link是做不到高可用的,所以该方案无法应用于生产环境。
生成环境中我们推荐使用容器服务的服务约束。不同于marathon的约束只能指定到节点一级,容器的约束可以具体到服务一级。例如设置affinity.service!=mysql
可以约束服务不运行在mysql所在的机器上。使用这种方式可以将有可能产生环回问题的服务约束到不同的节点上,从而避免TCP环回。相比而言,marathon仅能约束服务只能或不能运行在某些节点上,其本质还是对节点分组,不符合扁平的思想。在这一点上,容器服务是优于marathon的。
服务高可用
以上五点是我们三年来云计算实践经验的总结,其目的都是为了让服务高可用。下面是容器服务涉及到高可用的另外一些特性。
蓝绿发布
容器服务有两种部署方式,『标准发布』和『蓝绿发布』:
- 标准发布
停止旧版本容器,然后启动新版本容器。服务会中断。
- 蓝绿发布
先启动新版本容器,验证新版本容器后,停止旧版本容器。服务不会中断,发布过程对终端用户无感知。
我们所有服务都使用蓝绿发布模式。根据官方文档描述,蓝绿发布分四个步骤:
一, 启动新版本容器,等待容器状态为健康
二, 设置新版本容器权重为100,流量部分导入新容器
三, 设置旧版本权重为0,流量全部导入新容器
四, 确认发布完成,删除旧容器和对应SLB节点
这四个步骤需要分别手动操作,不能做到一键发布,非常繁琐。既然容器可以配置健康检查,那为什么不将它用作蓝绿发布时切换新旧容器的指标,还需要用户自己判断是否可以导流?在容器服务整个框架中,健康检查似乎只用于服务发现中加入健康容器移除不健康容器,其他时候健康状态只作为一个状态呈现在控制台和API中。我们期望,之后能将它用于蓝绿发布和服务漂移中,这样容器服务的易用性会更好。
我们服务有数十个,每日上线也在数十次,这种需要鼠标一步一步点击的发布过程是没法使用的。容器服务没有提供一键式的发布API,那么我们只能使用其中涉及到的各个API(某些暂未出现在正式文档)来开发。开发过程中遇到如下一些问题:
- 不能忽略SLB的状态
如果使用SLB,一定不能忽视对SLB健康状态的判断,这一步很容易遗漏。步骤一中容器服务状态已经健康,直接导入流量即可,为什么还要等待SLB健康呢。因为容器健康状态和SLB健康状态是两回事,SLB会查询容器健康状态,确定容器健康后,SLB状态才会变为健康,这个过程很慢,正常情况也需要数十秒。所以忽略这一步直接将旧版本容器权重设为0的话,会造成短时间无服务的状态。
- 需等待已有请求处理完毕
步骤三设置旧版本为0,新的请求不会转发到旧版容器,但已有的请求还在旧版容器中,这时如果立即移除旧版容器会使这些请求被重置。
- 确认发布完成API设计不合理
确认发布完成API中,有一个参数为force=true
。该参数对应控制台中的提示是否自动平滑更新
,平滑的含义为:自动平滑更新将会设置老的服务权重为0,新的服务权重为100。看上去光这一个API就包含了步骤二和步骤三的工作,但我们测试发现,在使用SLB的情况下,并不能达到平滑的效果。它是这样工作的:
- 设置旧版容器权重为0
- 设置新版容器权重为100
- 移除旧容器节点
这三步依次执行,间隔很短。这样是做不到平滑的,原因和上面第1点一致,忽略了检查SLB状态。合理的设计应该在中间检查健康状态,等待新节点健康后再切断旧版容器的流量:
- 设置新版容器权重为100
- 等待新版容器SLB状态变为健康
- 设置旧版容器权重为0
- 等待已有请求处理完毕
- 移除旧版容器节点
- 节点过多时需要先收缩
试想一下这样的场景,集群10个节点,某个服务需要绑定SLB,也就需要在节点监听端口。如果该服务需要部署8个节点,那么在蓝绿发布时,是不可能部署成功的:
- 旧版本已经运行在了8个节点上,只有两个节点可以部署新版本
- 新版本在两个节点上成功启动,另外6个容器因端口冲突启动失败
- 整个发布最终显示失败
对于这个问题,目前并没有完美的解决方案,勉强可行的方案是发布之前先收缩容器,将容器缩减为5个再部署,部署完之后再扩容到原来的8个。这个方案有点麻烦,而且收缩容器数量会使单个容器的压力增大,在高峰期也是不小的风险。
其实在标准发布中有一种滚动更新机制(标签rolling_updates
)。滚动更新时,一个新版本容器健康后,再停掉一个旧版本容器,这样逐个发布,期间容器服务会通过健康检查判断容器的健康状态,整个过程自动无需干预。如果这个机制用于蓝绿发布中,就能完美地解决这个问题。但难点在于,现在的蓝绿发布步骤繁琐,如果再加入逐个更新的机制,步骤将更加繁琐。所以,要加入滚动更新机制,蓝绿发布的整个过程必须先做到自动化。
综合以上提到的问题,最后重新梳理正确的发布流程:
一, 判断是否需要收缩服务
二, 启动新版本容器,等待容器状态为健康
三, 设置新版本容器权重为100,流量部分导入新容器
四, 等待SLB状态变为健康,如果有SLB的话
五, 设置旧版本权重为0,流量全部导入新容器
六, 等待已有请求处理完毕
七, 确认发布完成,删除旧容器和对应SLB节点
八, 如果之前有收缩,恢复服务到原来数量
集群升级
在控制台可以升级集群,可升级的部件有agent和docker。
agent可以看作集群中每个节点上的调度器,升级agent时无法对集群进行管理操作,但不会影响已有服务。
升级docker时其中容器会停止,控制台上提示升级时间大约3-30分钟。容器运行在docker中,升级docker自然不可能做到不影响其中的容器,但容器服务只能对所有节点统一升级,这就无可避免的会造成服务中断。
希望容器服务在这两个方面做出改进:
- 可以分节点升级docker版本
- 支持类似节点白名单功能,控制容器暂时不调度到某个节点上
有了这两个功能,升级过程才能做到平滑。不然对于生产环境,分钟级别甚至三十分钟的服务中断是断然不能接受的。
总结
容器服务作为运行容器的集群,可用性还是不错的。它有不少亮点:
- 服务发现
既可以解决HTTP SLB性能问题,又可以省去一个私网SLB。
- 灵活的约束关系
除了设置服务间的约束关系,还可以设置与节点、镜像间的约束关系。
- 绑定SLB
在marathon中,我们通过捕捉update event来实现自动加入/移除SLB。在容器服务中,只需在模板中声明SLB,即可实现自动添加移除SLB节点。但不足之处是只能绑定一个SLB。
- 支持横向的伸缩容器个数
横向的容器扩容可以在一定程度上应对高峰期的压力,期望容器服务伸缩机制更加完善,能支持纵向和节点的伸缩方式。
然而容器服务的问题也不少:
- 部署失败不会重试
我们近期正好遇到一个服务需要监听节点的28088端口,由于其中一个节点上28088端口一直被占用(也许是docker bug,容器退出后端口不释放),容器在此节点上无法启动,而且容器服务又固执地每一次都选择在这个问题节点上启动容器,导致服务始终无法成功部署。我们只得重启docker才解决问题。
- 服务不健康时不会漂移
容器服务能够获取容器的健康状态,但不会自动将异常容器调度到其他节点上去。即使SLB和服务发现会根据容器健康状态判断是否向该容器导流,但少了一个可用容器,对整个服务来讲也是不利的。
- API不够完善
目前只公开了集群和服务相关的API,发布、节点相关的API都还没有公开。
- 节点白名单机制缺失
如果某个节点出问题,需要重启机器或者docker服务,这时需要先将服务调度到其他节点。由于没有一个类似白名单的机制,很难实现这个需求。
- 不能对单个节点进行升级
目前只能所有节点统一一起升级,这种会中断服务的升级方案,基本上等于是完全不可用的。
- 蓝绿发布太繁琐
整个过程对用户最好是透明的,不用关心这些繁琐的细节。要做到自动且平滑发布新版本,虽然也可以实现,但相当复杂。需要根据我们上面整理出来的八个步骤一一开发,而且具体实现的时候还需要考虑每一个环节的异常情况处理。
总体来讲,其亮点无法盖过这些不足,要作为mesos+marathon的替代的,提供一个高可用零宕机的运行环境,容器服务就显得不够完善,这给用户增加了很多不必要的开发工作。究其原因,我们认为是设计思路的问题,从一些地方可以看出,容器服务仅仅是将传统跑在Host的服务放进了容器,模式并没有什么变化。假如说,如果我们的业务模式是,两三个服务,每月发布一次更新,那这样的设计也没什么问题。但这样的业务模式,似乎也没有太大的必要使用容器。