在私有云的容器化过程中,我们并不是白手起家开始的。而是接入了公司已经运行了多年的多个系统,包括自动编译打包,自动部署,日志监控,服务治理等等系统。在容器化之前,基础设施主要以物理机和虚拟机为主。因此,我们私有云落地的主要工作是基础设施容器化,同时在应用的运维方面,兼用了之前的配套系统。利用之前的历史系统有利有弊,这些后面再谈。在这里我主要同大家分享一下在容器化落地实践中的一些经验和教训。
容器与虚拟机当我们向别人讲述什么是容器的时候,常常用虚拟机作类比。在给用户进行普及的时候,我们可以告诉他,容器是一种轻量级的虚拟机。但是在真正的落地实践的时候,我们要让用户明白这是容器,而不是虚拟机。这两者是有本质的区别的。
虚拟机的本质上是模拟。通过模拟物理机上的硬件,向用户提供诸如CPU、内存等资源。因此虚拟机上可以且必须安装独立的操作系统,系统内核与物理机的系统内核无关。因此一台物理机上有多个虚拟机时,一个虚拟机操作系统的崩溃不会影响到其他虚拟机。而容器的本质是经过隔离与限制的linux进程。容器实际使用的还是物理机的资源,容器之间是共享了物理机的linux内核。这也就意味着当一个容器引发了内核crash之后,会殃及到物理机和物理机上的其他容器。从这个角度来说,容器的权限和安全级别没有虚拟机高。但是反过来说,因为能够直接使用CPU等资源,容器的性能会优于物理机。
容器之间的隔离性依赖于linux提供的namespace。namespace虽然已经提供了较多的功能,但是,系统的隔离不可能如虚拟机那么完善。一个最简单的例子,就是一个物理机上的不同虚拟机可以设置不同的系统时间。而同一个物理机的容器只能共享系统时间,仅仅可以设置不同的时区。
另外,对于容器资源的限制是通过linux提供的cgroup。在容器中,应用是可以感知到底层的基础设施的。而且由于无法充分隔离,从某种程度上来说,容器可以看到宿主机上的所有资源,但实际上容器只能使用宿主机上的部分资源。
我举个例子来说。一个容器的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崩溃。
这里我举一个实际的例子。
这是我们在线上的一个实际问题,主要现象是垃圾回收偏长。具体的问题记录和解决在开涛的博客中有详细记录使用Docker容器时需要更改GC并发参数配置。
这里主要转载下问题产生的原因。
1、因为容器不是物理隔离的,比如使用Runtime.getRuntime().availableProcessors() ,会拿到物理CPU个数,而不是容器申请时的个数,
2、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的坑