從 Hello World 容器進階是件困難的事情
在我的上一篇文章裡, 我介紹了 Linux 容器背後的技術的概念。我寫了我知道的一切。容器對我來說也是比較新的概念。我寫這篇文章的目的就是鼓勵我真正的來學習這些東西。
我打算在使用中學習。首先實踐,然後上手並記錄下我是怎麼走過來的。我假設這裡肯定有很多像 “Hello World” 這種型別的知識幫助我快速的掌握基礎。然後我能夠更進一步,構建一個微服務容器或者其它東西。
我想,它應該不會有多難的。
但是我錯了。
可能對某些人來說這很簡單,因為他們在運維工作方面付出了大量的時間。但是對我來說實際上是很困難的,可以從我在Facebook 上的狀態展示出來的挫折感就可以看出了。
但是還有一個好訊息:我最終搞定了。而且它工作的還不錯。所以我準備分享向你分享我如何製作我的第一個微服務容器。我的痛苦可能會節省你不少時間呢。
如果你曾經發現你也處於過這種境地,不要害怕:像我這樣的人都能搞定,所以你也肯定行。
讓我們開始吧。
一個縮圖微服務
我設計的微服務在理論上很簡單。以 JPG 或者 PNG 格式在 HTTP 終端釋出一張數字照片,然後獲得一個100畫素寬的縮圖。
下面是它的流程:
我決定使用 NodeJS 作為我的開發語言,使用 ImageMagick 來轉換縮圖。
我的服務的第一版的邏輯如下所示:
我下載了 Docker Toolbox,用它安裝了 Docker 的快速啟動終端(Docker Quickstart Terminal)。Docker 快速啟動終端使得建立容器更簡單了。終端會啟動一個裝好了 Docker 的 Linux 虛擬機器,它允許你在一個終端裡執行 Docker 命令。
雖然在我的例子裡,我的作業系統是 Mac OS X。但是 Windows 下也有相同的工具。
我準備使用 Docker 快速啟動終端裡為我的微服務建立一個容器映象,然後從這個映象執行容器。
Docker 快速啟動終端就執行在你使用的普通終端裡,就像這樣:
第一個小問題和第一個大問題
我用 NodeJS 和 ImageMagick 瞎搞了一通,然後讓我的服務在本地執行起來了。
然後我建立了 Dockerfile,這是 Docker 用來構建容器的配置指令碼。(我會在後面深入介紹構建過程和 Dockerfile)
這是我執行 Docker 快速啟動終端的命令:
$ docker build -t thumbnailer:0.1
獲得如下回應:
docker: "build" requires 1 argument.
呃。
我估摸著過了15分鐘我才反應過來:我忘記了在末尾引數輸入一個點.。
正確的指令應該是這樣的:
$ docker build -t thumbnailer:0.1 .
但是這不是我遇到的最後一個問題。
我讓這個映象構建好了,然後我在 Docker 快速啟動終端輸入了 run 命令來啟動容器,名字叫 thumbnailer:0.1:
$ docker run -d -p 3001:3000 thumbnailer:0.1
引數 -p 3001:3000 讓 NodeJS 微服務在 Docker 內執行在埠3000,而繫結在宿主主機上的3001。
到目前看起來都很好,對吧?
錯了。事情要馬上變糟了。
我通過執行 docker-machine 命令為這個 Docker 快速啟動終端裡建立的虛擬機器指定了 ip 地址:
$ docker-machine ip default
這句話返回了預設虛擬機器的 IP 地址,它執行在 Docker 快速啟動終端裡。在我這裡,這個 ip 地址是 192.168.99.100。
我瀏覽網頁 http://192.168.99.100:3001/ ,然後找到了我建立的上傳圖片的網頁:
我選擇了一個檔案,然後點選上傳圖片的按鈕。
但是它並沒有工作。
終端告訴我他無法找到我的微服務需要的 /upload 目錄。
現在,你要知道,我已經在此耗費了將近一天的時間-從浪費時間到研究問題。我此時感到了一些挫折感。
然後靈光一閃。某人記起來微服務不應該自己做任何資料持久化的工作!儲存資料應該是另一個服務的工作。
所以容器找不到目錄 /upload 的原因到底是什麼?這個問題的根本就是我的微服務在基礎設計上就有問題。
讓我們看看另一幅圖:
我為什麼要把檔案儲存到磁碟?微服務按理來說是很快的。為什麼不能讓我的全部工作都在記憶體裡完成?使用記憶體緩衝可以解決“找不到目錄”這個問題,而且可以提高我的應用的效能。
這就是我現在所做的。下面是我的計劃:
這是我用 NodeJS 寫的在記憶體執行、生成縮圖的程式碼:
// Bind to the packages var express = require('express'); var router = express.Router(); var path = require('path'); // used for file path var im = require("imagemagick"); // Simple get that allows you test that you can access the thumbnail process router.get('/', function (req, res, next) { res.status(200).send('Thumbnailer processor is up and running'); }); // This is the POST handler. It will take the uploaded file and make a thumbnail from the // submitted byte array. I know, it's not rocket science, but it serves a purpose router.post('/', function (req, res, next) { req.pipe(req.busboy); req.busboy.on('file', function (fieldname, file, filename) { var ext = path.extname(filename) // Make sure that only png and jpg is allowed if(ext.toLowerCase() != '.jpg' && ext.toLowerCase() != '.png'){ res.status(406).send("Service accepts only jpg or png files"); } var bytes = []; // put the bytes from the request into a byte array file.on('data', function(data) { for (var i = 0; i < data.length; ++i) { bytes.push(data[i]); } console.log('File [' + fieldname + '] got bytes ' + bytes.length + ' bytes'); }); // Once the request is finished pushing the file bytes into the array, put the bytes in // a buffer and process that buffer with the imagemagick resize function file.on('end', function() { var buffer = new Buffer(bytes,'binary'); console.log('Bytes got ' + bytes.length + ' bytes'); //resize im.resize({ srcData: buffer, height: 100 }, function(err, stdout, stderr){ if (err){ throw err; } // get the extension without the period var typ = path.extname(filename).replace('.',''); res.setHeader("content-type", "image/" + typ); res.status(200); // send the image back as a response res.send(new Buffer(stdout,'binary')); }); }); }); }); module.exports = router;
好了,一切回到了正軌,已經可以在我的本地機器正常工作了。我該去休息了。
但是,在我測試把這個微服務當作一個普通的 Node 應用執行在本地時…
它工作的很好。現在我要做的就是讓它在容器裡面工作。
第二天我起床後喝點咖啡,然後建立一個映象——這次沒有忘記那個”.”!
$ docker build -t thumbnailer:01 .
我從縮圖專案的根目錄開始構建。構建命令使用了根目錄下的 Dockerfile。它是這樣工作的:把 Dockerfile 放到你想構建映象的地方,然後系統就預設使用這個 Dockerfile。
下面是我使用的Dockerfile 的內容:
FROM ubuntu:latest MAINTAINER bob@CogArtTech.com RUN apt-get update RUN apt-get install -y nodejs nodejs-legacy npm RUN apt-get install imagemagick libmagickcore-dev libmagickwand-dev RUN apt-get clean COPY ./package.json src/ RUN cd src && npm install COPY . /src WORKDIR src/ CMD npm start
這怎麼可能出錯呢?
第二個大問題
我執行了 build 命令,然後出了這個錯:
Do you want to continue? [Y/n] Abort. The command '/bin/sh -c apt-get install imagemagick libmagickcore-dev libmagickwand-dev' returned a non-zero code: 1
我猜測微服務出錯了。我回到本地機器,從本機啟動微服務,然後試著上傳檔案。
然後我從 NodeJS 獲得了這個錯誤:
Error: spawn convert ENOENT
怎麼回事?之前還是好好的啊!
我搜尋了我能想到的所有的錯誤原因。差不多4個小時後,我想:為什麼不重啟一下機器呢?
重啟了,你猜猜結果?錯誤消失了!(LCTT 譯註:萬能的“重啟試試”)
繼續。
將精靈關進瓶子裡
跳回正題:我需要完成構建工作。
我使用 rm 命令刪除了虛擬機器裡所有的容器。
$ docker rm -f $(docker ps -a -q)
-f 在這裡的用處是強制刪除執行中的映象。
然後刪除了全部 Docker 映象,用的是命令 rmi:
$ docker rmi if $(docker images | tail -n +2 | awk '{print $3}')
我重新執行了重新構建映象、安裝容器、執行微服務的整個過程。然後過了一個充滿自我懷疑和沮喪的一個小時,我告訴我自己:這個錯誤可能不是微服務的原因。
所以我重新看到了這個錯誤:
Do you want to continue? [Y/n] Abort. The command '/bin/sh -c apt-get install imagemagick libmagickcore-dev libmagickwand-dev' returned a non-zero code: 1
這太打擊我了:構建指令碼好像需要有人從鍵盤輸入 Y! 但是,這是一個非互動的 Dockerfile 指令碼啊。這裡並沒有鍵盤。
回到 Dockerfile,指令碼原來是這樣的:
RUN apt-get update RUN apt-get install -y nodejs nodejs-legacy npm RUN apt-get install imagemagick libmagickcore-dev libmagickwand-dev RUN apt-get clean
第二個apt-get 忘記了-y 標誌,它用於自動應答提示所需要的“yes”。這才是錯誤的根本原因。
我在這條命令後面新增了-y :
RUN apt-get update RUN apt-get install -y nodejs nodejs-legacy npm RUN apt-get install -y imagemagick libmagickcore-dev libmagickwand-dev RUN apt-get clean
猜一猜結果:經過將近兩天的嘗試和痛苦,容器終於正常工作了!整整兩天啊!
我完成了構建工作:
$ docker build -t thumbnailer:0.1 .
啟動了容器:
$ docker run -d -p 3001:3000 thumbnailer:0.1
獲取了虛擬機器的IP 地址:
$ docker-machine ip default
在我的瀏覽器裡面輸入 http://192.168.99.100:3001/ :
上傳頁面開啟了。
我選擇了一個圖片,然後得到了這個:
工作了!
在容器裡面工作了,我的第一次啊!
這讓我學到了什麼?
很久以前,我接受了這樣一個道理:當你剛開始嘗試某項技術時,即使是最簡單的事情也會變得很困難。因此,我不會把自己當成最聰明的那個人,然而最近幾天嘗試容器的過程就是一個充滿自我懷疑的旅程。
但是你想知道一些其它的事情嗎?這篇文章是我在凌晨2點完成的,而每一個受折磨的時刻都值得了。為什麼?因為這段時間你將自己全身心投入了喜歡的工作裡。這件事很難,對於所有人來說都不是很容易就獲得結果的。但是不要忘記:你在學習技術,執行世界的技術。
P.S. 瞭解一下Hello World 容器的兩段視訊,這裡會有 Raziel Tabib’s 的精彩工作內容。
千萬被忘記第二部分…
相關文章
- 從零搭建Spring Boot的Hello WorldSpring Boot
- Hello, World
- Hello World!
- Hello World
- react的”Hello World !“React
- WebGL 的 Hello WorldWeb
- JVM中的Hello World是如何執行的?JVM
- 【日記】感覺溝通是件很難精通的事情(1397 字)
- 前端進階之困前端
- hello world 是如何輸出到瀏覽器的瀏覽器
- Docker Hello World容器執行報錯的解決辦法Docker
- Go - Hello WorldGo
- Docker Hello WorldDocker
- 【Java】Hello worldJava
- React Hello,WorldReact
- Mockito Hello WorldMockito
- ant Hello World
- Deep "Hello world!"
- Go:Hello WorldGo
- 體驗 Java 9(1):從 Hello World 到 LombokJavaLombok
- 從 Form1.Caption = “Hello World”說起 (轉)ORMAPT
- Hello Python worldPython
- ABAP程式Hello World
- dotnet hello world
- RabbitMQ tutorial - "Hello world!"MQ
- 輸出hello world
- hello world"你知多少------300種程式語言中的"hello world"程式匯
- Flutter Web 之 Hello WorldFlutterWeb
- ROS之初見Hello WorldROS
- 【Flutter 基礎】Hello WorldFlutter
- JMicro微服務Hello World微服務
- 01-C++ "hello world"C++
- RabbitMQ 入門 - Hello WorldMQ
- [WebAssembly 入門] Hello, world!Web
- 機器學習,Hello World from Javascript!機器學習JavaScript
- Spring版Hello WorldSpring
- Play框架之Hello, World!框架
- C# Hello,World(1)