阿里巴巴集团在中国开源年会上正式开源了其酝酿已久的容器 pouch 项目,为开源容器引擎家族又添加一名新成员。当前很多云服务提供商在其服务中添加了开源容器引擎,但其测试方式往往只是通过devops进行简单的功能测试,甚至未经测试直接使用开源社区的二进制发行版本。这使得很多缺陷遗漏到下游用户手中。开源引擎在商用时如何保障质量呢?本文通过多个类型的测试来系统的阐述容器引擎质量保障方案。
功能测试
适合场景:
容器功能验证是最基本的质量保障方式,适合于所有使用容器引擎的场景。功能测试中需要建立特性树,覆盖容器引擎常用的命令,如attach、build、commit、cp、create、diff、events、exec、export、history、images等。同时还需要尽可能多的覆盖到每一个命令中的选项,如create --hostname。从对外接口来分类,容器引擎的接口分为命令行接口和rest api接口。
通常做法:
直接使用开源社区中的自动化测试程序。但社区用例通常采用container in container的方式执行功能测试,这样做的好处是可以最大限度的保障测试环境的一致性。不足之处在于,因为采用了容器嵌套,被测试的容器引擎是container in container中的容器中的引擎,而不是主机中的容器引擎。这会导致容器引擎和宿主机适配的缺陷可能遗漏到下游。
优秀实践:
针对社区测试的不足,解决的方法是将整体的测试执行从容器中移植到宿主机中,将测试执行方式由container in container转变为container in host。具体的做法是,首先将container in container中的测试镜像对应的Dockerfile中的环境配置工作移植到主机中,之后使用"go test -c -o ${test_binary}"命令将社区源码用例在容器中编译为二进制后在主机中直接运行。这样做的好处是,被测容器引擎是宿主机中的容器引擎,而不是container in container中容器嵌套中的引擎,保证了被测对象的准确性。
压力测试
适合场景:
压力测试适用于业务中可能遇到大量用户请求的场景。当主机遇到大量请求时,会导致主机资源负载的增加。基本的功能测试只能验证容器引擎的功能是否正常,却无法保证容器引擎的各种操作在宿主机高负载的情况下仍然能够成功执行。通过压力测试可以触发一些在软件设计中未考虑到的隐藏较深的缺陷。
通常做法:
业界的压力测试主要使用开源测试套(如stress、fio等)对被测对象持续加压。这种方式的问题是加压方式比较单一,无法模拟出真实业务的压力场景。某些业务(如用户争抢优惠资源)往往会在几秒钟内会有短时间的压力峰值,之后会趋于平缓。对于这种业务,通常的压力测试方法是无法覆盖到的。
优秀实践:
将宿主机的加压方式分为3中,即CPU加压、内存加压和IO加压。同时可以结合各种业务场景设计对应的加压模型。常见的加压模型有阶梯压力、周期性稳定压力、随机脉冲压力、长期稳定压力。对于短时间内的压力峰值的场景,可以使用随机脉冲加压方式进行模拟,从而保证了加压方式更贴近于真实的场景。
- CPU负载加压:使宿主机CPU资源占有率达到30%~100%。
- 内存负载加压:通过频繁malloc/free操作实现宿主机内存的频繁操作,使内存占用率达到30%~80%。
- IO加压:使用 fio 工具可以实现对多种IO类型(sync, mmap, libaio, posixaio, SG v3, splice, null, network, syslet, guasi等)进行加压。
长时间稳定性测试
适合场景:
长时间稳定性测试适用于需要长久运行容器并且业务不能中断的业务场景。功能测试无法验证系统能否稳定运行较长的时间,一些隐藏较深的缺陷会在系统运行一段时间后爆发,如文件句柄泄露、各状态间锁冲突等。需要对容器引擎进行长时间稳定性测试,执行时间通常在7天以上。
通常做法:
将业务部署到容器中,同时模拟用户请求,观察业务运行一段时间。不足之处在于,业务本身只能覆盖与本业务最相关的部分接口,无法有效覆盖所有接口,并且无法模拟出容器各种状态频繁切换的场景。因此无法触发一些容器状态切换时锁冲突等深层缺陷。
优秀实践:
列举容器所有状态,并遍历状态切换的所有路径。从一个状态切换到另一个状态有多种方式。例如从running状态切换到exited状态可以使用stop命令或kill命令,而每一个命令又对应着多种命令选项。容器状态迁移的方式如下图所示,在执行测试时首先批量启动若干容器,之后通过随机算法频繁的随机切换容器的状态来达到触发深层缺陷的目的。
安全测试
适合场景:
与私有云相比,公有云对安全性要求较高。需要针对该场景进行安全加固。该项安全测试主要是针对基于runc的容器引擎,runv的加固方式不在其中。
通常做法:
执行开源社区已有的容器 安全测试套 来确认环境的安全性。不足之处在于社区中只包含了通用的安全检测点,无法针对自有的特定环境进行安全加固的验证。
优秀实践:
将容器安全测试分为安全合规测试和安全渗透测试。
1.安全合规测试
安全合规专项测试排查涉及到账号管理、文件管理、密码和密钥管理、端口占用、日志管理等内容。因安全合规测试涉及内容较多,下面只列举出部分例子,其余部分在此不再敖述,感兴趣的读者可以阅读网络安全的书籍。
- 账号管理:应用账号不能写死在代码中;需要单独接口进行账号权限认证。
- 文件管理:禁止存在无主文件;文件权限需要最小化处理,配置文件权限不高于644,执行文件权限不高于744。
- 密码和密钥管理:密钥不可硬编码到代码中;禁止使用不安全的加密算法(DES、RSA、MD2等);登录hub时需要对密码进行加密(aes加密)。
- 端口占用:通过nmap程序获取容器引擎所占用的端口,确认是否有多余的端口。
- 日志管理:需要提供日志防爆功能,防止恶意用户进行不断操作产生大量日志导致磁盘占满;daemon日志中不能包含debug信息;日志中不能存在明文敏感数据。
安全扫描测试:通过 govet 、 ineffassign 、 go ast scanner 、 golint 等工具可以对源码进行扫描。
除了以上源码扫描工具,还有一些其他开源或商业扫描工具可以使用。如
Nessus(扫描安全漏洞)、Retina(扫描安全漏洞) 、Bitdefender(病毒扫描) 、Avira AntiVir Server(病毒扫描)、Kaspersky Endpoint Security(病毒扫描)、McAfee VirusScan Enterprise(病毒扫描)、Symantec Endpoint Protection(病毒扫描)、TrendMicro OfficeScan(病毒扫描)、Nmap(端口检查)。
模糊测试(Fuzz testing):针对容器引擎的特点进行fuzz建模,通过peach和Codenomicon工具可以实现容器引擎接口和api测试,通过测试工具将生成的随机数据发送给引擎进行系统验证。
2.安全渗透测试
该项工作需要阅读源代码找到潜在的安全漏洞,同时需要及时同步社区中已经发现的cve漏洞,并进行版本升级。cvedetails网站收集了各开源社区的cve信息,在对cve进行排查时,不仅需要排查容器引擎的社区,还需要排查和其有交互的开源组件、编程语言库、编译器等,如 golang语言。
性能测试
适合场景:
性能测试适用于对性能要求较高、需要进行性能调优的容器引擎使用场景。
通常做法:
分别在宿主机和容器中执行常用的性能测试套(如lmbench),将测试结果进行对比。不足之处在于其无法覆盖到容器特有的性能参数,如启动时间、占用内存值等。
优秀实践:
通过对不同容器引擎以及容器与物理机虚拟机进行各关键指标的对比,可以找到项目的性能优化点。
容器引擎涉及到的性能指标主要有:
- 容器启动时间
测试方法:
$ time ${container_engine} run ${image} echo 0
- 容器占用内存值
测试方法:
$ smem ${container_pid}
smem 工具会获取三个主要的内存指标,在此选取PSS值,这是为了便于测试多个容器使用的内存总量。
- 主机最大容器承载量
测试方法:不断创建新的空载容器,同时使用如下命令获取当前容器数量,直至主机资源耗尽。
$ ${container_engine} info
- 容器CPU性能
测试方法:测试计算素数直到某个最大值所需要的时间。
$ sysbench --test=cpu --cpu-max-prime=${max_prime_value} run
- 容器内存性能
测试方法:每次读写8KB,直到10GB读取完成。
$ sysbench --test=memory --memory-block-size=8K --memory-total-size=10G run
- 容器磁盘IO性能
测试方法:使用sysbench --test=fileio或dd命令进行验证。
- 容器网络性能
测试方法:利用netperf测试套验证。
开源测试
适合场景:
适用于基于开源容器引擎二次开发的容器使用场景。
通常做法:
继承开源社区的测试用例,同时针对自研特性设计测试用例。不足之处在于无法有效利用社区资源进行容器引擎的质量保障。
优秀实践:
可以借助开源社区进一步完善质量保障,具体体现为:
- 上报缺陷
当软件在自己开发环境中发现缺陷时,可以查看社区中是否有其他人报过相同的缺陷。如果没有,可以在github中提交一个issue来跟踪,引导社区来解决缺陷,从而减少自身团队的投入。
- 提交测试用例
对于和自身平台有强相关性的用例可以贡献给社区,可以使社区在发布版本时预先验证某项功能,从而达到测试前移并减少自身项目工作量的目的。
- 排除社区已知缺陷
社区代码缺陷通常存放在 issue 中,很多高版本发现的缺陷在低版本中同样存在,需要进行人工排查,将issue中高版本的缺陷对应的patch合入到低版本的商用容器引擎中。
内核测试
适合场景:
容器引擎的运维系统往往包含着不同内核版本的宿主机,需要针对不同内核版本验证容器依赖的内核特性。
通常做法:
验证某个单一的内核版本中容器依赖的内核特性后,通过灰色发布的方式进行快速试错。不足之处在于快速试错采用的是线上的环境,如遇到问题可能会导致大面积服务中断。
优秀实践:
容器引擎通常需要运行在3.10或以上版本的内核中,这类内核中包含着cgroup、namespace、capability、seccomp、union fs等容器依赖的内核特性,需要对这些底层的内核特性进行充分的验证才能保证容器引擎的质量。
Ltp 是一款内核开源测试套。里面包含很多内核特性的测试用例,如 cgroup测试用例 、 namespace测试用例 。需要在验证容器引擎的同时,运行ltp中容器相关的内核特性测试用例。
故障注入测试
适合场景:
该项测试适用于对容错能力要求较高的场景。故障注入测试可以验证容器引擎的容错能力,从而减少故障对业务的影响。
通常做法:
执行一段时间的长稳测试,将所有已遇到的故障记录下来,录入故障模式库。这种方法较为被动,需要在故障出现时才能梳理出来,无法在故障发生前消除故障带来的影响。
优秀实践:
根据每一个容器引擎命令梳理出程序的控制流,在控制流中的每一个节点中增加典型的故障注入方式,确保不遗漏可能的故障点,从而保证容器引擎的容错能力。常用的故障类型包括但不限于CPU、内存、磁盘、网络等。
下面列举了部分故障供读者参考:
- CPU故障:将容器绑定到某CPU核后强制将该CPU核下线;主机CPU高负载时尝试启动容器;容器内部运行高负载程序时主机不会卡死。
- 内存故障:试图在容器中耗尽主机内存;使用---restart=always选项启动容器后在容器内部不断执行out of memory的操作;操作系统可用内存低时仍然可以启动容器。
- 磁盘故障:磁盘占满时尝试启动docker容器;容器运行中时umount graph所在的磁盘分区。
- 网络故障:容器网桥意外下线;ip资源耗尽;加大网络延迟,提高网络丢包率;分配给容器一个已被占用的ip地址。
- 其他故障:宿主机异常断电;随机杀掉系统进程;cgroup文件夹被umount;系统进程数量达到最大值时启动容器。
结尾
本文展示了多种容器引擎的专项测试方法,但测试只是验证其质量的方式。好的软件是设计出来的,而不是测试出来的。在加强容器测试的同时也需要企业去关注开发流程的质量和规范性。只有开发和测试共同努力才能有效的把控容器软件的质量。
作者简介
孙远,阿里巴巴测试开发专家,目前在阿里巴巴从事阿里容器pouch技术的质量保障工作。工作涉及到功能测试、性能测试、压力测试、稳定性测试、安全测试、测试管理、工程能力构建等内容。参与编写了《Docker进阶与实战》,曾多次受邀在中国软件测试大会、中国开源年会、中国质量大会发表演讲。先前曾先后就职于美国风河系统公司和华为技术有限公司。
刘璐,阿里巴巴资深测试开发工程师,目前在阿里巴巴从事阿里容器pouch技术的质量保障工作。曾任职甲骨文高级测试开发工程师(Solaris方向),有多年的系统软件测试经验。