0%

本篇文章来自《海盗派测试分析MFQ&PPDCS》作者邰晓梅,因原作者博客已经不再维护,现将文章转载记录于此。

​ Kevin Fjelsted是一个盲人,他曾写了一篇文章《A Brief History of the Accessibility of Computers by Blind People》,收录在《Amplifying Your Effectiveness》一书中。文中从一个盲人的视角描述了几十年来计算机的演进和变化。

​ 我对Kevin所描述的一个小细节很感兴趣:由于看不见,Kevin经常听别人为他描述一些图画或者画面。他发现,绝大部分人描述一幅画的时候,实际上是在描述一些画的特征,比如“画里有三个人”、“这是一副关于房子的画”。人们很少会描述画的细节部分,这就为听者留下了广泛的思考和遐想空间。

​ 当你用文字表述一件事情或一个事务的时候,不论你用多少文字、多么精雕细琢,当让接收者亲自去体验这件事情的时候,比如亲自用眼睛看这幅画、或者亲自动手去实验一下,接收者总是会有新的感受、会发现一些文字之外的东西。这就是一副图画可能胜过万语千言的道理了。

​ 对照我们的测试,写作测试用例的时候,不就是试图用文字向测试执行人员传达测试设计人员的想法吗?从传递者的角度,也许你期望不同的执行人员拿到这个用例,其执行结果都是一样的,希望测试效果受测试执行人员经验的影响降到最低,因此你试图准确而详细地描述每一个步骤。然而你的测试用例是不可能写得事无巨细的,因为不会有那么多的时间允许你这么做(假如你真的有这么多的时间,我倒建议你多做做探索性测试、多想想更有价值的测试内容);另外一方面,即使真的给你这么多时间写详尽的测试用例,你仍然无法保证囊括每一个与之相关的细节。从接收者的角度,优秀的测试执行人员阅读测试用例,就要像欣赏一幅画一 样,不仅仅靠听--这样只能接到别人描述的表面特征,更重要的是靠看--用你的眼睛去观察,去想一想设计人员是怎么想的?设计这个用例的目的是什么?为什么要这样设计用例?我怎样测试才能保证最优的测试效果?和用例相关的部分哪些也值得我关注?哪些是我所知道的重要信息但用例却没有提到?我应该以怎样的顺序执行这些用例为佳?我以大概怎样的进度执行这些用例比较合适?最重要的是靠动手--用你的心去思考,当你拿到一份待执行的用例,如果上述问题,你通过审视用例,就基本了然于胸,那很好。如果不是这样,比如你对被测特性还不大了解,也没有关系,你可以在执行用例的过程中进一步思考这些问题,通过动手操作,你得到了被测对象的一些最真实的反馈,你对测试用例有了更深刻的认识,你也在随时调整着自己的测试策略。

​ 所以,传统的脚本化测试(Scripted Testing)方法,即先花一段时间设计测试用例、再依据用例去执行的测试方法,不仅仅对测试设计人员有很高的要求(这里体现了大量的创造性的劳动),同时对测试执行人员也提出了相当高的要求:你得通过测试用例尽可能准确猜测出测试设计人员的心思,还得高于测试设计人员,找到测试用例文字以外的被忽略的但同时也是很重要的信息 --除非你不想得到更好的测试效果。所以测试执行也是体现了大量的创造性思维的劳动。记得昨日在某一ISTQB-FL课程研讨会上,某位讲师讲到了一页胶片,胶片上赫然把测试执行等之后的环节归为“机械式的活动”,而把之前的一系列测试设计活动归为“创造性活动”,如果你的测试执行都是工具在自动化的做,也许这样分类是说得通的吧。

​ 很多组织都过分地看重测试用例,认为测试用例是测试人员最核心的资产,让最优秀的人专职设计测试用例(他们从不或很少做测试执行了),花大量的时间去创建并维护这些用例,这些前端的活动投入非常大。而在最后一段路程,投入反而不那么大了:请一些缺乏经验的测试人员或者干脆雇佣一些对特性不熟的外包人员,依据用例做测试执行即可。当版本发布,用户反馈一些问题后,开始分析这些问题为什么会漏测,准确地说,应该是分析为什么会漏测试设计,因为鲜有人关注测试执行环节能力的提升。人们开始在测试设计阶段运用更多、更复杂的测试设计方法,开始添加更冗长的测试设计流程,开始采用更为详细要求的测试设计模板。。。

​ 我时常听到来自测试设计人员的求助,希望我告诉他们“如何才能提升测试用例的有效性?”“如何确保我设计的用例漏测率最低?” 在他们心中,很有责任感地认为:测试漏测,首先是我没有设计好的缘故。我常常会告诉这些测试设计人员:单单依靠测试用例没有必要也不可能发现大部分的bug,很多bug要依赖执行人员在测试执行阶段发现,这是正常的测试现象--你不可能要求盲人通过听得来的对一副画的理解和一个正常人通过看对一副画的理解一致;我不建议测试设计人员长期不做测试执行,不建议测试设计和测试执行的分离,如果你的组织还没有办法做到这一点,请你--测试设计人员--一定要时常和测试执行人员沟通,向他请教对你的用例的看法,实时收到反馈信息,调整你的设计;过分重视测试设计而忽视测试执行,就如同“行百里者半九十”一样,最终的测试效果很可能会输在“测试的最后一公里”上。

探索性测试也许就是看中了人在测试中发挥的作用要大于文档在测试中发挥的作用这一点吧。

​ 下班在家无聊打开力扣(LeetCode国内版)随便逛逛,突然发现还有SQL题目,随便点开一道简单的题目,平常一般也就用用普通的CURD、分组、排序,多表关联查询什么的,这道题我是没做出来!各位小伙伴看看这道题,大家会不会做~这道题是这样子的:

leetcode-sql-176-desc

看起来普普通通哦,第一反应,排个序嘛~

1
SELECT DISTINCT(Salary) AS SecondHighestSalary FROM Employee ORDER BY Salary DESC;

再然后就发现卡壳了,触发了我的知识盲区!

  1. 如何只取出第2行数据?
  2. 如何判断是否有第二高的薪水(有可能所有人薪水都一样或空表的情况)?
  3. 如何返回null?

开始补习功课!


各种数据库的SQL有一定差异,我们以使用较多的MySQL数据库的SQL语法为例。

一、回顾LIMIT子句知识细节

首先我们回顾下如何限制只返回查询结果的前两行?

1
SELECT some_col FROM some_table LIMIT 2;

上述代码使用SELECT语言检索单独的一列数据。LIMIT 2指示MySQL数据库返回不超过2行的数据,其实这就是我们平时最常用的LIMIT子句,但这不是完整的LIMIT子句。

知识点来了!

1. LIMIT子句的完整形态是这样子的:

1
LIMIT return_rows_number OFFSET start_index

2. OFFSET默认值为0:

我们上面语句的LIMIT 2其实是LIMIT 2 OFFSET 0的默认简写形式,不显式声明OFFSET值,则OFFSET默认为0。

3. 简化版的LIMIT子句:

1
LIMIT start_index, return_rows_number

接下来我们实战下看看效果,加深下理解:

limit-2-sql-demo-image-20190512095326845

对于这道题,我们要获取第二行数据,应该这样写

1
SELECT DISTINCT(Salary) FROM Employee ORDER BY Salary DESC LIMIT 1 OFFSET 1;

二、回顾IFNULL函数的细节

MySQL的IFNULL函数其实就是个if else语句,函数语法规则如下

1
IFNULL(expr1, expr2)

伪代码逻辑如下:

1
if expr1 == null:
2
   return expr2;
3
else:
4
   return expr1;

执行两条SQL语句实践下更有助于理解:

ifnull-sql-demo-image-20190512095828594

对于我们这道题SQL应该这样写:

1
SELECT IFNULL((SELECT DISTINCT(Salary) FROM Employee ORDER BY Salary DESC LIMIT 1 OFFSET 1), null) AS SecondHighestSalary;

我们来测试下:

1 - 正常的数据,有各种薪资:

image-20190512100622340

2 - 所有人薪水都相同的情况:

image-20190512100717179

3 - 空表的情况:

image-20190512100808599

所有测试通过~


这道题虽然标记为简单题目,但是提交通过率却不高,仅有1/3。

image-20190512100927779

说明还是有不少同学和我一样对SQL对掌握不够呀,还是要找时间好好学习使用下。

题目链接在这里:https://leetcode-cn.com/problems/second-highest-salary/

有兴趣对小伙伴可以试试其他数据库的SQL语句怎么写,欢迎交流讨论。

去年在公司推广了robotframework自动化框架,基于此框架我们设计开发了HTTPTestLibrary关键字库开展接口测试,效果挺好。我们部门测试开发的统一Python版本为python2.7,因为在我来公司前就在用这个版本,虽然2020年社区不再提供支持,但我们目前还没有迁移Python3的计划,这是前提。

有点别致的JSON

说到接口测试,必然要支持解析处理各种请求体,其中,我们的研发在某些项目的接口中使用了这样的请求体,我举个例子:

1
{"key1": 123, "trouble": "{\"inner\": \"hehe\"}"}

JSON里嵌套一个JSON对象,还是个字符串型的,这是个标准的JSON类型嘛???google得知如下知识

json_org_doc

我们用Python测试下:

1
In [8]: json_str = r'''{"key1": 123, "trouble": "{\"inner\": \"hehe\"}"}'''
2
3
In [9]: json_str
4
Out[9]: '{"key1": 123, "trouble": "{\\"inner\\": \\"hehe\\"}"}'
5
6
In [10]: print(json_str)
7
{"key1": 123, "trouble": "{\"inner\": \"hehe\"}"}
8
9
In [11]: import json
10
11
In [12]: json.loads(json_str)
12
Out[12]: {'key1': 123, 'trouble': '{"inner": "hehe"}'}
13
14
In [13]: type(json.loads(json_str))
15
Out[13]: dict

