从 Hello World 容器进阶是件困难的事情

在我的上一篇文章里, 我介绍了 Linux 容器背后的技术的概念。我写了我知道的一切。容器对我来说也是比较新的概念。我写这篇文章的目的就是鼓励我真正的来学习这些东西。

我打算在使用中学习。首先实践,然后上手并记录下我是怎么走过来的。我假设这里肯定有很多像 "Hello World" 这种类型的知识帮助我快速的掌握基础。然后我能够更进一步,构建一个微服务容器或者其它东西。

我想,它应该不会有多难的。

但是我错了。

可能对某些人来说这很简单,因为他们在运维工作方面付出了大量的时间。但是对我来说实际上是很困难的,可以从我在Facebook 上的状态展示出来的挫折感就可以看出了。

但是还有一个好消息:我最终搞定了。而且它工作的还不错。所以我准备分享向你分享我如何制作我的第一个微服务容器。我的痛苦可能会节省你不少时间呢。

如果你曾经发现你也处于过这种境地,不要害怕:像我这样的人都能搞定,所以你也肯定行。

让我们开始吧。

从 Hello World 容器进阶是件困难的事情

 

一个缩略图微服务

我设计的微服务在理论上很简单。以 JPG 或者 PNG 格式在 HTTP 终端发布一张数字照片,然后获得一个100像素宽的缩略图。

下面是它的流程:

从 Hello World 容器进阶是件困难的事情

container-diagram-0

我决定使用 NodeJS 作为我的开发语言,使用 ImageMagick 来转换缩略图。

我的服务的第一版的逻辑如下所示:

从 Hello World 容器进阶是件困难的事情

container-diagram-1

我下载了 Docker Toolbox,用它安装了 Docker 的快速启动终端(Docker Quickstart Terminal)。Docker 快速启动终端使得创建容器更简单了。终端会启动一个装好了 Docker 的 Linux 虚拟机,它允许你在一个终端里运行 Docker 命令。

虽然在我的例子里,我的操作系统是 Mac OS X。但是 Windows 下也有相同的工具。

我准备使用 Docker 快速启动终端里为我的微服务创建一个容器镜像,然后从这个镜像运行容器。

Docker 快速启动终端就运行在你使用的普通终端里,就像这样:

从 Hello World 容器进阶是件困难的事情

container-diagram-2

 

第一个小问题和第一个大问题

我用 NodeJS 和 ImageMagick 瞎搞了一通,然后让我的服务在本地运行起来了。

然后我创建了 Dockerfile,这是 Docker 用来构建容器的配置脚本。(我会在后面深入介绍构建过程和 Dockerfile)

这是我运行 Docker 快速启动终端的命令:

  1. <span class="pln">$ docker build </span><span class="pun">-</span><span class="pln">t thumbnailer</span><span class="pun">:</span><span class="lit">0.1</span>

获得如下回应:

  1. <span class="pln">docker</span><span class="pun">:</span><span class="str">"build"</span><span class="pln"> requires </span><span class="lit">1</span><span class="pln"> argument</span><span class="pun">.</span>

呃。

我估摸着过了15分钟我才反应过来:我忘记了在末尾参数输入一个点.

正确的指令应该是这样的:

  1. <span class="pln">$ docker build </span><span class="pun">-</span><span class="pln">t thumbnailer</span><span class="pun">:</span><span class="lit">0.1</span><span class="pun">.</span>

但是这不是我遇到的最后一个问题。

我让这个镜像构建好了,然后我在 Docker 快速启动终端输入了 run 命令来启动容器,名字叫 thumbnailer:0.1:

  1. <span class="pln">$ docker run </span><span class="pun">-</span><span class="pln">d </span><span class="pun">-</span><span class="pln">p </span><span class="lit">3001</span><span class="pun">:</span><span class="lit">3000</span><span class="pln"> thumbnailer</span><span class="pun">:</span><span class="lit">0.1</span>

参数 -p 3001:3000 让 NodeJS 微服务在 Docker 内运行在端口3000,而绑定在宿主主机上的3001。

到目前看起来都很好,对吧?

错了。事情要马上变糟了。

