【.com原稿】众所周知,微服务和容器是紧密结合的。以上逐渐扩展到各种云服务场景。那么面对常见的混合云服务,我们应该如何使用基于容器的模型来管理它们呢?2018年5月18-19日,由主办方主办的全球软件与运维技术峰会在北京召开。在“微服务架构设计”分论坛上,饿了么算力交付部高级工程师李健以《饿了么基于容器的混合云实践》为主题进行了精彩的演讲。本文将按照以下四个部分进行分享:算力交付技术选择基于Kubernetes的“算力外卖”Kubernetes扩容方案随着业务的快速增长,相应的资源规模也在快速增长。而这种增长直接导致了服务器类型的增多、管理任务的加剧、交付需求的多样化。比如在某些场景下,我们需要在交付的服务器上预装某个应用;有时,我们需要提供一个“依赖”服务,或者一组服务的集合。计算能力的交付虽然我们面临的物理资源和虚拟机资源的数量是巨大的,但是我们的运维人员是有限的,不可能无限扩大。因此,我们需要将物理资源统一抽象出来,输出给开发者。而这种标准化的抽象可以为企业带来两大好处:大大降低成本和增加服务器管理能力。同时,这种统一的抽象催生了我们算力交付部门的出现。具体来说,我们平时交付的服务器,包括以云服务IaaS的形式交付,其实和应用的交付是一样的,比如以SaaS的形式。在当今的虚拟化云时代,我们可以通过简单的命令输入为文件系统准备一个CentOS或Ubuntu操作系统。所以,服务器实际上变成了一个软件服务,或者说是一个App。通过对服务器和应用程序进行抽象的标准化,我们可以将所有的物理资源抽象出来,形成具有管理和控制能力的计算能力,从而实现统筹系统的能力。可以说,所有的交付行为都属于应用。所以,我们算力交付的关键在于对应用的管理,这也是我们重心的转变。容器技术的雏形始于20世纪70年代后期,但直到2013年Docker的出现,容器才成为主流技术。Docker对容器技术最大的贡献在于它通过真正面向应用的方式实现了跨平台的可移植性,将所有服务统一以Image打包的形式进行应用交付。此外,它还是应用程序的封装标准。基于这个标准,我们可以让应用程序运行在任何平台上。同时,这也进一步推动了自动化运维、AIOps、大数据等应用的发展。因此,在降低人工成本的同时,也提高了资源利用率。我们将计算能力交付分为三类:客户应用程序的部署。收到服务请求后,我们会开始部署,让服务顺利运行。一键交付标准服务。比如某部门的大数据业务,需要一个环境。这个环境包含的很多服务默认是相互隔离的,但同时也需要保证一些服务可以相互连接。那么我们就需要准备一些可重现的标准化服务模板,以保证即使在复杂的SOA系统中也能顺利发现应用服务。服务器交付。前面说了,服务器交付的标准化是我们算力交付部门能力的体现。技术选择现在可选的容器技术有很多,包括Kubernetes、Swarm、AWSECS,其中Kubernetes比较受欢迎。因此,我们在选型时需要考虑以下因素:Kubernetes项目已经成为容器编排的事实标准。由于大家都在广泛使用,遇到问题可以去社区里寻找答案。这无形中带来了降低成本的好处。实际需求与技术的契合度。可扩展性和生态发展。一些大公司背书的技术,一般都有很强的背景支持和一定的前瞻性,同时有利于建立一定的生态系统。基于Kubernetes的“算力外卖”,我们开发者平时对应用服务类型的需求与公司的订餐服务非常相似。上图中的绿色Box其实就是我们抽象出来的外卖盒。这些服务中的每一个都通过域名相互调用。而且这些盒子是可复制的,我们可以根据模板创建多个盒子。每个Box内部调用不同服务时,其域名在系统中是唯一的。因此,它们可以跨不同的环境调用,减少了开发人员的工作量。以往,服务启动时,系统会自动生成一个网络标识,当IP地址或域名发生变化时,他们必须进行相应的配置更改。今天,只要容器环境和相应的应用程序在运行,不同的服务就可以根据唯一的网络标识符相互调用。在具体实现中,我们使用Kubernetes做一个底层的容器引擎。在这些单元中的每一个中,域和Pod都将包括在内。并且每个Unit都有自己的副本用于负载均衡。我们使用Systemd这种启动方式来了解服务之间的相互依赖关系。它通过启动树实现Box中相同的功能,保证Box在启动时,能够按照我们建立的依赖描述,依次启动和运行应用程序。虽然从技术发展的角度来说,服务之间不应该有太多的依赖,但是既然我们部门是为业务部门服务的,那么我们要做的就是推动标准化,这样才能兼容开发习惯和他们现在的项目.如上图所示,每个Unit中还有一个Hook,可以辅助服务的启停和初始化。比如某个服务完成后,会调用另一个Pod进行初始化。当然,我们也涉及公共服务的使用。例如,Box1和Box2都通过公共服务传递数据。公共服务的唯一网络ID可能会因重启等外部因素而发生变化。所以为了一致性,我们尝试将上述内部身份转换为外部身份。事实上,我们的内部身份永远不会改变,而外部关系需要通过服务发现机制与内部服务动态关联。这样内部服务就不用考虑配置变更和服务发现等问题。上图是我们送货服务最简单的抽象。众所周知,Kubernetes服务主要依赖于Etcd的启动,但Etcd本身并不能支撑Kubernetes应用场景中的大量业务。所以,考虑到业务量的逐渐增加,以及对服务的稳定性要求,我们需要尽可能的对Kubernetes进行拆分。在拆分的过程中,我们将原来单机房网络中的资源池拆分成三到四个Kubernetes集群。拆分完成后,我们遇到了一个问题:由于拆分的太细,导致集群资源利用不均:有的集群负载不足,有的集群负载过重。因此,我们使用不同的Etcds来对应不同的Kubernetes集群,通过调度让服务在集群之间漂移,这样既解决了资源效率的问题,也解决了可靠性的问题。在上面的简单结构中,除了黄色部分,其他组件都是Kubernetes原生的组件,包括蓝色部分中具有调度pod功能的调度器(Scheduler)。我们根据上述不同Node层的属性和服务信息进行判断,确定调用哪个集群。由于我们采用了两层调度的方式,当我们转移到第一层时,我们并没有集群的实时信息,也无法知道集群已经将服务转移到了哪里。为此,我们开发了一个调度程序(以黄色显示)。另外,我们也开发了类似Kubernetes的APIServer服务。可见我们的目的是通过外围的方式扩展Kubernetes集群,而不是改变Kubernetes本身。即:在Kubernetes的外围,增加了一层APIServer和Scheduler。此举的直接好处体现在:我们节省了框架上的开发和维护成本。让我们仔细看看真实的容器环境:前面说了,我们是基于Kubernetes的。在网络上,我们使用阿里云的虚拟机。如果规模较小,我们将使用Vroute方法;如果是自建机房,我们就用LinuxBridge的方式。同时,我们的StorageDriver使用了Overlay2。OS方面,我们都是用的Centos7-3.10.0-693。在Docker的版本上,我们使用的是17.06。值得补充的是:现在社区有传言说这个版本会停止维护,所以我们会在近期开始升级。Registry说到Registry,以前小的时候是没有问题的。但是现在我们的规模已经发展到横跨几个机房。因此,我们需要同步Registry发送的数据,在OSS(对象存储服务)和Ceph(一个开源分布式存储系统,提供对象存储、块存储和文件系统存储机制)之间进行“双写”。答对了。由于我们在自己的物理机房里有Ceph,所以我们在下载的时候可以遵循就近下载的原则,不用走出机房。此举的好处是减少了机房之间传输带来的带宽瓶颈问题。那我们为什么要做同步呢?因为我们的服务一旦发布,就会自动异步部署。但是有些机房可能根本就没有需要的镜像,所以在执行“拉”操作的时候会报错。因此,基于这样的考虑,我们采用了“同步双写”的方式,并增加了鉴权环节。想必大家都知道,Registry运行时间长了,镜像中的blob就会越来越多。我们既可以自己清理,也可以按照官方的方法进行清理,但是清理时间通常会很长。因此,清洁工作会带来服务中断,这对于一般公司来说是无法接受的。对此,我们想到了一种“Bucketrotation”的方法。由于我们在CI(ContinuousIntegration)过程中会产生大量的图片,所以我们将图片分成两个季度作为一个存储周期,即为第一季度创建一个新的Bucket来存储图片。在第二个季度,一个新的图像生成到第二个Bucket中。在第三季度,我们清除了第一个桶中的图像以存储新图像。以此类推,通过这种方法,我们可以限制镜像,使其不会无限增长。我们目前的8个注册中心服务于数以万计的Docker对象。如果Registry出现问题,我们需要快速定位问题。因此,对Registry进行监控是非常有必要的。同时,我们还需要实时监控系统的使用情况,以便进行必要的扩展。在实现上,我们其实只是稍微增加了一些自己写的程序,同时整体上还是保留了和其他程序的解耦关系。如图所示,我们可以通过监控上传下载速度等指标,包括blob数量,实时跟踪Registry的运行状态。对于Docker,我们使用Dockersyslogdriver将收集到的用户进程输出到ELK中进行展示。但是当日志量过于频繁时,ELK会出现毫秒级的乱序。这种混乱会给无法解决问题的业务部门带来困难。因此,我们更改了Docker的相关代码,人为地为每条日志添加了一个增量序列号。对于一些对可靠性要求比较高的日志,我们选择了TCP方式。但是在TCP模式下,如果向某个服务输出某条日志,接收该服务的Socket会因为满载而被“戳”。然后容器无法继续,其Dockerps命令将停在那里。另外,即使开启了TCP模式,如果没有启动接收日志TCPServer也无济于事。可见,在使用中面临一些具体问题时,我们的需求与开源项目所能提供的服务还是有一定距离的。任何一个开源项目,在企业实施的时候,都不可能做到十全十美。开源项目往往提供标准化的服务,但我们的需求往往是根据我们的具体场景定制的。我们企业的软件环境是根据自身环境一点一滴成长起来的,差异在所难免。我们不可能让开源软件为我们改变。我们只能通过修改程序,使开源软件适应我们当前的软件状态和环境,进而解决各种具体问题。上图是对Docker的一些监控,可以帮助我们发现各种问题、bug,以及需要“拧紧”的地方。Init流程在传统业务容器化的过程中,我们对容器的Init流程进行了定制和改进。Init进程可以切换Image基础层管理的一些目录和环境变量,也可以替换基础配置文件的宏替换。对于一些被迁移的服务,它们以前是有配置项的,它们通常使用环境变量来读取配置信息。那么这样无形中会增加我们修改成容器的成本。因此,我们提供的方法是:虽然配置中写了变量,但是我们可以根据容器的环境变量设置,自动替换服务写的“死”配置。当容器尚未启动且服务的IP地址未知时,这种Init进程的方法特别有用。对于容器管理,我们也启动了Command来持续监控进程的状态,避免僵尸进程的出现。另外,因为我们公司内部有SOA系统,Docker产生停止信号后,需要把流量从公司整个集群中移除。因此,我们通过拦截信息,实现了在相关流量入口提供服务的清除动作。同时,我们也向应用进程传递停止信号,完成对软件的各种清理操作,从而实现传统服务向容器化的平滑过渡。Kubernetes资源管理扩展下面我们通过几个案例来看看我们是如何通过扩展来满足业务需求的。我们都知道Kubernetes的APIServer是一个核心组件,它使所有的资源都可以被描述和配置。这类似于Linux的“一切皆文件”理念。Kubernetes集群使用所有的操作,包括监控等资源,通过读取文件来掌握服务的状态,通过写入文件来修改服务的状态。利用这种方式,我们陆续开发了不同的组件和APIServer接口,以Restful接口的形式实现了服务的微服务。在实际部署中,虽然我们没有使用KubernetesProxy,但它反而减轻了APIServer的负载。当然,如果有需要,我们可以按需加载和部署,从而保证和增强系统整体的可扩展性。上图KubernetesAPIServer直观的展示了APIServer简单的内部结构。所有的资源实际上都放到了Etcd中。同时,每个资源都有一个InternalObject,对应Etcd中的一些存储类Key/Value,保证了每个对象的唯一性。当有外部请求访问服务时,需要通过ExternalObject和InternalObject进行转换。比如:我们可能在程序开发中使用了不同版本的API,这时候有一个需求变更,需要我们增加一个字段。事实上,这个字段可能已经存在于InternalObject中,所以我们可以通过一些处理直接生成这个字段。可见我们真正要实现的是ExternalObject,不需要改变InternalObject。这样,我们在不修改存储的基础上,尽量做到服务和存储的解耦,从而更好的应对外界的各种变化。当然,这也是KubernetesAPIServer中对象服务的一个过程。同时,我们也将一些API归类到一个APIGroup中。在同一个APIGroup中,不同的资源是相互解耦和并行的,我们可以通过增加接口来扩展资源。上面提到的两个对象之间的转换,以及类型注册,都是在图中的Scheme中完成的。如果你想搭建自己的APIServer,Google官方针对这个项目有相应的解决方案,并提供专门的技术支持。有兴趣的可以参考下图对应的Github项目的URL。此外,谷歌还提供了自动生成代码的工具。相应的,在开发过程中,我们还做了一个小工具,直接通过对象生成数据库的所有相关操作,为我们节省了很多时间。最后跟大家分享一下我们在Kubernetes实现过程中遇到的一些问题:Pod重启方法。由于所谓的重启Pod,其实就是创建一个新的Pod,丢弃旧的,这对于我们的企业环境来说是不能接受的。Kubernetes无法限制容器文件系统的大小。有时候程序员在写业务代码的时候,由于粗心大意,导致日志文件一天暴增100多GB,将磁盘占满,引发告警。但是我们发现Kubernetes里面好像没有相关的属性来限制文件系统的大小,只好稍微修改一下。DNS转换。你可以使用自己的DNS,也可以使用Kubernetes的DNS。但是,在使用KubernetesDNS之前,我们花时间对其进行了定制和改造。内存.kmem.slabinfo。当我们创建一个容器到一定程度的时候,我们会发现这个容器已经不能再创建了。虽然我们尝试释放容器,但我们发现我们仍然受到内存满的限制。经过分析发现,由于我们使用的Kernel版本是3,Kubelet激活开启了cgroupkernel-memory的测试属性特性,而Centos7-3.10.0-693的Kernel3并没有很好的支持这个特性,所以消耗所有的记忆都消失了。李健,饿了么算力交付部负责人,拥有多年丰富的容器体系建设经验,推动了饿了么平台的容器化;擅长在企业级实现容器的敏捷化和标准化。基于容器的云计算项目的开发负责人。热爱开源,热衷于使用开源项目解决企业问题。【原创稿件,合作网站转载请注明原作者和出处为.com】
