一種新的進入容器的方式: WebSocket + Docker Remote API
眾所周知,容器是基于操作系統(tǒng)內(nèi)核的一種輕量級的虛擬化技術(shù)。其可以類比于虛擬機,但其本身并不是虛擬機。在傳統(tǒng)的虛擬機使用場景中,每個用戶都會通過堡壘機,根據(jù)自己被分配的權(quán)限,登錄某些機器的某些賬號。當應用部署逐漸轉(zhuǎn)移到基于容器技術(shù)的PaaS平臺上后,讓用戶進入容器進行觀察、調(diào)試應用已經(jīng)成為了PaaS平臺的一個重要且必備的功能。
遠程進入容器功能的傳統(tǒng)實現(xiàn)方式是基于虛擬機的思想,在每個容器中啟動一個sshd進程。由于容器PID為1的進程的特殊性,為了保證容器不停,容器的ENTRYPOINT需要設(shè)置為類似于Supervisord這樣的進程管理程序。在這種多進程容器的使用場景中,用戶通過ssh-client指定容器的IP遠程連接到容器,讓用戶感覺到自己好像就在使用虛擬機。但是,這種方案會帶來以下問題:
- 權(quán)限管理。如何控制哪些用戶能夠登錄哪些容器?如何和平臺已有的權(quán)限管理系統(tǒng)集成?這種情況往往都需要通過堡壘機系統(tǒng)控制。而在PaaS中,引入單獨的堡壘機系統(tǒng)會增加PaaS的復雜度以及維護成本。
- 登錄方式選擇。無論使用密碼還是私鑰驗證登錄,容器內(nèi)的密碼或者authorized_keys的管理都需要通過加入額外的程序解決,無疑會增加容器的復雜度。同時還要面對同權(quán)限容器的密碼或authorized_keys的一致性問題。
基于以上問題,在我們的LAIN平臺中,設(shè)計出了基于WebSocket協(xié)議與Docker Remote API的遠程登錄方案。LAIN(https://laincloud.com)是一個基于Docker的PaaS。其面向技術(shù)棧多樣尋求高效運維方案的高速發(fā)展中的組織,DevOps人力缺乏的startup以及個人開發(fā)者。LAIN通過統(tǒng)一高效的開發(fā)工作流,降低應用運維復雜度;在IaaS / 私有IDC裸機的基礎(chǔ)上直接提供應用開發(fā),集成,部署,運維的一攬子解決方案。
該方案的整體架構(gòu)圖如下:
從圖中可以看出,在LAIN中實現(xiàn)容器遠程登錄支持需要以下兩個組件:
1. Entry應用。負責如下工作:
- 調(diào)用Docker Remote API
- 通過WebSocket 傳遞stdin,stdout和stderr。
- 根據(jù)protobuf3協(xié)議對各類消息進行序列化與反序列化。
- 對用戶登錄的鑒權(quán)。
- Entry是基于Go語言開發(fā)的,并依賴如下代碼庫:
- github.com/gorilla/websocket:WebSocket的服務端實現(xiàn)。
- github.com/fsouza/go-dockerclient:Go語言的Docker客戶端。
- github.com/golang/protobuf/proto:protobuf協(xié)議的支持庫。
2. 基于命令行的客戶端。負責如下工作:
- WebSocket連接請求的發(fā)送。
- 監(jiān)聽鍵盤輸入、窗口變化事件以及WebSocket返回的stream。
- 將遠端的stdout,stderr輸出到本地終端的標準輸出和標準錯誤。
Entry的工作流程
通過命令行客戶端遠程登錄容器的過程及其實現(xiàn)如下:
- 用戶通過客戶端命令向Entry應用發(fā)送WebSocket連接請求。
- Entry應用接收到用戶請求,得到請求Header中的access_token以及要進入的容器信息,通過調(diào)用LAIN的console接口判斷該用戶是否有權(quán)限進入容器。如果沒有權(quán)限,則直接通知客戶端鑒權(quán)失敗,本次連接結(jié)束。
- 如果通過了權(quán)限驗證,則WebSocket連接會被建立。緊接著Entry會去調(diào)用 execCreate 這個Docker Remote API。在調(diào)用時,需要指定Tty,AttachStdin、AttachStdout和AttachStderr參數(shù)均為true,Cmd參數(shù)為bash,這樣才能獲得bash進程的標準輸入輸出和錯誤。
- 如果調(diào)用execCreate成功,調(diào)用請求會返回該Exec的ID,Entry會繼續(xù)根據(jù)這個ID調(diào)用execStart接口。在調(diào)用時,需要指定Detach和Tty為false,這樣才能連接到bash進程的標準輸 入輸出和錯誤。調(diào)用execCreate成功后,會返回一個HTTP的stream。在Entry中則通過3個goroutine分別處理stdin,stdout和stderr。
- 客戶端會同時監(jiān)聽WebSocket連接與鍵盤輸入,對于WebSocket返回的Message,客戶端會通過Entry制定的protobuf3消息格式反序列化出消息結(jié)構(gòu),并根據(jù)消息的類型,將數(shù)據(jù)發(fā)送到本地終端的stdout或stderr。對于鍵盤輸入,客戶端會將輸入內(nèi)容封裝,經(jīng)過protobuf3序列化后,通過WebSocket發(fā)送給Entry應用,Entry應用經(jīng)過反序列化后,將輸入發(fā)送給bash的stdin。
以上就是Entry的工作原理。從中我們可以看出,Entry已經(jīng)很好地解決了傳統(tǒng)ssh-client登錄所遇到的問題:
- Entry通過調(diào)用console的接口完成了身份驗證工作,由于所有的權(quán)限都被console統(tǒng)一管理,因此Entry不需要自己維護權(quán)限信息,即Entry本身是無狀態(tài)應用。這種應用的優(yōu)勢在于可以低成本擴容,用以應對多并發(fā)的場景。
- Entry通過Docker Remote API連接容器,這樣只要被連接的容器內(nèi)可以啟動bash進程,用戶就可以通過客戶端連接到該容器。容器無需啟動sshd進程,也就無需再以supervisord等進程作為entrypoint。更多的容器就可以以單進程的形式運行,降低了容器本身的維護成本。
Entry的設(shè)計細節(jié)
俗話說,細節(jié)決定成敗。為了提高使用體驗,Entry應用在設(shè)計與實現(xiàn)時考慮到了很多細節(jié),在這里拿出來與大家分享。
1. 連接保持:當WebSocket連接在一段時間內(nèi)沒有數(shù)據(jù)傳輸后,會自動斷開。這給用戶的使用帶來了極大的不便。Entry在設(shè)計時,對每一個建立的WebSocket連接,會有一個單獨的goroutine每隔10秒發(fā)送一個PING類型的Message(不是WebSocket協(xié)議中的PingMessage),這樣保證了在不主動斷開的情況下,用戶和容器可以一直保持連接。
2. 使用protobuf3制定消息格式并實現(xiàn)序列化與反序列化:使用protobuf3可以方便地定義與擴展自己的消息格式,同時在傳輸時能減小一定的帶寬占用。
Entry的消息格式有兩類,RequestMessage和ResponseMessage??蛻舳税l(fā)送的請求都屬于RequestMessage,服務端返回的數(shù)據(jù)都封裝在ResponseMessage中。其中:
- RequestMessage類型包括:PLAIN和WINCH。PLAIN就是用戶通過鍵盤的輸入。WINCH則是終端窗口大小改變的消息,內(nèi)容中會攜帶新窗口的rows和cols。
- ResponseMessage類型包括:STDOUT, STDERR,PING和CLOSE。STDOUT和STDERR代表了該消息內(nèi)容是來自于標準輸出還是標準錯誤。PING則代表是連接保持專用的信息。CLOSE則是連接將要斷開前Entry返回的信息,會包含錯誤原因或者正常退出的信息。
3. 監(jiān)聽終端窗口大小改變:默認的終端窗口大小都是80 * 24,但該標準在當前的日常使用中早已過時。如果在一個全屏的terminal中仍然使用該大小顯然是不合理的。因此客戶端在成功連接到容器后,客戶端會首先根據(jù)當前的terminal大小發(fā)送一個WINCH類型的RequestMessage,Entry收到后會調(diào)用ExecResize接口,這樣之后所有的stdout和stderr都會按照新的終端大小顯示??蛻舳诉€會監(jiān)聽窗口大小改變的事件,如果發(fā)生改變,同樣還會發(fā)送WINCH到Entry。
4. UTF-8編碼檢查:客戶端和服務端在發(fā)送消息內(nèi)容時,都會對緩沖區(qū)內(nèi)要發(fā)送的數(shù)據(jù)做UTF-8編碼檢查。如果發(fā)送數(shù)據(jù)不符合編碼規(guī)則,則會先發(fā)送最長符合的緩沖區(qū)前綴,后面剩余的數(shù)據(jù)則被移到緩沖區(qū)的開始,待下次發(fā)送。這種設(shè)計是為了處理中文等非latin1字符的顯示問題。避免因為非法的UTF-8編碼造成終端顯示亂碼。
Entry存在的問題
非正常退出時,bash進程不會結(jié)束,而是會以sleep的狀態(tài)殘留于容器中。如果一個容器有過多的bash進程,很可能因為cgroup的內(nèi)存限制導致容器退出。目前官方并沒有給出類似execKill的API,只能期待在以后版本的docker中能解決這個問題。
Entry應用依賴特定的LAIN客戶端。之前用戶只能通過lain enter命令進入容器。但是12月份后我們升級了console的前端,增加了web terminal功能。用戶只需要通過點擊容器的ID就可以打開一個含有terminal的web頁面,然后通過該web頁面與容器進行交互,不需要再安裝任何客戶端。在這里要十分感謝開源項目xterm.js(https://github.com/sourcelair/xterm.js),該項目基于JavaScript與CSS實現(xiàn)了一個近乎完美模擬xterm終端的插件。目前,console的web terminal可以支持Firefox和Chrome,但是無法支持IE和Safari。
總結(jié)
Entry是LAIN中一款設(shè)計較為精巧、技術(shù)含量較高的應用。其利用了WebSocket全雙工傳輸?shù)奶攸c,在單進程容器的場景下實現(xiàn)了對容器的遠程登錄,同時保證了登錄權(quán)限的控制。本文希望通過分享LAIN中Entry的設(shè)計與實現(xiàn),為需要開發(fā)遠程登錄容器功能的PaaS同行提供技術(shù)方案參考。Entry已經(jīng)開源,地址在(https://github.com/laincloud/entry),歡迎一起討論交流學習。