我通过运行 docker-machine 命令为这个 Docker 快速启动终端里创建的虚拟机指定了 ip 地址:

  1. <span class="pln">$ docker</span><span class="pun">-</span><span class="pln">machine </span><span class="kwd">ip</span><span class="kwd">default</span>

这句话返回了默认虚拟机的 IP 地址,它运行在 Docker 快速启动终端里。在我这里,这个 ip 地址是 192.168.99.100。

我浏览网页 http://192.168.99.100:3001/ ,然后找到了我创建的上传图片的网页:

从 Hello World 容器进阶是件困难的事情

container-diagram-3

我选择了一个文件,然后点击上传图片的按钮。

但是它并没有工作。

终端告诉我他无法找到我的微服务需要的 /upload 目录。

现在,你要知道,我已经在此耗费了将近一天的时间-从浪费时间到研究问题。我此时感到了一些挫折感。

然后灵光一闪。某人记起来微服务不应该自己做任何数据持久化的工作!保存数据应该是另一个服务的工作。

所以容器找不到目录 /upload 的原因到底是什么?这个问题的根本就是我的微服务在基础设计上就有问题。

让我们看看另一幅图:

从 Hello World 容器进阶是件困难的事情

container-diagram-4

我为什么要把文件保存到磁盘?微服务按理来说是很快的。为什么不能让我的全部工作都在内存里完成?使用内存缓冲可以解决“找不到目录”这个问题,而且可以提高我的应用的性能。

这就是我现在所做的。下面是我的计划:

从 Hello World 容器进阶是件困难的事情

container-diagram-5