研发定义的请求体没毛病,接下来看看我们的Python2的robotframework遇到什么问题了。

robotframework测试用例

我们的robotframework测试用例如下:

rf_test_case

基于我们对robotframework的了解,robotframework会读取文本格式的robot测试用例,经过解析加载为内存对象,构建测试用例,我们先分别使用Python2和Python3读取测试用例,看看效果是什么样子:

py2_py3_read_rf_case

可以看到,虽然python2和python3对字符串的类型处理方式不同,但是对于我们的测试用例文本,都只进行了转译加上了一个\

为了方便定位问题,便于观察,我们统一修改了robotframework的源码,增加了文本用例解析的输出

rf_source_code

Python2版本的robotframework同学登场

先看下robotframework的测试执行结果,看起来没毛病。

py2_rf_log

再看看测试用例的debug输出:

py2_rf_debug

WTF!为神马变成了四个\???,这就是导致我们的请求体异常,接口响应错误的根本原因啊!

可是在上面的的Python2直接读取测试用例表现的不是这样啊!

稳住,我们不能冤枉Python2同学,我们看看Python3的表现。

Python3版本的robot framework同学低调入场

Python3版本我们使用pipenv创建了一个虚拟环境,同样也修改了robotframework的源码,输出repr

按惯例先看看Python3版本的robotframework的测试执行结果:

py3_rf_log

看起来和python2版本的一模一样,再看看测试用例的debug输出:

py3_rf_debug

一切正常,完美!这样的结果才是我们想要的,这样的结果才能保证我们的接口响应正常。

蛛丝马迹

我们再仔细阅读下源码,robotframework到底是怎么读取的文本用例:

rf_source_open_mode

robotframework是以rb模式打开文件进行读取的。

Python官方文档是这么说的:

通常文件是以 text mode 打开的,这意味着从文件中读取或写入字符串时,都会以指定的编码方式进行编码。如果未指定编码格式,默认值与平台相关 (参见 open())。在mode 中追加的 'b' 则以 binary mode 打开文件:现在数据是以字节对象的形式进行读写的。这个模式应该用于所有不包含文本的文件。

但是我们进行debug输出时,python2版本却输出的是<type 'str'>类型,python3版本输出的是<class bytes>类型,显然Python2版本的robotframework在搞事情!

Python 3最重要的新特性大概要算是对文本和二进制数据作了更为清晰的区分。文本总是Unicode,由str类型表示,二进制数据则由bytes类型表示。Python 3不会以任意隐式的方式混用str和bytes,正是这使得两者的区分特别清晰。

至此,真相逐渐浮出水面了,不过我们还是不知道为什么Python2版本的robotframework会出现这种情况,但我们可以肯定的是,尽早的迁移到Python3,一定是一件正确的事情!

Reference

http://www.json.org/json-zh.html
https://blog.csdn.net/lgysjfs/article/details/86678559
http://www.ituring.com.cn/article/1116
https://stackoverflow.com/questions/9644110/difference-between-parsing-a-text-file-in-r-and-rb-mode/9644141#9644141
https://docs.python.org/zh-cn/3/tutorial/inputoutput.html#reading-and-writing-files
https://docs.python.org/zh-cn/2.7/tutorial/inputoutput.html#reading-and-writing-files


2019年04月08日 于 南京
Email
GitHub

在之前的《Hello Docker》中,简单介绍过Docker,但仅限于在测试开发团队内部使用,或者说更多的是我个人在使用Orz,等了好久终于公司的研发团队开始正式使用Docker了。这次来西安分公司学习下公司基于Docker和K8S开发的容器云平台,深入了解了下Docker的底层原理,简单学习了下K8S的基本概念和操作,感觉对于研发团队来说,容器化的迁移还是有一定成本的。

Docker带来了什么?改变了什么?

一、传统软件开发流程有几大痛点:

  1. 开发、测试、发布环境不统一

  2. 配置测试环境过程冗长又复杂

  3. 自动化测试环境不稳定,容易受到污染,隔离不足

  4. 无法准确获得客户的软件环境

  5. 开发团队无法复现测试团队报出的软件缺陷,导致两个团队出现相互推诿的现象

二、当前测试技术面临的几大挑战:

  1. 配置一致的测试环境
  2. 快速部署软件
  3. 并行执行测试,在并行的同时还要保证测试任务各自的环境不被污染
  4. 成功的复现软件缺陷
  5. 创建干净的可信的测试环境
  6. 快速部署多个测试主机
  7. 快速导入测试数据
  8. 快速清理测试环境
  9. 快速保留、复制、恢复测试环境
  10. 正确配置测试工具。快速将测试环境在不同操作系统(类Unix)

三、Docker对测试技术的革命性影响

软件开发交付速度上不去,很大一个问题是软件运行环境这个环节存在瓶颈,Docker解决了这个瓶颈,促进了软件开发的DevOps模式推广,这对所有的软件行业从业者都是巨大利好。

软件测试的几个重要方面:测试策略、测试设计、测试方法、测试数据、测试环境,前三个是方法论思想层面的,后两个是需要技术突破的。测试数据因具体细分行业不同,各有各的痛点也各有各的解决方案,但是对于测试环境来说,Docker是个近乎接近“银弹”的技术解决方案。

通过Docker的软件环境快速部署能力,促进了测试时间的再分配

一个小小的Dockerfile明确声明了软件部署的所有细节和流程,从此忘记冗长的安装部署文档吧!无论是从研发自测、功能测试、集成测试等哪个环节来讲,测试环境的部署时间成倍缩短,能给工程师更多的时间做更多有意义的事情,开发工程师可以有更多时间完善优化设计,修复缺陷,测试工程师可以有跟多时间拓宽测试的广度和提高测试的深度,运维工程师有更多的时间专注于改进软件监控分析系统,这和有效的自动化测试的价值可以相提并论。

通过Docker的软件环境一致性能力,有效降低了偶然复杂度

Docker的镜像发布,容器编排运行等设计实现,保证了开发环境、测试环境、生产发布环境以及不同操作系统发行版的高度一致可信,几乎避免了测试团队发现的缺陷在开发环境无法复现、线上生产环境的缺陷在线下无法复现且不便线上调试等问题带来的一些列人力成本、时间成本、心智成本的无谓消耗。

通过Docker的环境隔离管理能力,提高了测试资源的自由度

以往虽然有各种虚拟化技术,可以一台服务器虚拟化为多台虚拟机,可以对虚拟机进行快照,随时恢复软件环境,但是终究太笨重,资源损耗太高,利用率太低,无法实现测试资源自由。现在有了Docker的资源隔离管理能力,我们可以按照测试需求,启动多个不同版本的服务,随时创建,随时销毁;我们甚至可以在一台服务器给每个测试人员启动一套独立的测试环境,大家并行测试,互不干扰,避免了互相踩踏。同时这种环境资源的自由度,对于自动化测试的执行过程是有很大帮助的,大大提高了自动化测试的成功率,进而提高自动化测试的ROI。

测试团队如何顺势而为?

上一小节总结的三点革命性影响,对于整个开发流程来说,已经为CI/CD提供了道路基石,势必将进一步缩短迭代周期,提高交付速度(虽然还是有需求变更,开发延期等软件开发管理上不可避免的问题和人的不可靠性等因素等debuff)

效率提高了,交付速度提高了,迭代速度更快了,单纯的手工测试已经跟不上DevOps的软件开发模式了(其实在很多年前就跟不上现代软件开发节奏了),对测试的要求更高了,应当思考下省下来的时间如何更好的利用,如何跟上研发提测的速度。对于我现在所在的这种相对传统的行业软件服务企业来说,测试团队必须尽快适应这种DevOps开发模式了,不然就会被历史的车轮碾死在尘埃里。

测试团队的核心职责是质量保障,围绕质量保障,我们可以将测试能力向前向后输出,输出的最好形式就是自动化。

  • 自动化测试环境资源不再是自动化实施的绊脚石,接下来需要考验的是我们的自动化框架、自动化工具是否真的可靠、可用。
  • 测试驱动开发的模式虽然很好,但是推广落地却不是那么容易的😊。测试团队还可以将测试用例封装为自动化测试服务,提供给研发自测,而不是等研发一次次快速提测再被打回,冲击消耗测试工程师的激情和精力。更近一步可以考虑通过感知镜像仓库中镜像的变化,动态触发自动化测试。当然长远看来还是要与开发团队共同实现CI/CD,需要各个研发部门通力合作。
  • 软件运行环境的高度一致性,理论上线上发现的缺陷可以等价于测试团队漏测,这对测试团队的可信度和口碑是一个挑战,需要进一步提高测试质量和标准
  • 上线后的软件运行状态检测,虽然按照传统分工应当是运维团队来做的,但是测试团队在这方面也有自己得天独厚的优势,对产品的深刻理解,测试角度,可以将内测测试用例调整转为适配线上数据环境的在线自动化测试用例,实现线上测试,在第一时间发现由于线上环境测试数据的多样性和量级带来的问题,及时收集触发线上自动化用例失败的测试数据来完善测试用例库,提高测试覆盖度。

总而言之,核心就是进一步提高自动化测试技术的深度和广度

暂时想到这些,有新的想法再补充。

Reference
https://www.infoq.cn/article/docker-lead-test-innovation


2019年03月01日 于 西安
Email
GitHub

程序员的自我修养

摘自“隔壁王校长”的《Go语言框架:Beego vs Gin

原则一:

