Docker在Coding WebIDE項目中的運用
隨著云計算技術的日新月異,云端的代碼倉庫、分工協作、演示運行已經被人們廣為接受。云端開發的出現也正是順應了這一趨勢。Docker作為一個輕量級的隔離環境,無疑是云端開發解決資源和效率問題的秘藥良方。
記得4月份的杭州Docker Meetup有一參會者提問,“作為一個云主機的租戶,向主機商購買的計算資源,其獲得的配額不是真實值而只是上限,覺得不值。”這個問題似乎揭露了商家 的生意經,但是本人卻有不同的看法。正是因為共享技術的發展,才讓云計算資源變得廉潔而被廣為接受,Docker最大的價值也在這里。
從技術特性上看,VM和Docker有些重合點。但是虛擬機是基于Hypervisor技術的,而Docker是基于容器技術的。 Hypervisor要比Container更底層,不是同一層面的競爭關系,真實的場景多是先 Hypervisor再Container,通俗的說法就是在VM里跑Container。
Hypervisor技術讓多個操作系統共享一個CPU硬件,這些操作系統獨立運行,并不知道彼此的存在,仿佛獨占了所有的硬件資源。
Container技術讓多個用戶空間共享一個操作系統,這些用戶空間彼此隔絕,仿佛獨占整個操作系統。
我們都知道,文件是對I/O設備的抽象表示,虛擬存儲器是對主存和磁盤I/O設備的抽象表示,進程則是對處理器、主存和I/O設備的抽象表示。相比之下,虛擬化將操作系統從硬件中抽象出來,容器技術將應用從操作系統抽象出來。
一個正在執行的進程,由于虛擬內存技術,就其視角來看,仿佛擁有了整個操作系統的計算資源。但是Container的抽象和進程抽象不是在一個層 面的。簡單說,一臺物理設備可以借助于Hypervisor技術,運行多個VM;而個操作系統可以借助Container技術,運行多個 Container,Container里又可以有多個進程。
上面簡單的介紹了一些Docker技術的背景,言歸正傳。
為什么選用Docker而不是更成熟的VM
實現WebIDE首先解決的就是環境隔離,多個用戶之間不會相互干擾。物理機是相互隔離的,但是為每一個用戶分配一臺真實的物理機,顯然是不合現實的。
VM可以提供和物理機一樣的隔離效果,由于VM共享硬件,所以更省資源。一個可行的方案是借助IaaS平臺商提供的OpenAPI來操作VM。這樣對物理主機和宿主操作系統的維護工作可以完全委托給IaaS平臺商。
相比Docker Container,VM有一個很大的技術優勢是支持休眠。操作系統在系統級實現了休眠,這樣用戶的工作狀態,內存中的數據可以完整的持久化。作為一個常 年不關機的開發者,個人覺得這個功能非常實用。可惜Docker只提供了睡眠(類似于進程級別的掛起),而做不到休眠。隨著 CRIU 技術的發展,相信Docker很快會支持的。
另外VM在不同宿主機之間的遷移問題,經過多年社區的積累越來越成熟。如果選擇向IaaS平臺商購買VM服務,這部分工作也不用關心。 Docker Container數據的遷移,面臨著自制。目前Docker官方提供遷移Container(非image)的命令,只能遷移文件,無法保留狀態(比如 外部mount的目錄)。
考慮到架構的微服務化,如文件服務、Git 服務、Terminal 服務、Runtime服務。有些服務是單例的,另一些則會隨著用戶會話狀態而動態地創建和銷毀。當應用實例很多的時候,虛擬化技術的Overhead是需 要考慮的因素。為了某個服務而啟動整個操作系統有些負擔不起。除了過度的內存消耗,啟動耗時也存在差異,Container只是用戶空間的一個或者一組進 程,所以啟動耗時基本是毫秒級別,而VM至少是秒級,有的甚至是分鐘級(休眠還原的時候)。
做比較的時候總是各有優劣,但最終打動我們的除了Docker的輕量,還有其生機勃勃。我們相信備受社區關注的技術,許多顧慮的問題終究會有解決方案的。
基于Container的Web Terminal
一個完整的IDE需要具備很多功能,比如文件管理、版本管理、編輯器、編譯器、執行環境等等。初次上線的最小功能集合里,我們認為Web IDE區別于Web Editor的一個功能亮點就是Web Terminal。
Web Terminal和SSH的工作原理類似,通過架設在TCP之上的應用層協議實現對主機的遠程控制。相信大多數開發者都有SSH的使用經驗,理解其工作原 理的僅占少數。開始研究之初,我們也和大多數人一樣搞不清楚terminal、tty、pty、shell、bash之間的區別,所以先來理理概念。
什么是Terminal?
從用戶的角度來看,Terminal是鍵盤和顯示器的組合,也稱為TTY(電傳打字機的縮寫)。鍵盤輸入字符,顯示器顯示字符。從進程的角度來看,終端是字符設備,可以通過read、write、ioctl等系統調用來讀寫和控制該設備。
TTY早已進入了博物館,桌面系統上字符界面基本被GUI界面替代。取而代之是一個稱之為Terminal Emulator(終端模擬器)的窗口程序,該程序顯示的字符界面就是曾經物理顯示器里的完整內容。
Terminal作為真實的物理設備已經不復存在了,但是為了和面向終端的程序(比如Bash)進行通信,于是就了發明了 pty(Pseudoterminal,偽終端)。pty是一對master-slave設備,master設備表現得像一個文件,slave設備表現得 像一個終端設備,當Terminal Emulator作為一個非面向終端的程序不直接與pty slave通訊,而是通過文件讀寫流與pty master通訊,pty master 再將字符輸入經過線路規程的轉換傳送給slave,slave進一步傳遞給bash。
Bash是一個命令行的解釋器,通常也是進程會話的主進程,其職責是解釋執行終端設備(或者偽終端的slave設備)傳遞過來的字符串和控制字符,執行命令。
Web Terminal 的工作原理
理解了上面背景知識之后,再看SSH的原理圖。
SSH是一個典型的server-client模式架構,用戶通過終端將字符流傳遞給SSH client。SSH client和SSH server之間通過TCP/IP協議進行通訊。遠端的server創建一對pty,并且fork+exec一個bash進程,server進程通過 pty對與bash進行交互。
仿照SSH的工作原理,我們在HTTP協議之上設計了Web Terminal,見下圖:
真實實現中,Socket.io是應用層的通訊協議。Terminal Emulator是一個純JS的實現,Node.js后端使用 pty.js 模塊來創建pty對。
當解決了Web Terminal的整體架構以后,嵌入Docker Container已是水到渠成。
僵尸進程問題
我們知道Docker由于缺少init 0而導致僵尸進程無法回收的問題迄今存在。Terminal作為控制終端,會在使用過程中執行若干命令,這些命令對應進程如果與其父進程脫離父子關系,那僵尸進程問題就來了。
Docker官方推薦的一個Container只跑一個進程。如果Container 與進程同生共死,僵尸進程的問題基本不會遇到。但是Web Terminal所在Container里啟動了bash,而bash可以隨意執行命令啟動進程,僵尸進程問題很難避免。好在社區提供了更好的解決方案: phusion/baseimage 。在Dockerfile里將的 FROM ubuntu
改為 FROM phusion/baseimage
,再按照文檔說明做些調整基本就好了。
Container 作為構建和管理工具
通常,我們都是把App部署到Docker里去。大致步驟就是編寫Dockerfile,再構建成image,然后借助private registry在分布式的集群中分發。由于開發環境、測試環境和生產環境存在差異,往往構建交付物涉及到大量參數和環境變量的設定,過程非常繁瑣,一般 都會腳本化。所以IDE項目基本都是Dockerfile旁邊放置了一個Gemfile和Rakefile。通過Ruby Rake來驅動整個構建過程。
作為腳本語言與Shell相比,Ruby的好處是:
- 隔絕了Darwin,Linux平臺之間某些命令的細微差異;
- 對于Shell擅長的部分,可以通過'`'符號方便的嵌入調用;
- 具備完備正則等字符串處理功能;
- 方便調用Docker api的;
- 可以集成Capistrano等分布式管理工具; </ul>
但Ruby不像Shell那樣信手拈來,需要進行適當的配置,比如,RVM安裝指定版本,修改gem source之類的。
從前配置這些基礎環境,都是記錄成Markdown文檔,一堆 apt-get、sed指令。但是引入Docker以后,有更好的選擇。
我們的方式如下:
編寫一個配置構建環境的Dockerfile,構建成image。
docker build --rm -t="ide-docker-registry.coding.local/ide-builder:0.0.5" .
push到registry里。
docker push ide-docker-registry.coding.local/ide-builder
在構建服務器創建構建所需的builder,通過mount外部目錄的方式,構建環境和外部環境交互文件。
docker run --name coding_ide_builder -d -t -v $CODING_IDE_HOME:/data/coding-ide-home --net=host --restart=always ide-docker-registry.coding.local/ide-builder
進入構建環境執行命令。
docker exec -i -t coding_ide_builder bash
或者直接構建。
docker exec -i -t coding_ide_builder rake
Container環境的資源限制問題
資源限制主要針對CPU、內存、磁盤和網絡帶寬等共享資源的限制。一方面,我們提倡共享,事實上不是所有的用戶都需要長時間的占滿所需的資源配 額,不需要的時候可以釋放出來分享給其他用戶,因為共享才會更便宜。另一方面,也需要對可共享資源設定一個最大的限制配額,以防止某些用戶過度占用而影響 其他用戶的使用體驗。
CPU限制
Docker提供了兩個參數來控制CPU的分配策略, --cpuset
和 --cpu-shares
。
--cpuset="0" [...]
將Container限定于某幾個CPU核心上。針對這一特性,我們制定的策略是將重要的Container服務分配在獨立的核心上,以保證服務的質量。
--cpu-shares
可以調節Container獲得的時間片。我們通過這個配置來調節Web Terminal所創建進程對CPU的占用率。
內存限制
Web Terminal里用戶的自由度是很大的,對內存限制可以減少惡意破壞。Docker配置內存限制相對簡單。另外,我們禁用了swap分區,以減少對磁盤的壓力。
磁盤限制
由于用戶可以完全自由的訪問磁盤,我們最希望Container 磁盤鏡像文件具備thin provisioning特性,不需要預分配所有空間也可以限定其大小。
對于Container的磁盤限制分為兩部分,對最上層可寫layer的限制和對被mount的可寫目錄的限制。
限制可寫layer
Docker Daemon提供了四種storage-driver: aufs
、 devicemapper
、 btrfs
、 overlay
。如果Linux發行版本支持aufs,那它就是默認的storage-driver,反之則是devicemapper。aufs最早被Docker支 持,而且支持共享二級制文件和動態庫文件所占用的內存,btrfs和overlay不支持此特性,但是比aufs速度更快。devicemapper 的特點是支持thin provisioning和copy on write。
限制layer的大小,devicemapper是目前唯一的選擇。啟動devicemapper后,Docker會為所有的Container 創建一個共享存儲池,其實質上是一個大文件,另外也會限定每個Container的大小。這兩個數字的制定需要慎重,因為考慮到數據遷移,修改很不容易。
限制被mount的可寫目錄
Docker run的時候mount進Container的可寫目錄是不受devicemapper的限制,所以需要額外處理。WebIDE場景中workspace目錄是被多個Container實例中共享讀寫的,作為用戶工作目錄,需要設定一個最大的空間限制。
談到Linux磁盤空間限制,最先想到quota,它常用于ftp服務中限定用戶最大可用空間。但quota有一個技術限制,僅僅適用于整個文件系統而無法針對單個目錄。所以quota方案在共享目錄的場景不可行。
Linux支持將一個磁盤鏡像文件mount成目錄,磁盤鏡像文件可以限定大小。當鏡像文件撐滿的時候,目錄就不可寫了。這是我們目前找到最靠譜的方案。
限制網絡帶寬
Docker沒有直接提供限制網絡帶寬的命令行參數,但借助Docker 的底層技術Cgroup可以實現。創建一個 Network classifier group,對 cgroup 進行帶寬限制的設定,將 Container 都指定到該組里去。Traffic Controller(tc) 和 Netfilter(iptables) 都支持針對 cgroup 指定規則。
關于Dockerize的程度與思考
基于Docker更容易實現架構的微服務化。借助于Docker的link特性和Fig工具,Container可以像樂高積木一樣把所有的組件都組合起來。Nginx、Jetty、MySQL、Redis等一系列服務都可以封裝到獨立的Container中去。
全面Dockerize的最大好處是整個體系都是一致的,所有的組件都是Container。WebIDE在架構初期,考慮全面 Dockerize的方案,比如把MySQL分成兩個Container,一個存放安裝文件,另一個存放數據文件。應用服務器自不必說也在 Container里。但是當考慮Nginx是否也要放進Container里,大家想法有些分歧,Container的價值在Nginx上是否明顯值得 探討。也正因為存在不同的聲音,我們放棄了全面Dockerize。個人的覺得已有的經驗和腳本不應該放棄,我們應該節省出更多的精力來做更重要和緊迫的 事情。
作者簡介
杜萬 , Coding.net 全棧工程師,目前負責Coding WebIDE項目的架構和研發。從事了近10年以Java語言為主的軟件開發工作,熱衷于整合框架和開發工具,關注交互設計,喜歡寫 技術博客 ,Linux擁躉。近期開始學習和關注 Elixir 函數語言。
參考閱讀
- Hypervisor - Wikipedia
- Operating-system-level virtualization - Wikipedia
- User space - Wikipedia
- Basics – Docker, Containers, Hypervisors, CoreOS
- devicemapper - a storage backend based on Device Mapper
- Resizing Docker Containers with the Device Mapper plugin
- Network classifier cgroup