这是我用 NodeJS 写的在内存运行、生成缩略图的代码:

  1. <span class="com">// Bind to the packages</span>
  2. <span class="kwd">var</span><span class="pln"> express </span><span class="pun">=</span><span class="kwd">require</span><span class="pun">(</span><span class="str">'express'</span><span class="pun">);</span>
  3. <span class="kwd">var</span><span class="pln"> router </span><span class="pun">=</span><span class="pln"> express</span><span class="pun">.</span><span class="typ">Router</span><span class="pun">();</span>
  4. <span class="kwd">var</span><span class="pln"> path </span><span class="pun">=</span><span class="kwd">require</span><span class="pun">(</span><span class="str">'path'</span><span class="pun">);</span><span class="com">// used for file path</span>
  5. <span class="kwd">var</span><span class="pln"> im </span><span class="pun">=</span><span class="kwd">require</span><span class="pun">(</span><span class="str">"imagemagick"</span><span class="pun">);</span>
  6. <span class="com">// Simple get that allows you test that you can access the thumbnail process</span>
  7. <span class="pln">router</span><span class="pun">.</span><span class="kwd">get</span><span class="pun">(</span><span class="str">'/'</span><span class="pun">,</span><span class="kwd">function</span><span class="pun">(</span><span class="pln">req</span><span class="pun">,</span><span class="pln"> res</span><span class="pun">,</span><span class="kwd">next</span><span class="pun">)</span><span class="pun">{</span>
  8. <span class="pln">res</span><span class="pun">.</span><span class="pln">status</span><span class="pun">(</span><span class="lit">200</span><span class="pun">).</span><span class="pln">send</span><span class="pun">(</span><span class="str">'Thumbnailer processor is up and running'</span><span class="pun">);</span>
  9. <span class="pun">});</span>
  10. <span class="com">// This is the POST handler. It will take the uploaded file and make a thumbnail from the </span>
  11. <span class="com">// submitted byte array. I know, it's not rocket science, but it serves a purpose</span>
  12. <span class="pln">router</span><span class="pun">.</span><span class="pln">post</span><span class="pun">(</span><span class="str">'/'</span><span class="pun">,</span><span class="kwd">function</span><span class="pun">(</span><span class="pln">req</span><span class="pun">,</span><span class="pln"> res</span><span class="pun">,</span><span class="kwd">next</span><span class="pun">)</span><span class="pun">{</span>
  13. <span class="pln">req</span><span class="pun">.</span><span class="pln">pipe</span><span class="pun">(</span><span class="pln">req</span><span class="pun">.</span><span class="pln">busboy</span><span class="pun">);</span>
  14. <span class="pln">req</span><span class="pun">.</span><span class="pln">busboy</span><span class="pun">.</span><span class="pln">on</span><span class="pun">(</span><span class="str">'file'</span><span class="pun">,</span><span class="kwd">function</span><span class="pun">(</span><span class="pln">fieldname</span><span class="pun">,</span><span class="kwd">file</span><span class="pun">,</span><span class="pln"> filename</span><span class="pun">)</span><span class="pun">{</span>
  15. <span class="kwd">var</span><span class="pln"> ext </span><span class="pun">=</span><span class="pln"> path</span><span class="pun">.</span><span class="pln">extname</span><span class="pun">(</span><span class="pln">filename</span><span class="pun">)</span>
  16. <span class="com">// Make sure that only png and jpg is allowed </span>
  17. <span class="kwd">if</span><span class="pun">(</span><span class="pln">ext</span><span class="pun">.</span><span class="pln">toLowerCase</span><span class="pun">()</span><span class="pun">!=</span><span class="str">'.jpg'</span><span class="pun">&&</span><span class="pln"> ext</span><span class="pun">.</span><span class="pln">toLowerCase</span><span class="pun">()</span><span class="pun">!=</span><span class="str">'.png'</span><span class="pun">){</span>
  18. <span class="pln">res</span><span class="pun">.</span><span class="pln">status</span><span class="pun">(</span><span class="lit">406</span><span class="pun">).</span><span class="pln">send</span><span class="pun">(</span><span class="str">"Service accepts only jpg or png files"</span><span class="pun">);</span>
  19. <span class="pun">}</span>
  20. <span class="kwd">var</span><span class="pln"> bytes </span><span class="pun">=</span><span class="pun">[];</span>
  21. <span class="com">// put the bytes from the request into a byte array </span>
  22. <span class="kwd">file</span><span class="pun">.</span><span class="pln">on</span><span class="pun">(</span><span class="str">'data'</span><span class="pun">,</span><span class="kwd">function</span><span class="pun">(</span><span class="pln">data</span><span class="pun">)</span><span class="pun">{</span>
  23. <span class="kwd">for</span><span class="pun">(</span><span class="kwd">var</span><span class="pln"> i </span><span class="pun">=</span><span class="lit">0</span><span class="pun">;</span><span class="pln"> i </span><span class="pun"><</span><span class="pln"> data</span><span class="pun">.</span><span class="pln">length</span><span class="pun">;</span><span class="pun">++</span><span class="pln">i</span><span class="pun">)</span><span class="pun">{</span>
  24. <span class="pln">bytes</span><span class="pun">.</span><span class="pln">push</span><span class="pun">(</span><span class="pln">data</span><span class="pun">[</span><span class="pln">i</span><span class="pun">]);</span>
  25. <span class="pun">}</span>
  26. <span class="pln">console</span><span class="pun">.</span><span class="pln">log</span><span class="pun">(</span><span class="str">'File ['</span><span class="pun">+</span><span class="pln"> fieldname </span><span class="pun">+</span><span class="str">'] got bytes '</span><span class="pun">+</span><span class="pln"> bytes</span><span class="pun">.</span><span class="pln">length </span><span class="pun">+</span><span class="str">' bytes'</span><span class="pun">);</span>
  27. <span class="pun">});</span>
  28. <span class="com">// Once the request is finished pushing the file bytes into the array, put the bytes in </span>
  29. <span class="com">// a buffer and process that buffer with the imagemagick resize function</span>
  30. <span class="kwd">file</span><span class="pun">.</span><span class="pln">on</span><span class="pun">(</span><span class="str">'end'</span><span class="pun">,</span><span class="kwd">function</span><span class="pun">()</span><span class="pun">{</span>
  31. <span class="kwd">var</span><span class="pln"> buffer </span><span class="pun">=</span><span class="kwd">new</span><span class="typ">Buffer</span><span class="pun">(</span><span class="pln">bytes</span><span class="pun">,</span><span class="str">'binary'</span><span class="pun">);</span>
  32. <span class="pln">console</span><span class="pun">.</span><span class="pln">log</span><span class="pun">(</span><span class="str">'Bytes got '</span><span class="pun">+</span><span class="pln"> bytes</span><span class="pun">.</span><span class="pln">length </span><span class="pun">+</span><span class="str">' bytes'</span><span class="pun">);</span>
  33. <span class="com">//resize</span>
  34. <span class="pln">im</span><span class="pun">.</span><span class="pln">resize</span><span class="pun">({</span>
  35. <span class="pln">srcData</span><span class="pun">:</span><span class="pln"> buffer</span><span class="pun">,</span>
  36. <span class="pln">height</span><span class="pun">:</span><span class="lit">100</span>
  37. <span class="pun">},</span><span class="kwd">function</span><span class="pun">(</span><span class="pln">err</span><span class="pun">,</span><span class="pln"> stdout</span><span class="pun">,</span><span class="pln"> stderr</span><span class="pun">){</span>
  38. <span class="kwd">if</span><span class="pun">(</span><span class="pln">err</span><span class="pun">){</span>
  39. <span class="kwd">throw</span><span class="pln"> err</span><span class="pun">;</span>
  40. <span class="pun">}</span>
  41. <span class="com">// get the extension without the period</span>
  42. <span class="kwd">var</span><span class="pln"> typ </span><span class="pun">=</span><span class="pln"> path</span><span class="pun">.</span><span class="pln">extname</span><span class="pun">(</span><span class="pln">filename</span><span class="pun">).</span><span class="pln">replace</span><span class="pun">(</span><span class="str">'.'</span><span class="pun">,</span><span class="str">''</span><span class="pun">);</span>
  43. <span class="pln">res</span><span class="pun">.</span><span class="pln">setHeader</span><span class="pun">(</span><span class="str">"content-type"</span><span class="pun">,</span><span class="str">"image/"</span><span class="pun">+</span><span class="pln"> typ</span><span class="pun">);</span>
  44. <span class="pln">res</span><span class="pun">.</span><span class="pln">status</span><span class="pun">(</span><span class="lit">200</span><span class="pun">);</span>
  45. <span class="com">// send the image back as a response</span>
  46. <span class="pln">res</span><span class="pun">.</span><span class="pln">send</span><span class="pun">(</span><span class="kwd">new</span><span class="typ">Buffer</span><span class="pun">(</span><span class="pln">stdout</span><span class="pun">,</span><span class="str">'binary'</span><span class="pun">));</span>
  47. <span class="pun">});</span>
  48. <span class="pun">});</span>
  49. <span class="pun">});</span>
  50. <span class="pun">});</span>
  51. <span class="kwd">module</span><span class="pun">.</span><span class="pln">exports </span><span class="pun">=</span><span class="pln"> router</span><span class="pun">;</span>