一切语言、技术或者框架,本质都是工具,工具的价值在于为使用者提供竞争优势。

原则二:

如果真的有一种语言或者框架很牛逼,那么荣耀或者赞誉也应该属于创造它的人,与使用者没有半毛钱关系;使用者的荣耀应该来自:针对恰当的需求使用恰当的语言或框架,并且做到按时交付以及高质量

原则三:

大部分人并不是天生具有选择恐惧症,也不是天生的杠精,在我看来所有关于选择的迷惑或者争吵,大都因为:

  • 没有设定清晰的标准
  • 标准不唯一
  • 标准之间没有优先级或者权重

2019年02月28日 于 西安
Email
GitHub

说起来Docker,大家都或多或少有耳闻,但是我的公司因为业务场景,长尾效应,至今为止Docker只在小部分项目轻度使用。但是这不妨碍我们主动学习,应用Docker,因此,从2018年开始,我就推动Docker在测试部应用。我们的测试平台,GitLab、JIRA、Confluence等都是通过Docker部署的。

团队内部做算法测试的同学搭建开发测试环境需要安装各种第三方库,因为工作环境是内网,无法连接互联网进行安装,痛苦不堪,使用Docker将相关环境在外网构建好传入内网使用是比较方便的方式;还有工具链建设、测试平台建设用到的各种基础设施MySQL、Nginx等快速部署,使用Docker都是最佳方案。为了方便团队同学了解Docker,团队老大让我来进行一次内部Docker入门分享。

没有Docker的从前

为了提高服务器利用效率,我们将一台高性能的服务器通过vSphere虚拟化,创建多台虚拟机,提供相对独立环境隔离的操作系统环境和计算资源,我们可以对每一台虚拟机灵活分配计算资源、存储资源等,还可以对每一台虚拟机创建多个快照,看起来很棒!是的,在传统的软件开发模式下,这种方式挺好,但是这并没有解决软件的部署效率低,交付困难,开发环境与测试环境不一致,测试环境与生产环境不一致等问题,这也是为什么我们有了持续集成,却没有做好持续交付的一大根本问题。

有了Docker的现在

各大互联网厂商都在使用Docker,通过Docker结合微服务等技术他们将DevOps这一理念落地了,真正做到了持续集成、持续部署、持续交付,快速迭代,可以实现一天多次版本上线发布!回头看有了Docker我们在日常工作中能做什么,举个最简单的例子:

以往我们想部署三套环境(一套Dev环境、一套Test环境、一套Release环境),需要三个MySQL数据库,需要怎么做呢?

  • 方案1:准备三台服务器,分别安装MySQL,干干净净,互不干扰,但是浪费资源啊;
  • 方案2:一台服务器部署三个MySQL实例,分别配置不同的MySQL数据存储路径、配置文件路径、端口,操作复杂,维护困难,想想都头疼;

用Docker以后呢,我们可以这样做:

1
docker run --name release-mysql -p 3306:3306 -d mysql:5.7
2
docker run --name dev-mysql -p 3307:3306 -d mysql:5.7
3
docker run --name test-mysql -p 3308:3306 -d mysql:5.7

这样我们就有了三个独立的MySQL数据库服务!当然这是个示例,没有做数据卷映射等配置,但那些配置相比于传统方案来说,复杂度不值一提。

Docker到底是什么

Docker 是世界领先的软件容器平台。开发人员利用 Docker 可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用 Docker 可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用 Docker 可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为 Linux 和 Windows Server 应用发布新功能。

什么是容器呢?

将软件打包成标准化单元,以用于开发、交付和部署

容器镜像是轻量的、可执行的独立软件包,包含软件运行所需的所有内容:代码、运行时环境、系统工具、系统库和设置。容器化软件适用于基于 Linux 和 Windows 的应用,在任何环境中都能够始终如一地运行。容器赋予了软件独立性,使其免受外在环境差异(例如,开发和预演环境的差异)的影响,从而有助于减少团队间在相同基础设施上运行不同软件时的冲突。

容器的优势特点

轻量

在一台机器上运行的多个 Docker 容器可以共享这台机器的操作系统内核;它们能够迅速启动,只需占用很少的计算和内存资源。镜像是通过文件系统层进行构造的,并共享一些公共文件。这样就能尽量降低磁盘用量,并能更快地下载镜像。

标准

Docker 容器基于开放式标准,能够在所有主流 Linux 版本、Microsoft Windows 以及包括 VM、裸机服务器和云在内的任何基础设施上运行。

安全

Docker 赋予应用的隔离性不仅限于彼此隔离,还独立于底层的基础设施。Docker 默认提供最强的隔离,因此应用出现问题,也只是单个容器的问题,而不会波及到整台机器。

容器 VS 虚拟机

容器和虚拟机具有相似的资源隔离和分配优势,但功能有所不同,因为容器虚拟化的是操作系统,而不是硬件,因此容器更容易移植,效率也更高。

容器

容器是一个应用层抽象,用于将代码和依赖资源打包在一起。多个容器可以在同一台机器上运行,共享操作系统内核,但各自作为独立的进程在用户空间中运行。与虚拟机相比,容器占用的空间较少(容器镜像大小通常只有几十兆),瞬间就能完成启动。

Container@2x

虚拟机

虚拟机 (VM) 是一个物理硬件层抽象,用于将一台服务器变成多台服务器。管理程序允许多个 VM 在一台机器上运行。每个 VM 都包含一整套操作系统、一个或多个应用、必要的二进制文件和库资源,因此占用大量空间。而且 VM 启动也十分缓慢。

VM@2x

对比传统虚拟机总结

特性 容器 虚拟机
启动 秒级 分钟级
硬盘使用 一般为 MB 一般为 GB
性能 接近原生 弱于
系统支持量 单机支持上千个容器 一般几十个

容器和虚拟机共用

将容器和虚拟机配合使用,为应用的部署和管理提供极大的灵活性。

containers-vms-together

如何使用Docker

安装Docker

Docker可以安装在Linux、Mac、Window上面,但是需要注意的是,Unix安装Docker(Linux、Mac)上对操作系统内核版本有要求(RedHat要7UX以上版本),Windows安装Docker推荐使用Win10操作系统(Win7也不是不能装,太折腾,人生苦短)。

具体安装方式Google搜一下就很多,不罗嗦了。

年轻人的第一个Docker命令

1546666360155

我们看下这条docker run hello-world命令执行后发生了什么:

  • 本地没有找到hello-world:latest这个镜像,通过pull命令从docker的仓库一个最新的镜像
  • 下载完毕后启动了hello-world容器,输出了一段介绍Docker的信息

基本概念

Docker包括三个基本概念

  • 镜像(Image
  • 容器(Container
  • 仓库(Repository

理解了这三个概念,就理解了 Docker 的整个生命周期。

Docker镜像

我们都知道,操作系统分为内核和用户空间。对于 Linux 而言,内核启动后,会挂载 root 文件系统为其提供用户空间支持。而 Docker 镜像(Image),就相当于是一个 root 文件系统。比如官方镜像 ubuntu:18.04 就包含了完整的一套 Ubuntu 18.04 最小系统的 root 文件系统。

Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。

Docker 容器

镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的 实例 一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。

容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的 命名空间。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。也因为这种隔离的特性,很多人初学 Docker 时常常会混淆容器和虚拟机。

前面讲过镜像使用的是分层存储,容器也是如此。每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为容器存储层

容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。

按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用 数据卷(Volume)、或者绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。

数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此,使用数据卷后,容器删除或者重新运行之后,数据却不会丢失。

Docker仓库

镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。

一个 Docker Registry 中可以包含多个仓库Repository);每个仓库可以包含多个标签Tag);每个标签对应一个镜像。

通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签> 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。

Ubuntu 镜像 为例,ubuntu 是仓库的名字,其内包含有不同的版本标签,如,16.04, 18.04。我们可以通过 ubuntu:14.04,或者 ubuntu:18.04 来具体指定所需哪个版本的镜像。如果忽略了标签,比如 ubuntu,那将视为 ubuntu:latest

仓库名经常以 两段式路径 形式出现,比如 jwilder/nginx-proxy,前者往往意味着 Docker Registry 多用户环境下的用户名,后者则往往是对应的软件名。但这并非绝对,取决于所使用的具体 Docker Registry 的软件或服务。

Docker Registry 公开服务

Docker Registry 公开服务是开放给用户使用、允许用户管理镜像的 Registry 服务。一般这类公开服务允许用户免费上传、下载公开的镜像,并可能提供收费服务供用户管理私有镜像。

最常使用的 Registry 公开服务是官方的 Docker Hub,这也是默认的 Registry,并拥有大量的高质量的官方镜像。除此以外,还有 CoreOSQuay.io,CoreOS 相关的镜像存储在这里;Google 的 Google Container RegistryKubernetes 的镜像使用的就是这个服务。

由于某些原因,在国内访问这些服务可能会比较慢。国内的一些云服务商提供了针对 Docker Hub 的镜像服务(Registry Mirror),这些镜像服务被称为加速器。常见的有 阿里云加速器DaoCloud 加速器 等。使用加速器会直接从国内的地址下载 Docker Hub 的镜像,比直接从 Docker Hub 下载速度会提高很多。在 安装 Docker 一节中有详细的配置方法。

国内也有一些云服务商提供类似于 Docker Hub 的公开服务。比如 时速云镜像仓库网易云镜像服务DaoCloud 镜像市场阿里云镜像库 等。

私有 Docker Registry

除了使用公开服务外,用户还可以在本地搭建私有 Docker Registry。Docker 官方提供了 Docker Registry镜像,可以直接使用做为私有 Registry 服务。在 私有仓库 一节中,会有进一步的搭建私有 Registry 服务的讲解。

开源的 Docker Registry 镜像只提供了 Docker Registry API 的服务端实现,足以支持 docker 命令,不影响使用。但不包含图形界面,以及镜像维护、用户管理、访问控制等高级功能。在官方的商业化版本 Docker Trusted Registry 中,提供了这些高级功能。

除了官方的 Docker Registry 外,还有第三方软件实现了 Docker Registry API,甚至提供了用户界面以及一些高级功能。比如,VMWare HarborSonatype Nexus

使用 Docker 镜像

在之前的介绍中,我们知道镜像是 Docker 的三大组件之一。

Docker 运行容器前需要本地存在对应的镜像,如果本地不存在该镜像,Docker 会从镜像仓库下载该镜像。

获取镜像

之前提到过,Docker Hub 上有大量的高质量的镜像可以用,这里我们就说一下怎么获取这些镜像。

从 Docker 镜像仓库获取镜像的命令是 docker pull。其命令格式为:

1
docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]

