搞事情之初識 Docker 與嘗試構建 Swift

PJHubs發表於2019-03-03

搞事情繫列文章主要是為了繼續延續自己的 “T” 字形戰略所做,同時也代表著畢設相關內容的學習總結。本文章是 Docker 部分的第一篇,主要是給自己解釋與 Docker 有關的內容。

原文地址:PJ 的 iOS 開發日常

虛擬化和容器化技術

虛擬化技術

虛擬化技術是一種將計算機物理資源進行抽象、轉換為虛擬的計算機資源提供給程式使用的技術。這些資源包括了 CPU 提供的運算控制資源,硬碟提供的資料儲存資源,網路卡提供的網路傳輸資源等。

跨平臺

保證程式跨平臺相容,也就是要保證作業系統或物理硬體所提供的介面呼叫方式一致,程式便不需要相容不同硬體平臺的介面。此時突然想到,使用 Swift 編寫 iOS app 時,構建出包後總是會帶上 Swift 的整個執行時,以保證隨著 iOS 系統版本的升級 app 的正常執行,因其 ABI 並未穩定,還不能內建在作業系統中。

資源管理

可將虛擬化技術運用於計算機資源的管理,其中最實用的就是“虛擬記憶體”虛擬化技術能夠提高計算機資源的使用率,是指利用虛擬化,可以將原來程式用不到的一些資源拿出來,分享給另外一些程式,讓計算機資源不被浪費。

虛擬化技術的分類

主要分為兩大類:硬體虛擬化軟體虛擬化

  • 硬體虛擬化:比如假設 iOS 基於的 arm 架構 CPU 能夠執行基於 x86 架構的 macOS 應用程式,這是因為 CPU 能夠將另外一個平臺的指令集轉換為自身的指令集執行(但實際上並不可能)。

  • 軟體虛擬化:在 2018 WWDC 中,宣佈可以在 UIKit 層面提供一部分把 iOS app 轉移到 macOS app 中的特性,可以理解為是 Apple 在 Xcode 層面協助開發者構建了遷移程式碼,幫開發者解決了不同平臺指令的轉換。也就是說,軟體虛擬化實際上是通過一層夾雜在應用程式和硬體平臺上的虛擬化實現軟體來進行指令的轉換。

其它虛擬化技術的分類:

  • 平臺虛擬化:在作業系統和硬體平臺間搭建虛擬化設施,使得整個作業系統都執行在虛擬後的環境中。類似 VMwarePD
  • 應用程式虛擬化:在作業系統和應用程式間實現虛擬化,只讓應用程式執行在虛擬化環境中。類似 Python 的虛擬環境;
  • 記憶體虛擬化:將不相鄰的記憶體區,甚至硬碟空間虛擬成統一連續的記憶體地址,即虛擬記憶體;
  • 桌面虛擬化:讓本地桌面程式利用遠端計算機資源執行,達到控制遠端計算機的目的。類似華為雲的雲桌面以及各種遠端桌面控制軟體,如 Teamviewer。
  • ......

虛擬機器

虛擬機器通常說法是通過一個虛擬機器監視器( Virtual Machine Monitor ) 的設施來隔離作業系統與硬體或應用程式和作業系統,以達到虛擬化的目的。這個虛擬機器監視器,通常被稱為:Hypervisor

虛擬機器有一個永遠都逃不掉的問題:效能低下。這種效率的低下有時候是無法容忍的,故真實的虛擬機器程式常常不完全遵守 Hypervisor 的設計結構,而是引入一些其它技術來解決效率低下問題,比如解釋執行、即時編譯(Just In Time)執行機制,但這些技術的引入已不屬於虛擬化的範疇了。

容器技術

按分類或者實現方式來說,容器技術應該屬於作業系統虛擬化,也就是在由作業系統提供虛擬化的支援。總的來說,容器技術指的是作業系統自身支援一些介面,能夠讓應用程式間可以互不干擾的獨立執行,並能夠對其在執行中所使用的資源進行干預。

那這也不應該被稱為“容器”呀?是的,這裡所謂的容器指的是由於應用程式的執行被隔離在了一個獨立的執行環境之中,這個獨立的執行環境就好似一個容器,包裹了應用程式。

容器這麼火爆,火到一心撲在 iOS 上的我都要好好梳理一番,很重要的一個原因是其在執行效能上遠超虛擬機器等其它虛擬化實現,甚至在執行效率上與真實執行在物理平臺的應用程式不相上下。但注意,容器技術並沒有進行指令轉換,執行愛容器中的應用程式自身必須支援在真實作業系統上執行,也就是必須遵守硬體平臺的指令規則。

