阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

容器化实践的经验分享

142次阅读
没有评论

共计 4721 个字符,预计需要花费 12 分钟才能阅读完成。

前言

在私有云的容器化过程中,我们并不是白手起家开始的。而是接入了公司已经运行了多年的多个系统,包括自动编译打包,自动部署,日志监控,服务治理等等系统。在容器化之前,基础设施主要以物理机和虚拟机为主。因此,我们私有云落地的主要工作是基础设施容器化,同时在应用的运维方面,兼用了之前的配套系统。利用之前的历史系统有利有弊,这些后面再谈。在这里我主要同大家分享一下在容器化落地实践中的一些经验和教训。

容器与虚拟机

当我们向别人讲述什么是容器的时候,常常用虚拟机作类比。在给用户进行普及的时候,我们可以告诉他,容器是一种轻量级的虚拟机。但是在真正的落地实践的时候,我们要让用户明白这是容器,而不是虚拟机。这两者是有本质的区别的。

虚拟机的本质上是模拟。通过模拟物理机上的硬件,向用户提供诸如 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 的坑

在容器化落地的实践过程中,难免会遇到很多坑。其中一个坑是就是 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 时,docker 的稳定版本还是 1.2 和 1.3。但是随着 docker 的火热,版本迅速迭代。诚然,新的版本增加了很多的新功能。但是也可能引入了一些新的 bug。我们使用 docker 的基本理念是新技术不一定都步步跟上。更多的是考虑实际情况,版本尽量的稳定。落地是第一要务。而且我们的容器平台 1.0 更偏向于基础设施层,容器尽量不进行重启和迁移。因为业务混合部署在集群中,当一个宿主机上的 docker 需要升级,则容器需要迁移,那么可能涉及到多个业务方,要去与各个业务方的运维、研发等进行沟通协调,相当的费时费力。我们在充分测试之后,定制了自己的 docker 版本,并稳定使用。docker 提供的功能足够,尽量不再升级,新增功能我们通过其他的方式进行实现。

监控与巡检

这里所指的监控主要指对资源,如 CPU、内存等的监控。如前文中所述,由于容器的隔离性不足,因此容器中使用 top 等看到的信息,也不完全是容器内的信息,也包含有宿主机的信息。如果用户直接在容器中使用工具获取资源监控信息,容易被这些信息误导,无法准确判断资源的使用情况。因此我们自己研发完成了一套容器平台监控系统,负责容器平台的资源监控,对于物理机和容器的资源使用情况进行采集和整理,统一在前端呈现给用户。并集成了资源告警、历史查询等功能。

由于容器平台系统,包含了多个子系统,以及许多的模块。系统庞大,就涉及到了诸多的配置、状态检查等等。我们对应开发了一套巡检系统,用于巡查一些关键信息等。巡检系统一方面定时巡查,以便核实各个组件的工作状态;另一方面,当出现状况时,使用巡检系统对于定位出现问题的节点,分析问题产生的原因有极多的帮助。

标准化的工具

在没有容器化之前,公司内部逐渐形成了自己的一套工具链,诸如编译打包、自动部署、统一监控等等。这些已经为部署在物理机和虚拟机中的应用提供了稳定的服务。因此在我们的私有云落地实践中,我们尽可能的兼容了公司的工具链,在制作镜像时,将原有的工具链也都打入了镜像中。真正实现了业务的平滑迁移。

当然,打包了诸多的工具,随之带来的就是镜像的庞大,镜像的体积也不可遏制的从几百兆增长到了 GB 级别。而借助于工具链的标准化,镜像的种类就被缩减为了几种。考虑到创建容器的速度,我们采用了镜像预分发的方式,将最新版本的镜像及时推送到计算节点上,虽然多占用了一些磁盘空间,但是有效防止了容器集中创建时,镜像中心的网络、磁盘读写成为瓶颈的问题。

使用已有工具链也意味着丧失了 docker 容器的一些优良特性。比如应用的发包升级上线仍然通过既有的自动部署系统,而无法利用 docker 的镜像分层。工具链之间的壁垒也制约了平台的集成能力,难于实现一键部署的效果。容��的弹性和迁移也只能以一个空壳容器本身的伸缩迁移体现,而不是应用层级的伸缩迁移。

弹性与迁移

弹性主要包括横向伸缩和纵向伸缩。横向伸缩主要是指调整应用容器的数量,这个主要通过创建 / 销毁容器进行实现。纵向伸缩主要是对单个容器的资源规格进行改变。因为容器对于 CPU 和内存的限制,主要是通过 cgroup 实现的。因此纵向的伸缩主要是通过修改 cgroup 中对应的值进行。

容器的迁移还是冷迁移的方式呈现。由于公司相关业务的要求,容器的 IP 要尽量保持不变。因此我们在 neutron 中做了定制,可以在迁移后保证容器的 ip 地址不变,这样对外启动后呈现的服务不会有变化。

容器的运维

目前运行有大量容器,部署在多个机房,分为多个集群。如此大规模的容器运维,实际集群的运维人员并不多。主要原因是对于运维的权限进行了分割。对于物理机、容器生命周期的管理,由集群的运维负责。而各个线上应用的运维,由各个应用配合的垂直运维 (又称为应用运维) 负责。一般问题,如物理机下线,因为涉及到应用下的该实例需要下线,由集群运维查看该物理机上的容器所属的应用,需要通知垂直运维,配合容器迁移,而后重新上线提供服务。二新增机器或者集群,对机器部署了容器平台系统后,即可交付集群运维,用以创建容器实例,并进而根据应用的申请,分配给各个应用。相较于一个应用的平台,很多操作还有一些手工的成分,因此还需要投入相当的人力在集群管理上。

本文永久更新链接地址:http://www.linuxidc.com/Linux/2017-10/147486.htm

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2022-01-21发表,共计4721字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中