具体的选项可以通过 docker pull --help 命令看到,这里我们说一下镜像名称的格式。

  • Docker 镜像仓库地址:地址的格式一般是 <域名/IP>[:端口号]。默认地址是 Docker Hub。
  • 仓库名:如之前所说,这里的仓库名是两段式名称,即 <用户名>/<软件名>。对于 Docker Hub,如果不给出用户名,则默认为 library,也就是官方镜像。

比如:

1
$ docker pull ubuntu:18.04
2
18.04: Pulling from library/ubuntu
3
bf5d46315322: Pull complete
4
9f13e0ac480c: Pull complete
5
e8988b5b3097: Pull complete
6
40af181810e7: Pull complete
7
e6f7c7e5c03e: Pull complete
8
Digest: sha256:147913621d9cdea08853f6ba9116c2e27a3ceffecf3b492983ae97c3d643fbbe
9
Status: Downloaded newer image for ubuntu:18.04

上面的命令中没有给出 Docker 镜像仓库地址,因此将会从 Docker Hub 获取镜像。而镜像名称是 ubuntu:18.04,因此将会获取官方镜像 library/ubuntu 仓库中标签为 18.04 的镜像。

从下载过程中可以看到我们之前提及的分层存储的概念,镜像是由多层存储所构成。下载也是一层层的去下载,并非单一文件。下载过程中给出了每一层的 ID 的前 12 位。并且下载结束后,给出该镜像完整的 sha256 的摘要,以确保下载一致性。

在使用上面命令的时候,你可能会发现,你所看到的层 ID 以及 sha256 的摘要和这里的不一样。这是因为官方镜像是一直在维护的,有任何新的 bug,或者版本更新,都会进行修复再以原来的标签发布,这样可以确保任何使用这个标签的用户可以获得更安全、更稳定的镜像。

运行

有了镜像后,我们就能够以这个镜像为基础启动并运行一个容器。以上面的 ubuntu:18.04 为例,如果我们打算启动里面的 bash 并且进行交互式操作的话,可以执行下面的命令。

1
$ docker run -it --rm \
2
    ubuntu:18.04 \
3
    bash
4
5
root@e7009c6ce357:/# cat /etc/os-release
6
NAME="Ubuntu"
7
VERSION="18.04.1 LTS (Bionic Beaver)"
8
ID=ubuntu
9
ID_LIKE=debian
10
PRETTY_NAME="Ubuntu 18.04.1 LTS"
11
VERSION_ID="18.04"
12
HOME_URL="https://www.ubuntu.com/"
13
SUPPORT_URL="https://help.ubuntu.com/"
14
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
15
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
16
VERSION_CODENAME=bionic
17
UBUNTU_CODENAME=bionic

docker run 就是运行容器的命令,具体格式我们会在 容器 一节进行详细讲解,我们这里简要的说明一下上面用到的参数。

  • -it:这是两个参数,一个是 -i:交互式操作,一个是 -t 终端。我们这里打算进入 bash 执行一些命令并查看返回结果,因此我们需要交互式终端。
  • --rm:这个参数是说容器退出后随之将其删除。默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动 docker rm。我们这里只是随便执行个命令,看看结果,不需要排障和保留结果,因此使用 --rm 可以避免浪费空间。
  • ubuntu:18.04:这是指用 ubuntu:18.04 镜像为基础来启动容器。
  • bash:放在镜像名后的是命令,这里我们希望有个交互式 Shell,因此用的是 bash

进入容器后,我们可以在 Shell 下操作,执行任何所需的命令。这里,我们执行了 cat /etc/os-release,这是 Linux 常用的查看当前系统版本的命令,从返回的结果可以看到容器内是 Ubuntu 18.04.1 LTS 系统。

最后我们通过 exit 退出了这个容器。

列出镜像

要想列出已经下载下来的镜像,可以使用 docker images 命令。

1
root@evi1-book:/home/evi1# docker images
2
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
3
evi1/pypiserver         1.2.5               be7e6669fce0        37 hours ago        88.5MB
4
hello-world             latest              fce289e99eb9        4 days ago          1.84kB
5
<none>                  <none>              00285df0df87        5 days ago          342 MB
6
nginx                   latest              02256cfb0e4b        9 days ago          109MB
7
alpine                  3.7                 9bea9e12e381        2 weeks ago         4.21MB
8
ubuntu                  18.04               f753707788c5        4 weeks ago         127 MB
9
ubuntu                  latest              f753707788c5        4 weeks ago         127 MB
10
mysql                   5.7                 ae6b78bedf88        7 weeks ago         372MB
11
pypiserver/pypiserver   v1.2.5              b26b8ee831d7        7 weeks ago         86MB
12
codekoala/pypi          latest              f6ab585f84ed        7 months ago        61.3MB

列表包含了 仓库名标签镜像 ID创建时间 以及 所占用的空间

其中仓库名、标签在之前的基础概念章节已经介绍过了。镜像 ID 则是镜像的唯一标识,一个镜像可以对应多个标签。因此,在上面的例子中,我们可以看到 ubuntu:18.04ubuntu:latest 拥有相同的 ID,因为它们对应的是同一个镜像。

镜像体积

如果仔细观察,会注意到,这里标识的所占用空间和在 Docker Hub 上看到的镜像大小不同。比如,ubuntu:18.04 镜像大小,在这里是 127 MB,但是在 Docker Hub 显示的却是 50 MB。这是因为 Docker Hub 中显示的体积是压缩后的体积。在镜像下载和上传过程中镜像是保持着压缩状态的,因此 Docker Hub 所显示的大小是网络传输中更关心的流量大小。而 docker image ls 显示的是镜像下载到本地后,展开的大小,准确说,是展开后的各层所占空间的总和,因为镜像到本地后,查看空间的时候,更关心的是本地磁盘空间占用的大小。

另外一个需要注意的问题是,docker image ls 列表中的镜像体积总和并非是所有镜像实际硬盘消耗。由于 Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于 Docker 使用 Union FS,相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。

你可以通过以下命令来便捷的查看镜像、容器、数据卷所占用的空间。

1
root@evi1-book:/home/evi1# docker system df
2
TYPE                TOTAL               ACTIVE              SIZE                RECLAIMABLE
3
Images              13                  2                   1.931GB             1.614GB (83%)
4
Containers          2                   1                   2B                  0B (0%)
5
Local Volumes       14                  0                   0B                  0B
6
Build Cache         0                   0                   0B                  0B

虚悬镜像

上面的镜像列表中,还可以看到一个特殊的镜像,这个镜像既没有仓库名,也没有标签,均为 <none>。:

1
<none>               <none>              00285df0df87        5 days ago          342 MB

这个镜像原本是有镜像名和标签的,原来为 mongo:3.2,随着官方镜像维护,发布了新版本后,重新 docker pull mongo:3.2 时,mongo:3.2 这个镜像名被转移到了新下载的镜像身上,而旧的镜像上的这个名称则被取消,从而成为了 <none>。除了 docker pull 可能导致这种情况,docker build 也同样可以导致这种现象。由于新旧镜像同名,旧镜像名称被取消,从而出现仓库名、标签均为 <none> 的镜像。这类无标签镜像也被称为 虚悬镜像(dangling image) ,可以用下面的命令专门显示这类镜像:

1
$ docker image ls -f dangling=true
2
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
3
<none>              <none>              00285df0df87        5 days ago          342 MB

一般来说,虚悬镜像已经失去了存在的价值,是可以随意删除的,可以用下面的命令删除。

1
$ docker image prune

删除本地镜像

如果要删除本地的镜像,可以使用 docker image rm 命令,其格式为:

1
$ docker image rm [选项] <镜像1> [<镜像2> ...]

用 ID、镜像名、摘要删除镜像

其中,<镜像> 可以是 镜像短 ID镜像长 ID镜像名 或者 镜像摘要

比如我们有这么一些镜像:

1
$ docker image ls
2
REPOSITORY                  TAG                 IMAGE ID            CREATED             SIZE
3
centos                      latest              0584b3d2cf6d        3 weeks ago         196.5 MB
4
redis                       alpine              501ad78535f0        3 weeks ago         21.03 MB
5
docker                      latest              cf693ec9b5c7        3 weeks ago         105.1 MB
6
nginx                       latest              e43d811ce2f4        5 weeks ago         181.5 MB

我们可以用镜像的完整 ID,也称为 长 ID,来删除镜像。使用脚本的时候可能会用长 ID,但是人工输入就太累了,所以更多的时候是用 短 ID 来删除镜像。docker image ls 默认列出的就已经是短 ID 了,一般取前3个字符以上,只要足够区分于别的镜像就可以了。

比如这里,如果我们要删除 redis:alpine 镜像,可以执行:

1
$ docker image rm 501
2
Untagged: redis:alpine
3
Untagged: redis@sha256:f1ed3708f538b537eb9c2a7dd50dc90a706f7debd7e1196c9264edeea521a86d
4
Deleted: sha256:501ad78535f015d88872e13fa87a828425117e3d28075d0c117932b05bf189b7
5
Deleted: sha256:96167737e29ca8e9d74982ef2a0dda76ed7b430da55e321c071f0dbff8c2899b
6
Deleted: sha256:32770d1dcf835f192cafd6b9263b7b597a1778a403a109e2cc2ee866f74adf23
7
Deleted: sha256:127227698ad74a5846ff5153475e03439d96d4b1c7f2a449c7a826ef74a2d2fa
8
Deleted: sha256:1333ecc582459bac54e1437335c0816bc17634e131ea0cc48daa27d32c75eab3
9
Deleted: sha256:4fc455b921edf9c4aea207c51ab39b10b06540c8b4825ba57b3feed1668fa7c7

我们也可以用镜像名,也就是 <仓库名>:<标签>,来删除镜像。

1
$ docker image rm centos
2
Untagged: centos:latest
3
Untagged: centos@sha256:b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c
4
Deleted: sha256:0584b3d2cf6d235ee310cf14b54667d889887b838d3f3d3033acd70fc3c48b8a
5
Deleted: sha256:97ca462ad9eeae25941546209454496e1d66749d53dfa2ee32bf1faabd239d38

当然,更精确的是使用 镜像摘要 删除镜像。

1
$ docker image ls --digests
2
REPOSITORY                  TAG                 DIGEST                                                                    IMAGE ID            CREATED             SIZE
3
node                        slim                sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228   6e0c4c8e3913        3 weeks ago         214 MB
4
5
$ docker image rm node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228
6
Untagged: node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228

导出和导入镜像

根据我们公司的工作环境,互联网隔绝,必然要从外网下载或构建好镜像传入内网使用

下面我们就以evi1/pypiserver:1.2.5这个镜像为例演示下

首先我们在外网查看下镜像信息

1
root@evi1-book:/tmp/docker_images# docker images
2
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
3
evi1/pypiserver         1.2.5               be7e6669fce0        38 hours ago        88.5MB

导出镜像

通过docker save命令来执行导出镜像

1
root@evi1-book:/tmp/docker_images# docker save --help
2
3
Usage:	docker save [OPTIONS] IMAGE [IMAGE...]
4
5
Save one or more images to a tar archive (streamed to STDOUT by default)
6
7
Options:
8
  -o, --output string   Write to a file, instead of STDOUT

针对我们这里的镜像,命令应如下书写

1
root@evi1-book:/tmp/docker_images# docker save -o evi1-pypiserver_1.2.5.tar evi1/pypiserver:1.2.5

此处注意几点:

  • save -o 后面的参数是我们要保存的文件, 我们约定使用.tar结尾,此时导出的文件是为压缩的文件

  • tar文件我们按照{用户名}-{镜像名}_{标签}.tar格式来命名,这样方便管理,统一格式

  • 镜像参数我们使用镜像名称+标签的方式,这样有一个好处,导入后的镜像自带名称和标签,无需手动命名,否则导入的镜像会显示为如下样子

    1
    REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
    2
    <none>              <none>              be7e6669fce0        38 hours ago        88.5MB

至此我们就可以将镜像传入内网了,但是这个文件导出后大小为88.5MB,我们还可以压缩下,加快传输速度

1
root@evi1-book:/tmp/docker_images# tar czvf evi1-pypiserver_1.2.5.tar.gz evi1-pypiserver_1.2.5.tar
2
evi1-pypiserver_1.2.5.tar
3
root@evi1-book:/tmp/docker_images# du -sh *
4
89M	evi1-pypiserver_1.2.5.tar
5
33M	evi1-pypiserver_1.2.5.tar.gz

看到文件压缩后大小仅为原始镜像的1/3,所以大家从外网拉取镜像时要保持压缩的好习惯

导入镜像

镜像压缩包被传入内网后,我们先将压缩包上传至Docker所在的服务器任意目录下(通常我们会建立一个目录专门存放各种镜像压缩包,也便于我们后续建立私有镜像仓库,统一管理)

第一步先解压镜像压缩包

1
root@evi1-book:/tmp/docker_images# tar zxvf pypiserver_1.2.5.tar.gz

解压后我们得到一个evi1-pypiserver_1.2.5.tar文件

接下里开始真正的导入镜像操作docker load

1
root@evi1-book:/tmp/docker_images# docker load --help
2
3
Usage:	docker load [OPTIONS]
4
5
Load an image from a tar archive or STDIN
6
7
Options:
8
  -i, --input string   Read from tar archive file, instead of STDIN
9
  -q, --quiet          Suppress the load output

针对我们这里的镜像,命令应如下书写

1
root@evi1-book:/tmp/docker_images# docker load < evi1-pypiserver_1.2.5.tar

然后我们通过docker image ls就能看到导入的镜像了

1
root@xxxx:/home/root# docker image ls evi1/pypiserver
2
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
3
evi1/pypiserver     1.2.5               be7e6669fce0        38 hours ago        88.5MB

操作 Docker 容器

容器是 Docker 又一核心概念。

简单的说,容器是独立运行的一个或一组应用,以及它们的运行态环境。对应的,虚拟机可以理解为模拟运行的一整套操作系统(提供了运行态环境和其他系统环境)和跑在上面的应用。

启动容器

启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(stopped)的容器重新启动。

因为 Docker 的容器实在太轻量级了,很多时候用户都是随时删除和新创建容器。

新建并启动

所需要的命令主要为 docker run

例如,下面的命令输出一个 “Hello World”,之后终止容器。

1
$ docker run ubuntu:18.04 /bin/echo 'Hello world'
2
Hello world

这跟在本地直接执行 /bin/echo 'hello world' 几乎感觉不出任何区别。

下面的命令则启动一个 bash 终端,允许用户进行交互。

1
$ docker run -t -i ubuntu:18.04 /bin/bash
2
root@af8bae53bdd3:/#

其中,-t 选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上, -i 则让容器的标准输入保持打开。

在交互模式下,用户可以通过所创建的终端来输入命令,例如

1
root@af8bae53bdd3:/# pwd
2
/
3
root@af8bae53bdd3:/# ls
4
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

当利用 docker run 来创建容器时,Docker 在后台运行的标准操作包括:

  • 检查本地是否存在指定的镜像,不存在就从公有仓库下载
  • 利用镜像创建并启动一个容器
  • 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
  • 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
  • 从地址池配置一个 ip 地址给容器
  • 执行用户指定的应用程序
  • 执行完毕后容器被终止

启动已终止容器

可以利用 docker container start 命令,直接将一个已经终止的容器启动运行。

容器的核心为所执行的应用程序,所需要的资源都是应用程序运行所必需的。除此之外,并没有其它的资源。可以在伪终端中利用 pstop 来查看进程信息。

1
root@ba267838cc1b:/# ps
2
  PID TTY          TIME CMD
3
    1 ?        00:00:00 bash
4
   11 ?        00:00:00 ps

可见,容器中仅运行了指定的 bash 应用。这种特点使得 Docker 对资源的利用率极高,是货真价实的轻量级虚拟化。

后台运行

更多的时候,需要让 Docker 在后台运行而不是直接把执行命令的结果输出在当前宿主机下。此时,可以通过添加 -d 参数来实现。

下面举两个例子来说明一下。

如果不使用 -d 参数运行容器。

1
$ docker run ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
2
hello world
3
hello world
4
hello world
5
hello world

容器会把输出的结果 (STDOUT) 打印到宿主机上面

如果使用了 -d 参数运行容器。

1
$ docker run -d ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
2
77b2dc01fe0f3f1265df143181e7b9af5e05279a884f4776ee75350ea9d8017a

此时容器会在后台运行并不会把输出的结果 (STDOUT) 打印到宿主机上面(输出结果可以用 docker logs 查看)。

注: 容器是否会长久运行,是和 docker run 指定的命令有关,和 -d 参数无关。

使用 -d 参数启动后会返回一个唯一的 id,也可以通过 docker container ls 命令来查看容器信息。

1
$ docker container ls
2
CONTAINER ID  IMAGE         COMMAND               CREATED        STATUS       PORTS NAMES
3
77b2dc01fe0f  ubuntu:18.04  /bin/sh -c 'while tr  2 minutes ago  Up 1 minute        agitated_wright

要获取容器的输出信息,可以通过 docker container logs 命令。

1
$ docker container logs [container ID or NAMES]
2
hello world
3
hello world
4
hello world
5
. . .

终止容器

可以使用 docker container stop 来终止一个运行中的容器。

此外,当 Docker 容器中指定的应用终结时,容器也自动终止。

例如对于上一章节中只启动了一个终端的容器,用户通过 exit 命令或 Ctrl+d 来退出终端时,所创建的容器立刻终止。

终止状态的容器可以用 docker container ls -a 命令看到。例如

1
docker container ls -a
2
CONTAINER ID        IMAGE                    COMMAND                CREATED             STATUS                          PORTS               NAMES
3
ba267838cc1b        ubuntu:18.04             "/bin/bash"            30 minutes ago      Exited (0) About a minute ago                       trusting_newton
4
98e5efa7d997        training/webapp:latest   "python app.py"        About an hour ago   Exited (0) 34 minutes ago                           backstabbing_pike

处于终止状态的容器,可以通过 docker container start 命令来重新启动。

此外,docker container restart 命令会将一个运行态的容器终止,然后再重新启动它。

进入容器

在使用 -d 参数时,容器启动后会进入后台。