曾經看到一篇文章說 linux 核心名稱空間的改進,直接推動了容器的最大化發展。

利用核心名稱空間,從程式 ID 到網路名稱,一切都可在 Linux 核心中實現虛擬化。新增的使用者名稱空間“使得使用者和組 ID 可以按名稱空間進行對映。對於容器而言,這意味著使用者和組可以在容器內部擁有執行某些操作的特權,而在容器外部則沒有這種特權。”Linux 容器專案 (LXC) 還新增了使用者亟需的一些工具、模板、庫和語言繫結,從而推動了進步,改善了使用容器的使用者體驗。LXC 使得使用者能夠通過簡單的命令列介面輕鬆地啟動容器。(來源 redhat 官網)

容器由於沒有虛擬作業系統和虛擬機器監視器這兩個層次,大幅減少了應用程式帶來的額外消耗。所以在容器中的應用程式其實完全執行在了宿主作業系統中,與其它真實執行在其中的應用程式在指令執行層面是完全沒有任何區別的。

Docker 的核心組成

四大組成物件

映象

可以理解為一個只讀的檔案包,其中包含了虛擬環境執行的最原始檔案系統的內容。

因為 Docker 採用 AUFS 作為底層檔案系統的實現,實現了一種增量式的映象結構。每次對映象內容修改,Docker 都會將這些修改鑄造成一個映象層,而一個映象本質上是由其下層所有的映象層所組成的,而每一個映象層單獨拿出來,都可以與它之下的映象層組成一個映象。正是由於這種結構,Docker 的映象本質上是無法被修改的,因為所以的映象修改只會產生新的映象,而不是更新原有的映象。

容器

在容器技術中,容器是用來隔離虛擬環境的基礎設施,但在 Docker 中,被引申為隔離出來的虛擬環境。如果我們把映象理解為類,則容器為例項物件。映象記憶體放的是不可變化的東西,當以他們為基礎的容器啟動後,容器內也就成為類一個“活”的空間。

Docker 的容器應該有三項內容組成:

  • 一個 Docker 映象;
  • 一個程式執行環境;
  • 一個指令集合。

網路

Docker 中可對每個容器進行單獨的網路配置,也可對各個容器間建立虛擬網路,將數個容器包裹其中,同時與其它網路環境隔離,並且 Docker 還能在容器中構造獨立的 DNS,我們可以在不修改程式碼和配置的前提下直接遷移容器。

資料卷

在以往的虛擬機器中,大部分情況下都直接使用虛擬機器的檔案系統作為應用資料等檔案的儲存位置,但並未是完全安全的,當虛擬機器或容器出現問題導致檔案系統無法使用時,雖可直接通過快速的映象進行重製檔案系統以至於恢復,但資料也就丟失了。

為保證資料的獨立性,通常會單獨掛在一個檔案系統來存放資料,得意與 Docker 底層的 Union File System 技術,我們可以不用管類似於搞定掛載在不同宿主機中實現的方法、考慮掛載檔案系統相容性、虛擬機器作業系統配置等問題。

映象與容器

Docker 映象

所有的 Docker 映象都是按照 Docker 所設定的邏輯打包的,也是收到 Docker Engine 所控制。常見的虛擬機器映象都是由其它使用者通過各自熟悉的方式打包成映象檔案,公佈到網上再被其它使用者所下載後,恢復到虛擬機器中的檔案系統中,但 Docker 的映象必須通過 Docker 來打包,也必須通過 Docker 下載或匯入後使用,不能單獨直接恢復成容器中的檔案系統。這樣,我們就可以直接在伺服器之間傳遞 Docker 映象,並配合 Docker 自身對映象的管理功能,使得在不同的機器中傳遞和共享變得非常方便。

每一個記錄檔案系統修改的映象層 Docker 都會根據它們的資訊生產一個64位的 hash 碼,正是因為這個編碼,可以能夠區分不同的映象層並保證內容和編碼是一致的,我們可以在映象之間共享映象層。當 A 映象依賴了 C 映象,且 B 映象也依賴了 C 映象,在實際使用過程中,AB 兩個映象是可以公用 C 映象內部的映象層的。

檢視映象

$ docker images
複製程式碼

映象命名

可以分為三部分:

  • username:一般都是映象創作者,但如果不寫則是由官方進行維護。
  • repository:一般都是該映象中所包含的軟體名。但映象名歸映象名,映象歸映象,Docker 對容器的設計和定義是微型容器而不是龐大臃腫的完整環境,所有通常只會在一個容器中執行一個應用程式,能夠大幅降低程式之間互相的影響,利用容器技術控制每個程式所使用的資源。
  • tag