好了,一切回到了正轨,已经可以在我的本地机器正常工作了。我该去休息了。

但是,在我测试把这个微服务当作一个普通的 Node 应用运行在本地时...

从 Hello World 容器进阶是件困难的事情

Containers Hard

它工作的很好。现在我要做的就是让它在容器里面工作。

第二天我起床后喝点咖啡,然后创建一个镜像——这次没有忘记那个"."!

  1. <span class="pln">$ docker build </span><span class="pun">-</span><span class="pln">t thumbnailer</span><span class="pun">:</span><span class="lit">01</span><span class="pun">.</span>

我从缩略图项目的根目录开始构建。构建命令使用了根目录下的 Dockerfile。它是这样工作的:把 Dockerfile 放到你想构建镜像的地方,然后系统就默认使用这个 Dockerfile。

下面是我使用的Dockerfile 的内容:

  1. <span class="pln">FROM Ubuntu</span><span class="pun">:</span><span class="pln">latest</span>
  2. <span class="pln">MAINTAINER bob@CogArtTech</span><span class="pun">.</span><span class="pln">com</span>
  3. <span class="pln">RUN </span><span class="kwd">apt-get</span><span class="pln"> update</span>
  4. <span class="pln">RUN </span><span class="kwd">apt-get</span><span class="pln"> install </span><span class="pun">-</span><span class="pln">y nodejs nodejs</span><span class="pun">-</span><span class="pln">legacy npm</span>
  5. <span class="pln">RUN </span><span class="kwd">apt-get</span><span class="pln"> install imagemagick libmagickcore</span><span class="pun">-</span><span class="pln">dev libmagickwand</span><span class="pun">-</span><span class="pln">dev</span>
  6. <span class="pln">RUN </span><span class="kwd">apt-get</span><span class="pln"> clean</span>
  7. <span class="pln">COPY </span><span class="pun">./</span><span class="kwd">package</span><span class="pun">.</span><span class="pln">json src</span><span class="pun">/</span>
  8. <span class="pln">RUN </span><span class="kwd">cd</span><span class="pln"> src </span><span class="pun">&&</span><span class="pln"> npm install</span>
  9. <span class="pln">COPY </span><span class="pun">.</span><span class="pun">/</span><span class="pln">src</span>
  10. <span class="pln">WORKDIR src</span><span class="pun">/</span>
  11. <span class="pln">CMD npm start</span>