某些时候需要进入容器进行操作,包括使用 docker attach 命令或 docker exec 命令,推荐大家使用 docker exec 命令,原因会在下面说明。

attach 命令

docker attach 是 Docker 自带的命令。下面示例如何使用该命令。

1
$ docker run -dit ubuntu
2
243c32535da7d142fb0e6df616a3c3ada0b8ab417937c853a9e1c251f499f550
3
4
$ docker container ls
5
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
6
243c32535da7        ubuntu:latest       "/bin/bash"         18 seconds ago      Up 17 seconds                           nostalgic_hypatia
7
8
$ docker attach 243c
9
root@243c32535da7:/#

注意: 如果从这个 stdin 中 exit,会导致容器的停止。

exec 命令

-i -t 参数

docker exec 后边可以跟多个参数,这里主要说明 -i -t 参数。

只用 -i 参数时,由于没有分配伪终端,界面没有我们熟悉的 Linux 命令提示符,但命令执行结果仍然可以返回。

-i -t 参数一起使用时,则可以看到我们熟悉的 Linux 命令提示符。

1
$ docker run -dit ubuntu
2
69d137adef7a8a689cbcb059e94da5489d3cddd240ff675c640c8d96e84fe1f6
3
4
$ docker container ls
5
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
6
69d137adef7a        ubuntu:latest       "/bin/bash"         18 seconds ago      Up 17 seconds                           zealous_swirles
7
8
$ docker exec -i 69d1 bash
9
ls
10
bin
11
boot
12
dev
13
...
14
15
$ docker exec -it 69d1 bash
16
root@69d137adef7a:/#

如果从这个 stdin 中 exit,不会导致容器的停止。这就是为什么推荐大家使用 docker exec 的原因。

更多参数说明请使用 docker exec --help 查看。

导出和导入容器

导出容器

如果要导出本地某个容器,可以使用 docker export 命令。

1
$ docker container ls -a
2
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                    PORTS               NAMES
3
7691a814370e        ubuntu:18.04        "/bin/bash"         36 hours ago        Exited (0) 21 hours ago                       test
4
$ docker export 7691a814370e > ubuntu.tar

这样将导出容器快照到本地文件。

导入容器快照

可以使用 docker import 从容器快照文件中再导入为镜像,例如

1
$ cat ubuntu.tar | docker import - test/ubuntu:v1.0
2
$ docker image ls
3
REPOSITORY          TAG                 IMAGE ID            CREATED              VIRTUAL SIZE
4
test/ubuntu         v1.0                9d37a6082e97        About a minute ago   171.3 MB

此外,也可以通过指定 URL 或者某个目录来导入,例如

1
$ docker import http://example.com/exampleimage.tgz example/imagerepo

注:用户既可以使用 docker load 来导入镜像存储文件到本地镜像库,也可以使用 docker import 来导入一个容器快照到本地镜像库。这两者的区别在于容器快照文件将丢弃所有的历史记录和元数据信息(即仅保存容器当时的快照状态),而镜像存储文件将保存完整记录,体积也要大。此外,从容器快照文件导入时可以重新指定标签等元数据信息。

删除容器

可以使用 docker container rm 来删除一个处于终止状态的容器。例如

1
$ docker container rm  trusting_newton
2
trusting_newton

如果要删除一个运行中的容器,可以添加 -f 参数。Docker 会发送 SIGKILL 信号给容器。

清理所有处于终止状态的容器

docker container ls -a 命令可以查看所有已经创建的包括终止状态的容器,如果数量太多要一个个删除可能会很麻烦,用下面的命令可以清理掉所有处于终止状态的容器。

1
$ docker container prune

访问仓库

仓库(Repository)是集中存放镜像的地方。

Docker Hub

目前 Docker 官方维护了一个公共仓库 Docker Hub,其中已经包括了数量超过 15,000 的镜像。大部分需求都可以通过在 Docker Hub 中直接下载镜像来实现。

注册

你可以在 https://hub.docker.com 免费注册一个 Docker 账号。

登录

可以通过执行 docker login 命令交互式的输入用户名及密码来完成在命令行界面登录 Docker Hub。

你可以通过 docker logout 退出登录。

拉取镜像

你可以通过 docker search 命令来查找官方仓库中的镜像,并利用 docker pull 命令来将它下载到本地。

例如以 centos 为关键词进行搜索:

1
$ docker search centos
2
NAME                                            DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
3
centos                                          The official build of CentOS.                   465       [OK]
4
tianon/centos                                   CentOS 5 and 6, created using rinse instea...   28
5
blalor/centos                                   Bare-bones base CentOS 6.5 image                6                    [OK]
6
saltstack/centos-6-minimal                                                                      6                    [OK]
7
tutum/centos-6.4                                DEPRECATED. Use tutum/centos:6.4 instead. ...   5                    [OK]

可以看到返回了很多包含关键字的镜像,其中包括镜像名字、描述、收藏数(表示该镜像的受关注程度)、是否官方创建、是否自动创建。

官方的镜像说明是官方项目组创建和维护的,automated 资源允许用户验证镜像的来源和内容。

根据是否是官方提供,可将镜像资源分为两类。

一种是类似 centos 这样的镜像,被称为基础镜像或根镜像。这些基础镜像由 Docker 公司创建、验证、支持、提供。这样的镜像往往使用单个单词作为名字。

还有一种类型,比如 tianon/centos 镜像,它是由 Docker 的用户创建并维护的,往往带有用户名称前缀。可以通过前缀 username/ 来指定使用某个用户提供的镜像,比如 tianon 用户。

另外,在查找的时候通过 --filter=stars=N 参数可以指定仅显示收藏数量为 N 以上的镜像。

下载官方 centos 镜像到本地。

1
$ docker pull centos
2
Pulling repository centos
3
0b443ba03958: Download complete
4
539c0211cd76: Download complete
5
511136ea3c5a: Download complete
6
7064731afe90: Download complete

推送镜像

用户也可以在登录后通过 docker push 命令来将自己的镜像推送到 Docker Hub。

以下命令中的 username 请替换为你的 Docker 账号用户名。

1
$ docker tag ubuntu:18.04 username/ubuntu:18.04
2
3
$ docker image ls
4
5
REPOSITORY                                               TAG                    IMAGE ID            CREATED             SIZE
6
ubuntu                                                   18.04                  275d79972a86        6 days ago          94.6MB
7
username/ubuntu                                          18.04                  275d79972a86        6 days ago          94.6MB
8
9
$ docker push username/ubuntu:18.04
10
11
$ docker search username
12
13
NAME                      DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
14
username/ubuntu

Docker 数据管理

types-of-mounts

在容器中管理数据主要有两种方式:

  • 数据卷(Volumes)
  • 挂载主机目录 (Bind mounts)

数据卷

数据卷 是一个可供一个或多个容器使用的特殊目录,它绕过 UFS,可以提供很多有用的特性:

  • 数据卷 可以在容器之间共享和重用
  • 数据卷 的修改会立马生效
  • 数据卷 的更新,不会影响镜像
  • 数据卷 默认会一直存在,即使容器被删除

注意:数据卷 的使用,类似于 Linux 下对目录或文件进行 mount,镜像中的被指定为挂载点的目录中的文件会隐藏掉,能显示看的是挂载的 数据卷

创建一个数据卷

1
$ docker volume create my-vol

查看所有的 数据卷

1
$ docker volume ls
2
3
local               my-vol

在主机里使用以下命令可以查看指定 数据卷 的信息

1
$ docker volume inspect my-vol
2
[
3
    {
4
        "Driver": "local",
5
        "Labels": {},
6
        "Mountpoint": "/var/lib/docker/volumes/my-vol/_data",
7
        "Name": "my-vol",
8
        "Options": {},
9
        "Scope": "local"
10
    }
11
]

启动一个挂载数据卷的容器

在用 docker run 命令的时候,使用 --mount 标记来将 数据卷 挂载到容器里。在一次 docker run 中可以挂载多个 数据卷

下面创建一个名为 web 的容器,并加载一个 数据卷 到容器的 /webapp 目录。

1
$ docker run -d -P \
2
    --name web \
3
    # -v my-vol:/wepapp \
4
    --mount source=my-vol,target=/webapp \
5
    training/webapp \
6
    python app.py

查看数据卷的具体信息

在主机里使用以下命令可以查看 web 容器的信息

1
$ docker inspect web

数据卷 信息在 “Mounts” Key 下面

1
"Mounts": [
2
    {
3
        "Type": "volume",
4
        "Name": "my-vol",
5
        "Source": "/var/lib/docker/volumes/my-vol/_data",
6
        "Destination": "/app",
7
        "Driver": "local",
8
        "Mode": "",
9
        "RW": true,
10
        "Propagation": ""
11
    }
12
],

删除数据卷

1
$ docker volume rm my-vol

数据卷 是被设计用来持久化数据的,它的生命周期独立于容器,Docker 不会在容器被删除后自动删除 数据卷,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的 数据卷。如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用 docker rm -v 这个命令。

无主的数据卷可能会占据很多空间,要清理请使用以下命令

1
$ docker volume prune

挂载主机目录

挂载一个主机目录作为数据卷

使用 --mount 标记可以指定挂载一个本地主机的目录到容器中去。

1
$ docker run -d -P \
2
    --name web \
3
    # -v /src/webapp:/opt/webapp \
4
    --mount type=bind,source=/src/webapp,target=/opt/webapp \
5
    training/webapp \
6
    python app.py

上面的命令加载主机的 /src/webapp 目录到容器的 /opt/webapp目录。这个功能在进行测试的时候十分方便,比如用户可以放置一些程序到本地目录中,来查看容器是否正常工作。本地目录的路径必须是绝对路径,以前使用 -v 参数时如果本地目录不存在 Docker 会自动为你创建一个文件夹,现在使用 --mount 参数时如果本地目录不存在,Docker 会报错。