主程式

Docker 的設計中,容器的生命週期與容器中 PID 為 1 這個程式由密切的關係,容器的啟動本質上可以理解為這個程式的啟動,而容器的停止也就意味著這個程式的停止。

寫時複製

通過映象執行容器時並不是立即把映象裡所有內容拷貝到容器所執行的沙盒檔案系統中,而是利用 UnionFS 將映象以只讀方式掛載到沙盒檔案系統中,只有在容器對檔案的修改時,修改才會體現到沙盒環境上。

從映象倉庫獲得映象

獲取映象

docker pull ubuntu
複製程式碼

獲取映象更詳細的資訊

docker inspect ubuntu
複製程式碼

搜尋映象

docker search django
複製程式碼

刪除映象

docker rmi ubuntu
複製程式碼

執行和管理容器

容器的生命週期

  • Created
  • Running
  • Paused
  • Stopped:容器的停止狀態下,佔用的資源和沙盒環境都存在,只是容器中的應用程式均已停止
  • Deleted

建立容器

$ docker create ubuntu
複製程式碼

如果我們之前選擇的 docker pull 容器並不是預設的 latest 版本,而是手動選擇了一個版本,那映象的名字將會比如 nginx:1.12,對於後續的操作都十分的不方便,對此,我們可以採用 --name 進行重新命名:

$ docker create --name nginx nginx:1.12
複製程式碼

啟動容器

$ docker start ubuntu
複製程式碼

通過 docker run 可將上述兩個命令進行合併:

$ docker run --name nginx nginx:1.12
複製程式碼

以上命令跑起來的容器執行都是執行在前臺,如果我們想要容器執行在後臺,可以通過 -d,其是 -detach 的簡稱,告訴 Docker 在啟動後將程式和控制進行分離。:

$ docker run -d ubuntu 
複製程式碼

管理容器

列出執行中的所有容器

$ docker ps 
複製程式碼

列出所有容器

$ docker ps -a/-all
複製程式碼

其中列印出的列表需要注意的是 STATUS 欄位,常見的狀態表示有三種:

  • Create:容器已建立,沒有啟動過;
  • Up[ Time ]:容器正在執行,[ Time ] 代表從開始執行到檢視時的時間;
  • Exited([ Code ]) [ Time ]:容器已結束執行,[ Code ] 表示容器結束執行時,主程式返回的程式退出碼,而 [ Time ] 則表示容器結束到檢視時的時間。

停止和刪除容器

$ docker stop ubuntu
複製程式碼

容器停止後,其維持的檔案系統沙盒環境會一直儲存,內部被修改的內容也會被保留。通過 docker start 將容器繼續啟動。

當需要把容器完全刪除容器,可以使用:

$ docker rm ubuntu
複製程式碼

但在執行中的容器預設情況下是不能被刪除的,但我們可以通過以下命令進行刪除:

$ docker rm -f ubuntu
複製程式碼

隨時刪除容器

Docker 與其它虛擬機器不同,其所定位的輕量級設計講究隨用隨開,隨關隨刪,當我們短時間內不需要使用容器時,最佳的做法是刪除它而不是僅僅停止它。

如果我們要對程式做一些環境配置,完全可以直接將這些配置打包至一個新的映象中,下次直接使用該映象建立容器即可。對於一些重要的檔案資料,不能隨著容器的刪除而刪除,可以使用 Docker 中的資料卷來單獨存放。

進入容器

直接建立,進入

$ docker run -it --name ubuntu ubuntu 
複製程式碼

已經建立完成,進入

$ docker exec -it ubuntu /bin/bash
複製程式碼
  • -i 表示保持我們的輸入流;
  • -t 表示啟用一個偽終端,形成我們與 bash 的互動。

當容器執行在後臺,想要在將當前的輸入輸出流連線到指定的容器上,可以這麼做:

$ docker attach ubuntu
複製程式碼

通過 docker attach 啟動的容器,可以理解為與 docker run -d 做了相反的事情,把當前容器從後臺拉回了前臺。

為容器配置網路

容器網路

Docker 網路中,有三個比較核心的概念,形成了 Docker 的網路核心模型,即容器網路模型(Container Network Model)

  • 沙盒:提供容器的虛擬網路棧。比如埠套接、IP 路由表、防火牆等;
  • 網路:Docker 內部的虛擬子網,網路內的參與者相互可見並能夠進行通訊。需要注意的是,這種虛擬網路與宿主機存在隔離關係。
  • 端點:主要目的是形成一個可以控制的突破封閉網路環境的出入口,當容器的端點與網路的端點形成配對後,就如同在這兩者之間搭建了橋樑,可進行資料傳輸。