这怎么可能出错呢?

 

第二个大问题

我运行了 build 命令,然后出了这个错:

  1. <span class="typ">Do</span><span class="pln"> you want to </span><span class="kwd">continue</span><span class="pun">?</span><span class="pun">[</span><span class="pln">Y</span><span class="pun">/</span><span class="pln">n</span><span class="pun">]</span><span class="typ">Abort</span><span class="pun">.</span>
  2. <span class="typ">The</span><span class="pln"> command </span><span class="str">'/bin/sh -c apt-get install imagemagick libmagickcore-dev libmagickwand-dev'</span><span class="pln"> returned a non</span><span class="pun">-</span><span class="pln">zero code</span><span class="pun">:</span><span class="lit">1</span>

我猜测微服务出错了。我回到本地机器,从本机启动微服务,然后试着上传文件。

然后我从 NodeJS 获得了这个错误:

  1. <span class="typ">Error</span><span class="pun">:</span><span class="pln"> spawn convert ENOENT</span>

怎么回事?之前还是好好的啊!

我搜索了我能想到的所有的错误原因。差不多4个小时后,我想:为什么不重启一下机器呢?

重启了,你猜猜结果?错误消失了!(LCTT 译注:万能的“重启试试”)

继续。

 

将精灵关进瓶子里

跳回正题:我需要完成构建工作。

我使用 rm 命令删除了虚拟机里所有的容器。

  1. <span class="pln">$ docker </span><span class="kwd">rm</span><span class="pun">-</span><span class="pln">f $</span><span class="pun">(</span><span class="pln">docker </span><span class="kwd">ps</span><span class="pun">-</span><span class="pln">a </span><span class="pun">-</span><span class="pln">q</span><span class="pun">)</span>

-f 在这里的用处是强制删除运行中的镜像。

然后删除了全部 Docker 镜像,用的是命令 rmi:

  1. <span class="pln">$ docker rmi </span><span class="kwd">if</span><span class="pln"> $</span><span class="pun">(</span><span class="pln">docker images </span><span class="pun">|</span><span class="kwd">tail</span><span class="pun">-</span><span class="pln">n </span><span class="pun">+</span><span class="lit">2</span><span class="pun">|</span><span class="pln"> awk </span><span class="str">'{print $3}'</span><span class="pun">)</span>

我重新执行了重新构建镜像、安装容器、运行微服务的整个过程。然后过了一个充满自我怀疑和沮丧的一个小时,我告诉我自己:这个错误可能不是微服务的原因。

所以我重新看到了这个错误:

  1. <span class="typ">Do</span><span class="pln"> you want to </span><span class="kwd">continue</span><span class="pun">?</span><span class="pun">[</span><span class="pln">Y</span><span class="pun">/</span><span class="pln">n</span><span class="pun">]</span><span class="typ">Abort</span><span class="pun">.</span>
  2. <span class="typ">The</span><span class="pln"> command </span><span class="str">'/bin/sh -c apt-get install imagemagick libmagickcore-dev libmagickwand-dev'</span><span class="pln"> returned a non</span><span class="pun">-</span><span class="pln">zero code</span><span class="pun">:</span><span class="lit">1</span>