Docker 挂载主机目录的默认权限是 读写,用户也可以通过增加 readonly 指定为 只读

1
$ docker run -d -P \
2
    --name web \
3
    # -v /src/webapp:/opt/webapp:ro \
4
    --mount type=bind,source=/src/webapp,target=/opt/webapp,readonly \
5
    training/webapp \
6
    python app.py

加了 readonly 之后,就挂载为 只读 了。如果你在容器内 /opt/webapp 目录新建文件,会显示如下错误

1
/opt/webapp # touch new.txt
2
touch: new.txt: Read-only file system

查看数据卷的具体信息

在主机里使用以下命令可以查看 web 容器的信息

1
$ docker inspect web

挂载主机目录 的配置信息在 “Mounts” Key 下面

1
"Mounts": [
2
    {
3
        "Type": "bind",
4
        "Source": "/src/webapp",
5
        "Destination": "/opt/webapp",
6
        "Mode": "",
7
        "RW": true,
8
        "Propagation": "rprivate"
9
    }
10
],

挂载一个本地主机文件作为数据卷

--mount 标记也可以从主机挂载单个文件到容器中

1
$ docker run --rm -it \
2
   # -v $HOME/.bash_history:/root/.bash_history \
3
   --mount type=bind,source=$HOME/.bash_history,target=/root/.bash_history \
4
   ubuntu:18.04 \
5
   bash
6
7
root@2affd44b4667:/# history
8
1  ls
9
2  diskutil list

这样就可以记录在容器输入过的命令了。

Reference

https://www.docker-cn.com

https://yeasy.gitbooks.io/docker_practice/

题目要求

最近做了一道题,题目是这样的:

1
找到一个巨大数组的中位数。
2
# demo:[1,100,2,5,12,44,88,77,54,932,61]

解题方法

巨大的数组,排序肯定不是最优解了,解题思路可以借鉴快排算法那种分而治之的思想。

详情直接看代码实现吧。

1
#!/usr/bin/env python
2
# -*- coding:utf-8 -*-
3
# author: toddler
4
5
import random
6
import statistics
7
import time
8
import sys
9
sys.setrecursionlimit(1000000)
10
11
12
def find_mid(mid_index, __list):
13
    """
14
    寻找中位数算法
15
    :param mid_index: 中位数索引
16
    :param __list: 目标数组
17
    :return: 中位数数值
18
    """
19
    # 随机取一个数作为分割元素, 以分割元素为界限,将数组分割大小两部分
20
    random_num = random.choice(__list)
21
    small_list = [i for i in __list if i < random_num]
22
    # 若小数组的右端索引大于中位数索引, 则继续缩小小数组的区间长度, 这样可以直接舍弃比中位数大的元素, 减少计算量
23
    if len(small_list) > mid_index:
24
        return find_mid(mid_index, small_list)
25
    # 分割点左边的元素没有价值, 被舍弃, 相应的中位数索引左移对应长度, 保证相对原始数据索引长度不变
26
    mid_index -= len(small_list)
27
    # 判断分割点有几个, 若分割点所占空间长度大于新的中位数索引, 则分割点就是中位数
28
    same_mid_num = __list.count(random_num)
29
    if same_mid_num > mid_index:
30
        return random_num
31
    # 接下来向右计算, 所以切分点所占据的索引区间元素将不在计算, 大数组将舍弃这些值, 因此调整中位数的索引值
32
    mid_index -= same_mid_num
33
    big_list = [i for i in __list if i > random_num]
34
    return find_mid(mid_index, big_list)
35
36
37
def run(__list):
38
    """
39
    调度处理算法无关的业务逻辑
40
    :param __list: 目标数组
41
    :return: 中位数数值
42
    """
43
    list_length = len(__list)
44
    if list_length != 0:
45
        # 判断奇数个还是偶数个
46
        if list_length % 2:
47
            mid_index = list_length // 2
48
            print("奇数个数字: {}".format(list_length))
49
            return find_mid(mid_index, __list)
50
        else:
51
            print("偶数个数字: {}".format(list_length))
