在私有云的容器化过程当中,咱们并非白手起家开始的。而是接入了公司已经运行了多年的多个系统,包括自动编译打包,自动部署,日志监控,服务治理等等系统。在容器化以前,基础设施主要以物理机和虚拟机为主。所以,咱们私有云落地的主要工做是基础设施容器化,同时在应用的运维方面,兼用了以前的配套系统。利用以前的历史系统有利有弊,这些后面再谈。在这里我主要同你们分享一下在容器化落地实践中的一些经验和教训。前端
当咱们向别人讲述什么是容器的时候,经常用虚拟机做类比。在给用户进行普及的时候,咱们能够告诉他,容器是一种轻量级的虚拟机。可是在真正的落地实践的时候,咱们要让用户明白这是容器,而不是虚拟机。这二者是有本质的区别的。java
虚拟机的本质上是模拟。经过模拟物理机上的硬件,向用户提供诸如CPU、内存等资源。所以虚拟机上能够且必须安装独立的操做系统,系统内核与物理机的系统内核无关。所以一台物理机上有多个虚拟机时,一个虚拟机操做系统的崩溃不会影响到其余虚拟机。而容器的本质是通过隔离与限制的linux进程。容器实际使用的仍是物理机的资源,容器之间是共享了物理机的linux内核。这也就意味着当一个容器引起了内核crash以后,会殃及到物理机和物理机上的其余容器。从这个角度来讲,容器的权限和安全级别没有虚拟机高。可是反过来讲,由于可以直接使用CPU等资源,容器的性能会优于物理机。node
容器之间的隔离性依赖于linux提供的namespace。namespace虽然已经提供了较多的功能,可是,系统的隔离不可能如虚拟机那么完善。一个最简单的例子,就是一个物理机上的不一样虚拟机能够设置不一样的系统时间。而同一个物理机的容器只能共享系统时间,仅仅能够设置不一样的时区。linux
另外,对于容器资源的限制是经过linux提供的cgroup。在容器中,应用是能够感知到底层的基础设施的。并且因为没法充分隔离,从某种程度上来讲,容器能够看到宿主机上的全部资源,但实际上容器只能使用宿主机上的部分资源。nginx
我举个例子来讲。一个容器的CPU,绑定了0和1号核(经过cpuset设置)。可是若是应用是去读取的/proc/cpuinfo
的信息,做为其能够利用的CPU资源,则将会看到宿主机的全部cpu的信息,从而致使使用到其余的没有绑定的CPU(而实际因为cpuset的限制,容器的进程是不能使用除0和1号以外的CPU的)。相似的,容器内/proc/meminfo
的信息也是宿主机的全部内存信息(好比为10G)。若是应用也是从/proc/meminfo
上获取内存信息,那么应用会觉得其可用的内存总量为10G。而实际,经过cgroup对于容器设置了1G的最高使用量(经过mem.limit_in_bytes)。那么应用觉得其能够利用的内存资源与实际被限制使用的内存使用量有较大出入。这就会致使,应用在运行时会产生一些问题,甚至发生OOM崩溃。redis
这里我举一个实际的例子。docker
这是咱们在线上的一个实际问题,主要现象是垃圾回收偏长。具体的问题记录和解决在开涛的博客中有详细记录使用Docker容器时须要更改GC并发参数配置。centos
这里主要转载下问题产生的缘由。安全
一、由于容器不是物理隔离的,好比使用Runtime.getRuntime().availableProcessors() ,会拿到物理CPU个数,而不是容器申请时的个数,网络
二、CMS在算GC线程时默认是根据物理CPU算的。
这个缘由在根本上来讲,是由于docker在建立容器时,将宿主机上的诸如/proc/cpuinfo
,/proc/meminfo
等文件挂载到了容器中。使得容器从这些文件中读取了相关信息,误觉得其能够利用所有的宿主机资源。而实际上,容器使用的资源受到了cgroup的限制。
上面仅仅举了一个java的例子。其实不只仅是java,其余的语言开发出来的应用也有相似的问题。好比go上runtime.GOMAXPROCS(runtime.NumCPU())
,nodejs的Environment::GetCurrent()
等,直接从容器中读取了不许确的CPU信息。又好比nginx设置的cpu亲和性绑定worker_cpu_affinity
。也可能绑定了不许确的CPU。
解决的渠道通常分为两种:
一种是逢山开路遇水搭桥,经过将容器的配置信息,好比容器绑定的cpu核,容器内存的限制等,写入到容器内的一个标准文件container_info
中。应用根据container_info
中的资源信息,调整应用的配置来解决。好比修改jvm的一些参数,nginx的修改绑定的cpu编号等。
在docker后来的版本里,容器本身的cgroup会被挂载到容器内部,也就是说容器内部能够直接经过访问
/sys/fs/cgroup
中对应的文件获取容器的配置信息。就没必要再用写入标准文件的方式了。
另外一种是加强容器的隔离性,经过向容器提供正确的诸如/proc/cpuinfo
,/proc/meminfo
等文件。lxcfs
项目正是致力于此方式。
咱们使用的是前一种方式。前一种方式并不能一劳永逸的解决的全部问题,须要对于接入的应用进行分析,可是使用起来更为稳定。
在容器化落地的实践过程当中,不免会遇到不少坑。其中一个坑是就是graph driver的选择问题。当时使用的centos的devicemapper,遇到了内核crash的问题。这个问题在当时比较棘手,咱们一开始没能解决,因而咱们本身写了一版graph driver,命名为vdisk。这个vdisk主要是经过稀疏文件来模拟union filesystem的效果。这个在实际使用中,会减慢建立速度,可是益处是带来了极高的稳定性,并且再也不有dm的data file的预设磁盘容量的限制了。由于容器建立后,还须要启动公司的工具链,运维确认,而后切流量等才能完成上线,因此在实际中,该方式仍然有着极好的效果。
不过咱们没有放弃dm,在以后咱们也解决了dm带来的内核crash问题,这个能够参见蘑菇街对此的分享记录使用Docker时内核随机crash问题的分析和解决。配置nodiscard虽然解决了内核crash的问题,可是在实际的实践中,又引入了一个新的问题,就是容器磁盘超配的问题。就好比dm的data是一个稀疏问题,容量为10G,如今有5个容器,每一个容器磁盘预设空间是2G。可是容器文件实际占用只有1G。这时理论上来讲,建立第6个2G的容器是没问题的。可是若是这个5个容器,虽然实际文件只占用1G,可是容器中对于某个大文件进行反复的建立、删除(如redis的aof),则在dm的data中,会实际占用2G的空间。这时建立第6个容器就会由于dm的data空间不足而没法建立。这个问题从dm角度来讲很差直接解决,在实际操做中,经过与业务的沟通,将这种频繁读写的文件放置到外挂的volume中,从而解决了这个问题。
从这个坑中,咱们也对于后来的业务方作了建议,镜像和根目录尽可能只存只读和少许读写的文件,对于频繁读写或大量写的文件,尽可能使用外挂的volume。固然,咱们对外挂的volume也作了一些容量和写速度的限制,以避免业务之间互相影响。规范业务对于容器的使用行为。
在开始研究docker时,docker的稳定版本仍是1.2和1.3。可是随着docker的火热,版本迅速迭代。诚然,新的版本增长了不少的新功能。可是也可能引入了一些新的bug。咱们使用docker的基本理念是新技术不必定都步步跟上。更多的是考虑实际状况,版本尽可能的稳定。落地是第一要务。并且咱们的容器平台1.0更偏向于基础设施层,容器尽可能不进行重启和迁移。由于业务混合部署在集群中,当一个宿主机上的docker须要升级,则容器须要迁移,那么可能涉及到多个业务方,要去与各个业务方的运维、研发等进行沟通协调,至关的费时费力。咱们在充分测试以后,定制了本身的docker版本,并稳定使用。docker提供的功能足够,尽可能再也不升级,新增功能咱们经过其余的方式进行实现。
这里所指的监控主要指对资源,如CPU、内存等的监控。如前文中所述,因为容器的隔离性不足,所以容器中使用top
等看到的信息,也不彻底是容器内的信息,也包含有宿主机的信息。若是用户直接在容器中使用工具获取资源监控信息,容易被这些信息误导,没法准确判断资源的使用状况。所以咱们本身研发完成了一套容器平台监控系统,负责容器平台的资源监控,对于物理机和容器的资源使用状况进行采集和整理,统一在前端呈现给用户。并集成了资源告警、历史查询等功能。
因为容器平台系统,包含了多个子系统,以及许多的模块。系统庞大,就涉及到了诸多的配置、状态检查等等。咱们对应开发了一套巡检系统,用于巡查一些关键信息等。巡检系统一方面定时巡查,以便核实各个组件的工做状态;另外一方面,当出现情况时,使用巡检系统对于定位出现问题的节点,分析问题产生的缘由有极多的帮助。
在没有容器化以前,公司内部逐渐造成了本身的一套工具链,诸如编译打包、自动部署、统一监控等等。这些已经为部署在物理机和虚拟机中的应用提供了稳定的服务。所以在咱们的私有云落地实践中,咱们尽量的兼容了公司的工具链,在制做镜像时,将原有的工具链也都打入了镜像中。真正实现了业务的平滑迁移。
固然,打包了诸多的工具,随之带来的就是镜像的庞大,镜像的体积也不可遏制的从几百兆增加到了GB级别。而借助于工具链的标准化,镜像的种类就被缩减为了几种。考虑到建立容器的速度,咱们采用了镜像预分发的方式,将最新版本的镜像及时推送到计算节点上,虽然多占用了一些磁盘空间,可是有效防止了容器集中建立时,镜像中心的网络、磁盘读写成为瓶颈的问题。
使用已有工具链也意味着丧失了docker容器的一些优良特性。好比应用的发包升级上线仍然经过既有的自动部署系统,而没法利用docker的镜像分层。工具链之间的壁垒也制约了平台的集成能力,难于实现一键部署的效果。容器的弹性和迁移也只能以一个空壳容器自己的伸缩迁移体现,而不是应用层级的伸缩迁移。
弹性主要包括横向伸缩和纵向伸缩。横向伸缩主要是指调整应用容器的数量,这个主要经过建立/销毁容器进行实现。纵向伸缩主要是对单个容器的资源规格进行改变。由于容器对于CPU和内存的限制,主要是经过cgroup实现的。所以纵向的伸缩主要是经过修改cgroup中对应的值进行。
容器的迁移仍是冷迁移的方式呈现。因为公司相关业务的要求,容器的IP要尽可能保持不变。所以咱们在neutron中作了定制,能够在迁移后保证容器的ip地址不变,这样对外启动后呈现的服务不会有变化。
目前运行有大量容器,部署在多个机房,分为多个集群。如此大规模的容器运维,实际集群的运维人员并很少。主要缘由是对于运维的权限进行了分割。对于物理机、容器生命周期的管理,由集群的运维负责。而各个线上应用的运维,由各个应用配合的垂直运维(又称为应用运维)负责。通常问题,如物理机下线,由于涉及到应用下的该实例须要下线,由集群运维查看该物理机上的容器所属的应用,须要通知垂直运维,配合容器迁移,然后从新上线提供服务。二新增机器或者集群,对机器部署了容器平台系统后,便可交付集群运维,用以建立容器实例,并进而根据应用的申请,分配给各个应用。相较于一个应用的平台,不少操做还有一些手工的成分,所以还须要投入至关的人力在集群管理上。