Docker 的網路實現

目前官方提供了五種網路驅動:

  • Bridge Driver(default):通過基於硬體或軟體的網橋來實現通訊
  • Host Driver
  • Overlay Driver:藉助 Docker 的叢集模組 Docker Swarm 來搭建的跨 Docker Daemon 網路,可以通過它搭建跨物理主機的虛擬網路,從而讓不同物理機中執行的容器感知不到多個物理機的存在。
  • MacLan Driver
  • None Driver

網路剩餘內容將在下篇文章中繼續進行......

搞點事情

學習到這裡後,開始對 Docker 所謂“輕量級”的主打理念有了一個初步的認識,準備利用 Docker 的這一特性做一個 Swift 編譯服務,主要想利用 Vapor/Perfect (這兩個到底選哪一個還需調研)來搭建 HTTP 服務,接收傳入的程式碼文字,執行並返回結果。

思考了一下,需要:

  • 具備編譯 Swift 能力的 Docker 映象;
  • 具備 Vapor/Perfect 框架的 Docker 映象;
  • 具備 Nginx web 伺服器的 Docker 映象;

這一套下來後,將重新釋出一個“開箱即用”的提供 Swift 編譯服務的 Docker 映象~想想就是個非常美好的事情呢!接下來開始第一步

構建具備編譯 Swift 能力的 Docker 映象

之前有看到的文章說直接可以在 Ubuntu 上構建自己的 Swift 版本,所以我的第一步先去找一個 Ubuntu 映象,這點非常容易:

$ docker pull ubuntu
$ docker run -it --name ubuntu ubuntu /bin/bash
複製程式碼

成功進入到 bash 後,繼續下一步。找到一個萬能命令,根據這個命令可以先把編譯 Swift 需要的相關依賴都下載完成:

sudo apt-get install git cmake ninja-build clang python uuid-dev libicu-dev icu-devtools libbsd-dev libedit-dev libxml2-dev libsqlite3-dev swig libpython-dev libncurses5-dev pkg-config
複製程式碼

接著,下載 Swift 原始碼:

git clone https://github.com/apple/swift.git
複製程式碼

再下載專案依賴的其它原始碼:

./utils/update-checkout --clone
複製程式碼

完成後,即可開始利用原始碼中的工具進行編譯和測試!

utils/build-script -t
複製程式碼

此處將會經歷漫長的等待。二十分鐘後,我得到了兩個報錯:

clang: error: unable to execute command: Killed
clang: error: linker command failed due to signal (use -v to see invocation)
ninja: build stopped: subcommand failed.
utils/build-script: fatal error: command terminated with a non-zero exit status 1, aborting
複製程式碼
clang: error: unable to execute command: Killed
clang: error: linker command failed due to signal (use -v to see invocation)
[1747/3019] Linking CXX shared library lib/libLTO.so.7svn
FAILED: lib/libLTO.so.7svn 
複製程式碼

看提示是一些依賴庫出了問題,剛開始以為更新下對應的依賴庫就完事了,沒想到在網上居然找到不對應的報錯提示!這對於第一次手動編譯 Swift 的玩家來說十分的不友好,折騰了一會兒後放棄!

此時,又看到一篇文章有說可以直接利用 Swift 官網已經構建完成的二進位制檔案進行使用,地址在此 swift.org/download/ ,在 Docker 中可以通過 wget 進行下載。但因未找到 Swift 4.2.1 的正確下載地址,並且也擔心直接修改以往版本下載地址進行猜測地址不對,在宿主機上下載完成後,通過 docker cp /path dockerContainer:/path 命令把資料夾傳遞到了容器中。

在新增 PATH 我又遇到了如下錯誤:

swift: error while loading shared libraries: libatomic.so.1: cannot open shared object file: No such file or directory
複製程式碼
swift: error while loading shared libraries: libedit.so.2: cannot open shared object file: No such file or directory
複製程式碼

幾乎已經把 SO 上所有的解決方案進行了嘗試,皆無果,有 issue 說估計是 Docker 本身的問題,折騰了好一會兒,遂放棄。

當時以為這已經是最後一種方案,所以折騰了特別久,沒想到其實 Apple 官方居然維護了一套 swift-docker,開箱即用,特別香!!!

$ docker pull swift
$ docker run --privileged -i -t --name swiftfun swift:latest /bin/bash
複製程式碼

至此,第一步已經完成!這回都省去了自己構建映象的工作了~

Swift-Docker.png

相關文章