52
            left_num = find_mid((list_length - 1) // 2, __list)
53
            right_num = find_mid((list_length + 1) // 2, __list)
54
            return (left_num + right_num) / 2
55
    else:
56
        return "输入列表是否为空"
57
58
59
def test(test_data):
60
    """
61
    测试验证
62
    :param test_data: 待测数组
63
    :return:
64
    """
65
    # print('原始数据: {}'.format(test_data))
66
    stand_s_time = time.clock()
67
    expect_mid = statistics.median(test_data)
68
    stand_e_time = time.clock()
69
    print("statistics计算耗时: {}".format(stand_e_time - stand_s_time))
70
    start_time = time.clock()
71
    actual_mid = run(test_data)
72
    end_time = time.clock()
73
    print("我的算法计算耗时: {}".format(end_time-start_time))
74
    print('期望结果: 中位数为{}'.format(expect_mid))
75
    print('实际结果: 中位数为{}'.format(actual_mid))
76
    assert expect_mid == actual_mid, '计算错误'
77
78
79
demo_list = [1, 100, 2, 5, 12, 44, 88, 77, 54, 932, 61]
80
print('样例测试=====>')
81
test(demo_list)
82
print('\r\n大数据量测试======>')
83
test([random.randint(0, int(1e6)) for _ in range(int(1e6))])

测试结果

测试环境:

硬件 指标
CPU Intel(R) Core(TM) i3-4370 CPU @ 3.80GHz
MEM 8G DDR3

算法表现,稳定性不是很好:

数据量 排序时间
10 ^ 6 0.5 s
10 ^ 7 5 ~ 6 s
10 ^ 8 65 ~ 100 s

总结分析

最差时间分析 平均时间复杂度 稳定度 空间复杂度
O(n) O(logn) 不稳定 O(logn)

最近微信上线了一款小应用—“跳一跳”,这个规则简单,让人上瘾的小游戏和2048一样魔性,朋友圈也是各路小伙伴各显神通:硬件流(树莓派+步进电机)、日天流(篡改http请求)、软件流(adb控制手机模拟点击)。

今天我们也来实践下,当然选择最顺手的Python来搞咯,直接找到开源项目wechat_jump_game进行优化改造。此项目有个pull request[优化]跑分17000+ 新增AI模块,机器人自主学习生成跳跃公式,看到AI我们就来了兴趣,只见过理论,还没有实践过,可以拿这个实践下。

这个pull request介绍如下:

机器人精确采集跳跃结果并自主学习,使用线性回归方法
拟合出最优 [按压时间]->[弹跳距离] 线性公式 Y =kX + b
本优化无需修改config文件,可以适配所有手机,经过十次以上跳跃学习,机器人即可
模拟出相对稳定的线性公式。随着采集结果越多,跳跃也越精确,后期基本连续命中靶心。
理论上只要目标点获取无误,会一直跳下去。

工作两年多,一直在做服务端后台应用相关的测试,没接触过移动端测试呢,正好趁这次机会学习下怎么通过代码自动化控制安卓手机。
下面来动手试一下,找出下岗多年的MX3,充电开机。

第一次调试

  1. 安装好adb,配置好环境变量。
  2. 手机打开开发者模式,连接PC。
  3. 命令行测试是否连接成功: adb devices,手机弹出是否信任窗口,点击确定,已经链接成功。
  4. 测试一些adb命令是否正常: adb shell wm size,返回信息:Physical size: 1080x1800,完美。
  5. 通过virtualenv建立虚拟环境,安装项目所需的第三方库。
  6. 手机微信打开跳一跳,点击开始游戏。
  7. 运行wechat_jump_auto_ai.py,报错T_T…
    查看代码发现是截图部分操作不适配MX3,手动修改代码后成功截图运行
    1
    screenshot = screenshot.replace(b'\r\n', b'\n')
    修改为
    1
    screenshot = screenshot.replace(b'\r\r\n', b'\n')

第二次调试

按照程序逻辑,运行十次之后即可采用线性回归算法学习得到的公式,根据已知距离得出按压时间,但实际结果却和一个弱智一样,2分就挂掉了…
查看代码发现有个魔法数字要自己设置,程序根据这个数字进行截图计算误差:time.sleep(0.2)。

1
# 在跳跃落下的瞬间 摄像机移动前截图 这个参数要自己校调
2
time.sleep(0.2)
3
pull_screenshot_temp()
4
im_temp = Image.open('./autojump_temp.png')
5
temp_piece_x, temp_piece_y = find_piece(im_temp)
6
debug.computing_error(press_time, board_x, board_y, piece_x, piece_y, temp_piece_x, temp_piece_y)

经过debug截图不断调整,得出我的PC和MX3配合的最佳数值是0.04。
删除学习数据jump_range.csv,重新开始训练,程序终于能磕磕绊绊达到400分左右,但是大概需要1小时左右,对学习数据通过pandas和matplotlib进行绘图,看到训练采集到的数据离散程度很高,明显学习效果不佳。

第三次调试(重点)

再次阅读代码,发现这个AI版本的代码有瑕疵:

1
def computing_error(last_press_time, target_board_x, target_board_y, last_piece_x, last_piece_y, temp_piece_x, temp_piece_y):
2
	"""
3
	计算跳跃实际误差
4
	"""
5
	target_distance = math.sqrt(abs(target_board_x - last_piece_x) ** 2 + abs(target_board_y - last_piece_y) ** 2)  # 上一轮目标跳跃距离
6
	actual_distance = math.sqrt(abs(temp_piece_x - last_piece_x) ** 2 + abs(temp_piece_y - last_piece_y) ** 2)  # 上一轮实际跳跃距离
7
	jump_error_value = math.sqrt(abs(target_board_x - temp_piece_x) ** 2 + abs(target_board_y - temp_piece_y) ** 2)  # 跳跃误差
8
9
	print "目标距离: {0}, 实际距离: {1}, 误差距离: {2}, 蓄力时间: {3}ms".format(round(target_distance), round(actual_distance), round(jump_error_value), round(last_press_time))
10
	# 将结果采集进学习字典
11
12
	if last_piece_x > 0 and last_press_time > 0 :
13
		ai.add_data(round(actual_distance, 2), round(last_press_time))

问题1

target_distance与actual_distance有可能会等于0.0或者一个超出实际范围的值,这些值会在游戏失败重新开始时出现,原程序没有进行过滤;

问题2

jump_error_value作为一个很重要的值,没有进行有效利用。

上述两个问题会导致数据质量不高,干扰项太多,目标函数拟合速度太慢。

因此改进数据采集策略:

1
if jump_error_value < 5 and last_piece_x > 0 and last_press_time > 0 and target_distance > 0 and actual_distance > 0:
2
	ai.add_data(round(actual_distance, 2), round(last_press_time))

改进1

要求target_distance与actual_distance必须要大于0才是有效值,剔除干扰项;

改进2

jump_error_value合理利用,开始时将此参数放大到50左右,后续根据学习效果,逐步缩小jump_error_value的阈值,提高精确度,加速训练效果。

效果展示

训练数据拟合函数

经过大概四五轮训练,采样到571条数据,拟合出线性回归函数如图

debug误差效果

  1. 运行debug截图, 基本完美命中目标

  2. 运行debug日志,误差基本控制在40以内

自动运行视频

成绩

因为MX3手机硬件有问题,电量低于75%就会出现屏幕抖动,所以目前成绩如此
AI成绩

项目地址

优化后的代码以及训练数据: https://github.com/toddlerya/WechatJumpAI

后记

不得不承认,机器学习在合适的领域使用合适方式,达到的效果非常棒!只用了简单的线性回归算法,最经典最基本的机器学习算法,达到的效果已经秒杀人类上千倍。
为了不被这个时代淘汰,一定要跟上节奏,加油~


2018年01月07日 于 南京
Email
GitHub

闲扯

2017年的最后一天啦!今天是最后一批90后(1999年12月31日出生)的18岁生日,祝他们生日快乐!明天就是2018年啦,意味着90后已经全部成年,逐步成为社会的中流砥柱啦!
顺便吐槽下,被朋友圈的18岁照片刷屏啦,岁月是把杀猪刀,18岁的少年们都去哪啦!小伙伴们不要只顾着工作,也注意下身体啊,要变强不要变秃啊!不要变成油腻的中年胖子啊!

祝自己在2018年能更加努力,锻炼好身体!还有很多很多事情等我去做!Fight!

言归正传,在Golang学习(一)那篇博客中我们写了一个小工具,当时还没学习到并发,正好元旦假期学习了下并发,来实践改进下我们的工具。


正文

遗留Bug修复

文件复制过程中异常报错,程序没有退出的问题已经修复,创建文件和打开文件时加入了panic

1
srcFile, err := os.Open(src)
2
if err != nil {
3
    fmt.Println(err)
4
    panic("打开文件错误!")
5
}
6
defer srcFile.Close()
7
desFile, err := os.Create(des)
8
if err != nil {
9
    fmt.Println(err)
10
    panic("创建文件错误!")
11
}
12
defer desFile.Close()

使用Golang的gorutine来并发,提高性能

创建一个容量与期望生成文件个数大小相当的布尔型的chan,然后循环执行任务,每次向chan写入一个值,并通过读取chan来阻塞main函数的结束,伪代码如下

1
func main() {
2
    c := make(chan bool, *generateFileNumber)
3
    for count := 0; count < *generateFileNumber; count++ {
4
        dosomething(c)
5
    }
6
    <-c
7
}
8
func dosomething(c) {
9
    do
10
    c <- true
11
}

代码

1
/*
2
 * User: toddlerya
3
 * Date: 2017/12/23
4
 * Update: 2017/12/31
5
 * ds接入模块加压工具
6
 */
7
8
package main
9
10
import (
11
	"flag"
12
	"fmt"
13
	"io"
14
	"math/rand"
15
	"os"
16
	"runtime"
17
	"strconv"
18
	"strings"
19
	"time"
20
)
21
22
func judgeExists(name string) bool {
23
	if _, err := os.Stat(name); err != nil {
24
		if os.IsNotExist(err) {
25
			return false
26
		}
27
	}
28
	return true
29
}
30
31
func copyFile(c chan bool, src, des string) (w int64, err error) {
32
	srcFile, err := os.Open(src)
33
	if err != nil {
34
		fmt.Println(err)
35
		panic("打开文件错误!")
36
	}
37
	defer srcFile.Close()
38
39
	desFile, err := os.Create(des)
40
	if err != nil {
41
		fmt.Println(err)
42
		panic("创建文件错误!")
43
	}
44
	defer desFile.Close()
45
46
	c <- true
47
48
	return io.Copy(desFile, srcFile)
49
}
50
51
func generateRandomNumber(start int, end int) int {
52
	if end < 0 || start < 0 || (end-start) <= 0 {
53
		fmt.Println("[-] 随机数起始值[start]必须大于等于0, 截至值[end]必须大于起始值!")
54
		fmt.Printf("[-] 请检查配置是否正确: start=%v, end=%v\n", start, end)
55
		panic("随机数参数错误")
56
	}
57
	rand.Seed(time.Now().UnixNano())
58
	num := rand.Intn((end - start)) + start
59
	return num
60
}
61
62
func main() {
63
	runtime.GOMAXPROCS(runtime.NumCPU())
64
	srcFilePath := flag.String("s", "/home", "输入原始文件路径")
65
	dstFilePath := flag.String("d", "/tmp", "目标输出路径")
66
	renameFormat := flag.String("f", "abc-{random1}-456_780_{random2}.zip", "参数原始文件替换格式")
67
	random1Start := flag.Int("r1s", 100, "随机参数1的起始值")
68
	random1End := flag.Int("r1e", 999, "随机参数1的截止值")
69
	random2Start := flag.Int("r2s", 100000, "随机参数2的起始值")
70
	random2End := flag.Int("r2e", 999999, "随机参数2的截止值")
71
	generateFileNumber := flag.Int("g", 10000, "需要生成的文件个数")
72
	flag.Parse()
73
	fmt.Printf("[srcFilePath]          :%s\n", *srcFilePath)
74
	fmt.Printf("[dstFilePath]          :%s\n", *dstFilePath)
75
	fmt.Printf("[renameFormat]         :%s\n", *renameFormat)
76
	fmt.Printf("[random1Start]         :%v\n", *random1Start)
77
	fmt.Printf("[random1End]           :%v\n", *random1End)
78
	fmt.Printf("[random2Start]         :%v\n", *random2Start)
79
	fmt.Printf("[random2End]           :%v\n", *random2End)
80
	fmt.Printf("[generateFileNumber]   :%v\n", *generateFileNumber)
81
	fmt.Println("=======================================")
82
	t1 := time.Now()
83
	if judgeExists(*srcFilePath) {
84
		c := make(chan bool, *generateFileNumber)
85
		fmt.Println("[+] 开始随机生成目标数据, 请注意", *dstFilePath, "目录是否有数据生成\t", "计划生成", *generateFileNumber, "个文件Orz")
86
		for count := 0; count < *generateFileNumber; count++ {
87
			random1Val := generateRandomNumber(*random1Start, *random1End)
88
			random2Val := generateRandomNumber(*random2Start, *random2End)
89
			temp := strings.Replace(*renameFormat, "{random1}", strconv.Itoa(random1Val), -1)
90
			newFileName := strings.Replace(temp, "{random2}", strconv.Itoa(random2Val), -1)
91
			dstJoinList := []string{*dstFilePath, newFileName}
92
			newDstFilePath := strings.Join(dstJoinList, "/")
93
			// fmt.Println("[*] 输出随机生成的文件: ", newDstFilePath)
94
			go copyFile(c, *srcFilePath, newDstFilePath)
95
		}
96
		<-c
97
		fmt.Println("[+] 已经完成目标: 共计生成", *generateFileNumber, "个文件")
98
	} else {
99
		fmt.Println("[-] 原始文件不存在, 请检查: ", *srcFilePath)
100
	}
101
	elapsed := time.Since(t1)
102
	fmt.Println("运行耗时: ", elapsed)
103
}

使用效果如下

没有使用gorutine之前

使用gorutine后的并发效果

异常处理

第一次自己写的代码遇到这个文件打开数过多的问题,还有点小激动呢!查看下系统当前的ulimit open files配置值

果断手动改大些(个人笔记本,不会经常进新大量IO操作,就临时改下吧)ulimit -n 65535

修改后的使用效果就是上面的并发测试那个图啦!

后续

其实还有一个使用了sync的版本,但是测试效果和没有使用并发的测试结果一样,甚至还要差一些,还是不懂的太多,留个坑,等搞明白了再来补充。

相关伪代码逻辑如下

1
func main() {
2
    wg := sync.WaitGroup{}
3
	wg.Add(*generateFileNumber)
4
    for count := 0; count < *generateFileNumber; count++ {
5
        dosomething(&wg)
6
    }
7
    wg.Wait()
8
}
9
func dosomething(wg *sync.WaitGroup) {
10
    do
11
    wg.Done()
12
}
13
{% endcodeblock %}

2017年12月31日 于 南京
Email
GitHub