这太打击我了:构建脚本好像需要有人从键盘输入 Y! 但是,这是一个非交互的 Dockerfile 脚本啊。这里并没有键盘。

回到 Dockerfile,脚本原来是这样的:

  1. <span class="pln">RUN </span><span class="kwd">apt-get</span><span class="pln"> update</span>
  2. <span class="pln">RUN </span><span class="kwd">apt-get</span><span class="pln"> install </span><span class="pun">-</span><span class="pln">y nodejs nodejs</span><span class="pun">-</span><span class="pln">legacy npm</span>
  3. <span class="pln">RUN </span><span class="kwd">apt-get</span><span class="pln"> install imagemagick libmagickcore</span><span class="pun">-</span><span class="pln">dev libmagickwand</span><span class="pun">-</span><span class="pln">dev</span>
  4. <span class="pln">RUN </span><span class="kwd">apt-get</span><span class="pln"> clean</span>

第二个apt-get 忘记了-y 标志,它用于自动应答提示所需要的“yes”。这才是错误的根本原因。

我在这条命令后面添加了-y

  1. <span class="pln">RUN </span><span class="kwd">apt-get</span><span class="pln"> update</span>
  2. <span class="pln">RUN </span><span class="kwd">apt-get</span><span class="pln"> install </span><span class="pun">-</span><span class="pln">y nodejs nodejs</span><span class="pun">-</span><span class="pln">legacy npm</span>
  3. <span class="pln">RUN </span><span class="kwd">apt-get</span><span class="pln"> install </span><span class="pun">-</span><span class="pln">y imagemagick libmagickcore</span><span class="pun">-</span><span class="pln">dev libmagickwand</span><span class="pun">-</span><span class="pln">dev</span>
  4. <span class="pln">RUN </span><span class="kwd">apt-get</span><span class="pln"> clean</span>

猜一猜结果:经过将近两天的尝试和痛苦,容器终于正常工作了!整整两天啊!

我完成了构建工作:

  1. <span class="pln">$ docker build </span><span class="pun">-</span><span class="pln">t thumbnailer</span><span class="pun">:</span><span class="lit">0.1</span><span class="pun">.</span>

启动了容器:

  1. <span class="pln">$ docker run </span><span class="pun">-</span><span class="pln">d </span><span class="pun">-</span><span class="pln">p </span><span class="lit">3001</span><span class="pun">:</span><span class="lit">3000</span><span class="pln"> thumbnailer</span><span class="pun">:</span><span class="lit">0.1</span>

获取了虚拟机的IP 地址:

  1. <span class="pln">$ docker</span><span class="pun">-</span><span class="pln">machine </span><span class="kwd">ip</span><span class="kwd">default</span>

在我的浏览器里面输入 http://192.168.99.100:3001/

上传页面打开了。

我选择了一个图片,然后得到了这个:

从 Hello World 容器进阶是件困难的事情

container-diagram-7

工作了!

在容器里面工作了,我的第一次啊!

 

这让我学到了什么?

很久以前,我接受了这样一个道理:当你刚开始尝试某项技术时,即使是最简单的事情也会变得很困难。因此,我不会把自己当成最聪明的那个人,然而最近几天尝试容器的过程就是一个充满自我怀疑的旅程。

但是你想知道一些其它的事情吗?这篇文章是我在凌晨2点完成的,而每一个受折磨的时刻都值得了。为什么?因为这段时间你将自己全身心投入了喜欢的工作里。这件事很难,对于所有人来说都不是很容易就获得结果的。但是不要忘记:你在学习技术,运行世界的技术。

P.S. 了解一下Hello World 容器的两段视频,这里会有 Raziel Tabib’s 的精彩工作内容。

 

 

千万被忘记第二部分...

 

 


via: https://deis.com/blog/2015/beyond-hello-world-containers-hard-stuff

作者:Bob Reselman 译者:Ezio 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

相关推荐