通過做一個基於Node的微伺服器來學習Docker

小謝發表於2016-07-04

如果你正準備著手學習 Docker,別再觀望,動起手來吧!

在這篇文章中,我將告訴你 Docker 是如何工作的?使用中會遇到什麼問題?如何通過 Docker 完成一個基本的開發任務——構建一個微伺服器。

我們將以一臺配有 Node.js 服務和 MySQL 後臺的伺服器為例,從在本地執行程式碼開始,完成一個執行著微服務和資料庫的容器。

通過做一個基於Node的微伺服器來學習Docker

什麼是 Docker ?

從本質上來說,Docker 是一種軟體,讓使用者建立映象檔案(就像虛擬機器中的模板),然後在容器中執行這個映象的例項。

Docker 維護著有著大量映象的儲存庫,名字叫 Docker Hub ,你可以將它作為嘗試映象的起始點,或者用來免費儲存你的映象。你可以安裝 Docker ,選擇你喜歡的映象,然後在容器中執行它的例項。

本文我們將介紹建立映象、從映象建立容器等一系列內容。

安裝 Docker

如果你想跟上本文的節奏,那麼你需要安裝 Docker 。

點選 docs.docker.com/engine/installation 這個連結,在上面檢視適合你的系統的安裝嚮導。

如果你是 Mac 或者 Windows 作業系統,那麼你需要使用虛擬機器。我在 Mac OS X 上使用 Parallels 安裝 Ubuntu 虛擬機器來應付大多數的開發任務。因為它支援快照功能,當你做實驗的時候,他可以方便的將破壞了的環境恢復回去。

試試看

輸入以下命令:

一段時間後,你將會看到如下提示:

試試如下的命令,然後退出容器:

這看起來沒什麼,但是其實在後臺發生了很多事情。

你看到的是在你的機器上執行著的 Ubuntu 的隔離容器環境裡的 bash shell。這個環境完全歸你所有——可以在上面安裝軟體,執行軟體,可以做任何你想做的事情。

下圖表明瞭剛剛發生了什麼(圖來自於《 理解 Docker 架構 》一文):

Docker Run Flow

1. 列出如下的 Docker 指令:

  • docker : 執行 docker 客戶端
  • run : 執行一個新的容器
  • -it :讓容器帶有“互動終端”的一個引數
  • ubuntu : 容器所依賴的基礎映象

2. 在主機(我們的機器)上執行的 docker 服務檢查本地是否有所請求的映象拷貝——這裡發現沒有。

3. docker 服務檢查公有儲存庫(the docker hub),看是否有可用的名為 ubuntu 的映象——這裡發現有。

4. docker 服務下載映象,將其儲存到本地快取裡(為了下一次直接使用)。

5. docker 服務基於 ubuntu 映象建立新的容器。

Try any of these:

試試下面這些命令:

我們沒準備使用 Haskell ,但是你可以看到,搭建一個環境是多麼容易。

構建自己的映象也很輕鬆,可以在這上面安裝應用程式或者服務,可以是資料庫,或者是其他你需要的。隨後就可以在任意安裝了 Docker 的機器上執行它們——要保證映象是相同的、可預測的方式在每臺機器上執行。我們可以將軟體及其執行所需的環境整體構建成程式碼,並且輕鬆部署。

讓我們以一個簡單微伺服器為例。

概述

我們將要用 Node.js 和 MySQL 建立一個讓我們管理郵件地址到電話號碼目錄的微服務。

開始

要完成本地開發,需要安裝MySQL,並且建立一個測試資料庫…

…搖頭。

建立本地資料庫,並且上面執行指令碼,這很容易,但是可能會帶來一些問題。很多不受控制的事情開始了。它可能工作,我們甚至可以通過提交進程式碼庫的 shell 指令碼來控制這些步驟,但是如果其他開發人員已經安裝了 MySQL 了呢?如果他們的資料庫已經使用了我們想要建立的名稱  ‘users’  了呢?

第一步:在 Docker 中建立一個資料庫測試伺服器

這是很好的 Docker 應用場景。我們可能不想在 Docker 裡執行生產環境資料庫(比如可能會使用 Amazon RDS),但是可以使用 Docker 容器建立一個乾淨的 MySQL 資料庫做開發——讓我們的開發及其保持乾淨,並且保證所有東西都在控制中,並且可重複使用。

執行下面的命令:

該命令啟動一個執行著的 MySQL 例項,通過 3306 埠訪問,root 密碼為 123 。

  1.  docker run 告訴引擎,使用者想要執行一個映象(在最後傳入的是映象,mysql:latest
  2.  –name db 將整個容器命名為 db 。
  3.  -d detach,在後臺執行容器。
  4.  -e MYSQL_ROOT_PASSWORD=123(或者是 –env)環境變數 – 引數告訴 docker 所提供的環境變數。這之後跟著的變數正是 MySQL 映象檢查且用來設定的預設 root 密碼。
  5.  -p 3306:3306(或者 --publish) 告訴引擎使用者想要將容器內的3306埠對映到外部的3306埠上。

最後一部分很重要——即使這是 MySQL 的預設埠,如果使用者不顯式告訴 docker 想要對映的埠,docker 就會阻塞該埠的訪問(因為容器預設是隔離的,直到使用者告訴 docker 想要訪問它們)。

該命令返回值是容器 id,這是容器的指標,使用者可以用它來停止容器,向容器傳送命令等等。讓我們看看正在執行的是哪些容器:

關鍵的資訊是容器 ID,映象和名稱。連線到這個映象看看裡面有什麼:

下面這麼做也很有意思:

1. docker exec -it db :告訴 docker 使用者想要在名為 db 的容器裡執行一個命令(我們也可以使用 id,或者 id 的前幾個字母)。 -it 確保使用者有互動型終端。

2. mysql -uroot -p123 :我們實際在容器裡作為程式執行的命令,這裡是 mysql 客戶端。

我們可以建立資料庫,表,使用者,其他你需要的等等。

打包測試資料庫

在容器內執行 MySQL 需要一些 Docker 技巧,但是讓我們先打住,看看服務。現在,使用指令碼建立一個 test-database 目錄來啟動資料庫,停止資料庫以及搭建測試資料:

啟動指令碼很簡單:

該指令碼在一個分離容器鍾執行資料庫映象(比如,在後臺執行),建立了一個使用者來訪問 users 資料庫,然後等待資料庫伺服器啟動,隨後執行 setup.sql 指令碼來設定初始資料。

setup.sql 的內容是:

stop.sh 指令碼會停止容器並且刪除容器(docker 預設會保留容器,這樣能夠快速重啟,本示例中並不需要這樣):

之後會進一步簡化這個過程,讓它更加順暢。在 repo 裡的 step1 分支裡檢視這一階段的程式碼。

第二步:用 Node.js 建立一個微服務

本文的主題是 Docker 的學習,因此並不會花太多篇幅講解 Node.js 的微服務。只是強調一些重點。

讓我們仔細看看這一部分。首先看看這個程式碼庫。最好將你的資料庫訪問封裝和抽象成一些類,允許模擬它來實現測試目的:

其實有很多種其他實現方式!但是我們可以像下面這樣建立 Repository 物件:

在 repository/repository.spec.js 檔案裡也有一系列的單元測試。得到 repo 後,就可以建立伺服器了。server/server.js 如下:

該模組暴露了一個 start 函式,可以像下面這樣使用:

注意到 server.js 中使用了 api/users/js 吧?程式碼如下:

這些檔案都有和原始碼匹配的單元測試。

我們還需要配置。與其使用特定的庫函式,不如使用一個簡單的檔案 – config/config.js :

我們可以按需進行配置。目前,大部分配置是硬編碼的,但是從埠的配置中可以看出,我們可以很容易的通過新增環境變數的方式來改變它。
最後一步 – 將它和包含所有東西的 index.js 檔案連線到一起:

我們做了一點錯誤處理,在此之上僅僅載入了配置,建立了 repo 並且啟動了伺服器。
這就是微服務,它讓使用者能夠得到所有使用者,或者搜尋某個使用者:

如果下載了相關程式碼,可以發現有一些可用的命令:

除了程式碼之外,我們完成了:
1. 用於除錯的 Node 皮膚
2. 用於單元測試的 Mocha/shoud/supertest
3. 用於 linting 的 ESlint

大功告成!
使用如下命令執行資料庫測試:

然後啟動服務:

可以用瀏覽器開啟 localhost:8123/users,就可以看到資料庫已經可以使用了。如果使用的是 Docker Machine(假設,在 Mac 或者 Windows 上),那麼 localhost 無法訪問,你需要使用 docker 的 IP。可以通過 docker-machine ip 得到 IP 地址。
可以看到,我們很快完成了服務的建立。繼續下一步之前如果想檢視程式碼,見 step2 分支。

第三步:微服務 Docker 化

現在開始變得有趣啦!

我們已經有了一個可以執行在開發環境裡的微服務,只要它和安裝的 Node.js 版本相容即可。這一步想做的是搭建起我們的服務,這樣可以從其中建立一個 Docker Image,從而可以將服務部署到任何支援 docker 的地方。

要達到這一目的,需要建立一個 Dockerfile。Dockerfile 告訴 Docker 引擎如何構建映象。我們會在 users-service 目錄下建立一個簡單的 Dockerfile,並且研究如何通過修改它來適應需求。

建立 Dockerfile

在 users-service/ 目錄下建立名為 Dockerfile 的文字檔案,內容如下:

執行如下命令列構建映象,並在映象上執行容器:

首先看看構建命令。
1. docker build 告訴引擎使用者需要建立一個新的映象
2. -t node4 使用標籤 node4 標記該映象。之後就可以使用這個標籤來指代該映象。
3. 在當前目錄裡查詢 Dockerfile.

控制檯打出一些輸出之後,就可以看到新的映象建立好了。使用 docker images 命令可以在系統裡看到所有映象。下面的命令和之前的很類似:
1. docker run 從某個映象裡執行新容器
2. -it 使用互動式終端
3. node4 是想要在容器裡使用的映象的標籤。

當執行該映象時,會得到 node repl,執行如下命令檢查當前版本:

這很可能和你當前機器上的 node 版本不同。

檢查 Dockerfile

從 Dockerfile 裡可以很容易看出發生了什麼:
1. FROM node:4 在 Dockerfile 裡指定的第一件事就是基礎映象。docker hub上的 Node 官方頁面可以搜尋列出所有可用映象。這裡用的是安裝了 node 的 ubuntu。
2. CMD ["node"] 裡的  CMD 告訴 docker 該映象需要執行 node 程式。當 node 程式終止時,容器會關閉。

使用額外的幾個命令,可以更新 Dockerfile,從而執行服務:

唯一的改變是使用了 ADD 命令將當前目錄下的所有東西拷貝到名為 app/ 的容器目錄裡。隨後使用 RUN 在映象裡執行命令,該命令安裝了模組。最後,EXPOSE 了伺服器埠,告訴 docker 想要支援 8123 埠的連線,然後執行伺服器程式碼。
確保 test-database 服務已經執行著,然後再次構建並且執行映象:

如果在瀏覽器裡檢視 localhost:8123/users,會看到一個錯誤,檢查控制檯,提示容器報告了一些問題:

我勒個去!從 users-service 容器到 test-database 容器的連線被拒絕了。執行 docker ps 檢視所有執行著的容器:

這兩個容器都執行著呢,到底怎麼回事呢?

連線容器

我們看到的問題實際上是可以預期的。Docker 容器應該是互相隔離的,因此如果不顯式地允許容器間連線的話就容器間就無法互聯。

是的,使用者可以從自己的機器(宿主機)連線到容器裡,因為我們為這樣的連線開啟了埠(比如,使用了-p 8123:8123)。如果以同樣的方式允許容器間互聯,那麼執行在同一臺機器上的兩個容器之間就應該能夠通訊,即使開發人員不想這麼做。並且這是災難性的,尤其是我們在叢集的機器上利用容器執行不同的應用程式的時候。

如果想要從某個容器連線到另一個容器,需要連線這兩個容器,告訴 docker,使用者顯式想要允許這兩個容器間通訊。有兩種方式可以完成這一目標,第一種是“不流行的舊方式”但是非常簡單,第二種之後會介紹。

使用 link 引數連線容器

當執行容器時,可以使用 link 引數告訴 docker 我們想要連線到另外的容器上。本文示例中,可以通過如下命令正確執行服務:

1. docker run -it 在容器裡執行 docker 映象,使用互動式終端。
2. -p 8123:8123 將宿主機的 8123 埠對映到容器的 8123 埠上
3. link db:db 連線到名為 db的 容器上,並且用 db 指代該容器
4. -e DATABASE_HOST=db 將環境變數 DATABASE_HOST 設定為 db
5. users-service 是在容器內執行的映象名稱

現在當我們訪問 localhost:8123/users 時一切工作正常。

它是如何工作的呢?

還記得服務的配置檔案麼?它讓使用者能夠使用環境變數指定資料庫的主機名:

執行容器時,將環境變數設定為 DB,這意味著要連線到一個名字為 DB 的主機上。當連線到容器上時,docker 引擎會自動為我們設定好一切。

嘗試執行 docker ps 列出所有執行著的容器。查詢執行 users-service 的容器名稱,這是個隨機名稱,例如 trusting_jang:

現在可以看到容器可用的主機:

還記得 docker exec 是怎麼工作的嗎?選擇一個容器名,之後跟著想在容器上執行的命令,在本例中是 cat /etc/hosts

好了,主機檔案之前可沒有 # linking magic!! 註釋,可以看到 -docker 將 db 新增到了主機檔案裡,因此可以通過主機名連線到容器上。這是連線資訊:

從該命令還可以看到當 docker 連線容器時,它還提供了一系列包含有用資訊的環境變數,比如,主機名,tcp 埠和容器名。

第3步完成了 —— MySQL 資料庫正常執行在容器裡,還可以在本地或者在容器裡執行 node.js 微服務,並且已經知道了如何連線這兩者。

如果你想了解更多,可以在 step3 的分支裡檢視這一階段的程式碼。

第4步:環境的整合測試

現在可以編寫整合測試,呼叫實際伺服器,作為 docker 容器執行,呼叫容器化的測試資料庫。

可以用任何語言,或者在任何平臺上完成整合測試,但是為了保持簡潔,這裡使用的是 Node.js,因為專案裡已經使用了Mocha 和 Supertest。

在名為 integration-tests 的新目錄下,建立一個 index.js:

它會檢查 API 呼叫,並且顯示測試結果。
只要 users-services 和 test-database 正在執行,測試就能夠通過。但是,這時候服務開始變得有點難處理:
1. 需要使用 shell 指令碼來啟動和停止資料庫
2. 需要記住一系列命令來基於資料庫啟動使用者服務
3. 需要使用 node 直接執行整合測試
既然我們已經很熟悉 Docker 了,應該能夠解決這些問題。

簡化 Test 資料庫

目前測試資料庫有如下檔案:

既然已經很熟悉 Docker 了,讓我們嘗試改進它們。

在 Docker Hub 上檢視 mysql 映象文件,有一處註釋告訴使用者任何新增到映象的 /docker-entrypoint-initdb.d 目錄的 .sql 或者 .sh 檔案會在搭建 DB 的時候執行。

這意味著可以使用 Dockerfile 代替 start.sh 和 stop.sh 

現在執行測試資料庫只需要:

組合

構建並且執行每個容器仍然有些費時。可以使用 Docker Compose 工具進一步簡化。

Docker Composer 允許使用者建立一個檔案,在其中定義系統裡的每個容器,容器間的關係,並且構建或者執行它們。
首先,安裝 Docker Compose。在專案根目錄下建立一個新檔案,稱為 docker-compose.yml:

現在就可以試一下啦:

Docker Compose 會構建出應用程式所需要的所有映象,從其上建立出容器,並且以正確順序執行容器,從而啟動整個應用程式!
docker-compose build 命令構建 docker-compose.yml 檔案裡列出的每個映象:

每個服務的 build 值告訴 docker 到哪裡找到 Dockerfile 。當使用者執行 docker-compose up 時,docker 會啟動所有服務。注意在 Dockerfile 裡,使用者可以指定埠和依賴關係。實際上,在這個檔案裡,使用者可以更改所有配置。
在另一個終端裡,執行 docker compose down,可以正常關閉容器。

總結

本文裡介紹了很多 docker 知識,不過這裡還有一些。我希望你能夠從本文找到一些有趣有用的東西,能夠幫助你在工作中使用 docker。

和平常一樣,歡迎提問和建議!同時強烈推薦文件《理解 Docker 》,可以幫助大家更深入地理解 docker 的工作機制。

github.com/dwmkerr/node-docker-microservice 處可以看到本文所構建的專案的最終原始碼。

注意

1. 將所有東西完全拷貝不是什麼好主意,因為那樣也會拷貝 node_modules 資料夾。通常來說顯式指定想要拷貝的檔案或資料夾會更好,或者使用 .dockerignore 檔案,它和 .gitignore 檔案類似。
2. 如果伺服器不正常工作,顯示一個很討厭的 exception,這是由一個bug造成的,詳見 github.com/visionmedia/supertest/issues/314

打賞支援我翻譯更多好文章,謝謝!

打賞譯者

打賞支援我翻譯更多好文章,謝謝!

任選一種支付方式

通過做一個基於Node的微伺服器來學習Docker 通過做一個基於Node的微伺服器來學習Docker

相關文章