探祕 Go Playground 服務
什麼是 Playground?
Playground 是一個 Web 服務,允許任何擁有網絡瀏覽器的人編寫 Go 代碼,可以立即進行編譯、鏈接並在服務器上運行。這是給好奇的程序員一個在安裝之前嘗試 Go 語言的機會,並給有經驗的 Go 用戶一個方便的實驗場所。
當然,在 Playground 上運行的程序種類有一些限制,我們不能簡單地接受任意代碼並在我們的服務器上無限制的運行它。這些程序是在一個刪減的標準庫的沙箱中構建和運行。程序與外界的唯一通信就是通過標準輸出,CPU 和內存的使用也有限制。因此,不妨把這看作是對 Go 的世界的一種體驗,要獲得完整的體驗,你需要自己下載 Go 安裝包,並在本機上進行開發測試。
Playground 如何實現?
我們來了解一下,Playground 初始版本如何實現,以及我們如何與這些服務集成。該實現涉及到可變的操作系統環境變量和運行時。我們假設您對使用 Go 進行的系統編程有所熟悉。
Playground 架構視圖:
Playground 服務由三個部分組成:
1,運行在 Google 服務器上的後端,它接收 RPC 請求,使用 gc 工具鏈編譯用戶程序,執行用戶程序,並將程序輸出(或編譯錯誤)作爲 RPC 響應返回。
2,在 Google 應用引擎上運行的前端。它從客戶端接收 HTTP 請求,並向後端發出相應的 RPC 請求。它還進行一些緩存。
3,一個 JavaScript 客戶端,用於實現用戶界面並向前端發出 HTTP 請求。
後端的實現
後端程序本身是瑣碎的,所以我們不在這裏討論它的實現。有趣的部分是我們如何在安全的環境中安全地執行任意用戶代碼,同時仍然提供核心功能,如時間、網絡和文件系統。
爲了將用戶程序與谷歌的基礎設施隔離開來,後端在 Native Client(或 “NaCl”)下運行這些程序,這是谷歌開發的一種技術,允許在網絡瀏覽器中安全執行 x86 程序。後端使用生成 NaCl 可執行文件的 gc 工具鏈的特殊版本。(此專用工具鏈已合併到 Go 1.3 中。要了解更多信息,請閱讀設計文檔。)
NaCl 限制了程序可能消耗的 CPU 和 RAM 的數量,並阻止程序訪問網絡或文件系統。然而,這帶來了一個問題。Go 的併發性和網絡支持是其主要優勢之一,對文件系統的訪問對許多程序來說至關重要。爲了有效地展示併發性,我們需要時間,爲了展示網絡和文件系統,我們顯然需要一個網絡和一個文件系統。
儘管所有這些東西今天都得到了支持,但 2010 年推出的第一個版本的遊樂場卻沒有。目前的時間定爲 2009 年 11 月 10 日。time.Sleep 沒有影響,操作系統和網絡包的大多數功能都被截斷,返回 EINVALID 錯誤。
一年前,我們在操場上實施了假時間,這樣睡眠程序就能正常工作。最近對遊樂場的更新引入了一個假網絡堆棧和一個假文件系統,使遊樂場的工具鏈類似於普通 go 工具鏈。以下各節對這些設施進行了說明。
僞造時間
Playground 程序在 CPU 時間和內存使用量方面受到限制,但在實時使用量方面也受到限制。這是因爲每個正在運行的程序都會消耗後端的資源以及它與客戶端之間的任何有狀態基礎設施。限制每個遊樂場程序的運行時間可以使我們的服務更加可預測,並保護我們免受拒絕服務攻擊。
但是,當運行需要時間的代碼時,這些限制會變得令人窒息。Go 併發模式討論通過使用時間等計時函數的示例來演示併發性。像 time.Sleep 和 time.After。當在早期版本的 Playground 上運行時,這些程序的睡眠不會有任何影響,它們的行爲也會很奇怪(有時甚至是錯誤的)。
通過使用一個巧妙的技巧,我們可以讓 Go 程序認爲它在睡覺,而實際上睡覺根本不需要時間。爲了解釋這個技巧,我們首先需要了解調度器是如何管理睡眠 goroutine 的。
當一個 goroutine 調用 time.Sleep(或類似)調度器將一個定時器添加到一個掛起的定時器堆中,並使 goroutine 進入睡眠狀態。同時,一個特殊的定時器 goroutine 管理這個堆。當定時器 goroutine 啓動時,它告訴調度程序在下一個掛起的定時器準備好啓動時喚醒它,然後休眠。當它醒來時,它會檢查哪些計時器已經過期,喚醒相應的 goroutines,然後返回睡眠。
訣竅是改變喚醒計時器 goroutine 的條件。我們修改調度程序以等待死鎖,而不是在特定的時間段後將其喚醒;這時所有的 goroutines 應該是阻塞狀態。
Playground 版本的運行時維護自己的內部時鐘。當修改後的調度程序檢測到死鎖時,它會檢查是否有任何計時器掛起。如果有,它會將內部時鐘提前到最早計時器的觸發時間,然後喚醒計時器 goroutine。執行仍在繼續,程序認爲時間已經過去,而事實上睡眠幾乎是瞬間的。這些對調度程序的更改可以在 proc.c 和 time.goc 中找到。
僞造時間解決了後端資源耗盡的問題,但程序輸出呢?如果看到一個睡眠程序在不花任何時間的情況下正確運行到完成,那將是很奇怪的。以下程序每秒打印當前時間,然後在三秒後退出。試着運行它。
func main() {
stop := time.After(3 * time.Second)
tick := time.NewTicker(1 * time.Second)
defer tick.Stop()
for {
select {
case <-tick.C:
fmt.Println(time.Now())
case <-stop:
return
}
}
}
這是如何工作的?它是後端、前端和客戶端之間的協作。我們捕獲每次寫入標準輸出和標準錯誤的時間,並將其提供給客戶端。然後,客戶端可以以正確的時間 “回放” 寫入,這樣輸出就好像程序在本地運行一樣。
Playground 的運行時包提供了一個特殊的寫入功能,在每次寫入之前都包含一個小的 “回放頭”。回放頭包括魔術串、當前時間和寫入數據的長度。具有回放頭的寫入具有以下結構:
0 0 P B <8-byte time> <4-byte data length> <data>
上面程序的原始輸出如下所示:
\x00\x00PB\x11\x74\xef\xed\xe6\xb3\x2a\x00\x00\x00\x00\x1e2009-11-10 23:00:01 +0000 UTC
\x00\x00PB\x11\x74\xef\xee\x22\x4d\xf4\x00\x00\x00\x00\x1e2009-11-10 23:00:02 +0000 UTC
\x00\x00PB\x11\x74\xef\xee\x5d\xe8\xbe\x00\x00\x00\x00\x1e2009-11-10 23:00:03 +0000 UTC
前端將此輸出解析爲一系列事件,並將事件列表作爲 JSON 對象返回給客戶端:
{
"Errors": "",
"Events": [
{
"Delay": 1000000000,
"Message": "2009-11-10 23:00:01 +0000 UTC\n"
},
{
"Delay": 1000000000,
"Message": "2009-11-10 23:00:02 +0000 UTC\n"
},
{
"Delay": 1000000000,
"Message": "2009-11-10 23:00:03 +0000 UTC\n"
}
]
}
JavaScript 客戶端(在用戶的 web 瀏覽器中運行)然後使用所提供的延遲間隔播放事件。對用戶來說,程序似乎是實時運行的。
僞造文件系統
使用 Go 的 NaCl 工具鏈構建的程序無法訪問本地機器的文件系統。相反,系統調用包的文件相關功能(打開、讀取、寫入等)在由系統調用包本身實現的內存文件系統上運行。由於包 syscall 是 Go 代碼和操作系統內核之間的接口,因此用戶程序對文件系統的看法與對真實文件系統的理解完全相同。
以下示例程序將數據寫入文件,然後將其內容複製到標準輸出。試着運行它。(你也可以編輯它!)
func main() {
const filename = "/tmp/file.txt"
err := ioutil.WriteFile(filename, []byte("Hello, file system\n"), 0644)
if err != nil {
log.Fatal(err)
}
b, err := ioutil.ReadFile(filename)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", b)
}
當進程啓動時,文件系統中會填充 / dev / 下的一些設備和一個空的 / tmp 目錄。程序可以像往常一樣操作文件系統,但當進程退出時,對文件系統的任何更改都會丟失。
還有一項規定是在初始化時將 zip 文件加載到文件系統中(請參閱 unzip _ nacl.go)。到目前爲止,我們只使用解壓縮功能來提供運行標準庫測試所需的數據文件,但我們打算爲 Playg 程序提供一組文件,這些文件可用於文檔示例、博客文章和 Go Tour。
該實現可以在 fs_nacl.go 和 fd_nacl.go 文件中找到(由於它們的_nacl 後綴,只有當 GOOS 設置爲 nacl 時,它們纔會內置到包 syscall 中)。文件系統本身由 fsys 結構表示,在初始化時會創建一個全局實例(名爲 fs)。然後,各種與文件相關的函數在 fs 上操作,而不是進行實際的系統調用。例如,syscall.Open 函數:
func Open(path string, openmode int, perm uint32) (fd int, err error) {
fs.mu.Lock()
defer fs.mu.Unlock()
f, err := fs.open(path, openmode, perm&0777|S_IFREG)
if err != nil {
return -1, err
}
return newFD(f), nil
}
文件描述符由名爲 files 的全局切片跟蹤。每個文件描述符對應一個文件,每個文件提供一個實現 fileImpl 接口的值。該接口有幾種實現方法:
-
常規文件和設備(如 / dev/random)由 fsysFile 表示,
-
標準輸入、輸出和錯誤是 naclFile 的實例,它使用系統調用與實際文件交互(這些是 Playground 程序與外部世界交互的唯一方式),
-
網絡套接字有自己的實現,將在下一節中討論。
僞造網絡
與文件系統一樣,Playground 的網絡堆棧也是由系統調用包實現的進程中的僞堆棧。它允許 Playground 項目使用環回接口(127.0.0.1)。對其他主機的請求將失敗。
例如下面的示例。它在 TCP 端口上偵聽,等待傳入連接,將數據從該連接複製到標準輸出,然後退出。在另一個 goroutine 中,它建立到偵聽端口的連接,向連接寫入一個字符串,然後關閉它。
func main() {
l, err := net.Listen("tcp", "127.0.0.1:4000")
if err != nil {
log.Fatal(err)
}
defer l.Close()
go dial()
c, err := l.Accept()
if err != nil {
log.Fatal(err)
}
defer c.Close()
io.Copy(os.Stdout, c)
}
func dial() {
c, err := net.Dial("tcp", "127.0.0.1:4000")
if err != nil {
log.Fatal(err)
}
defer c.Close()
c.Write([]byte("Hello, network\n"))
}
網絡接口比文件接口更復雜,因此僞網絡的實現比僞文件系統更大、更復雜。它必須模擬讀寫超時、不同的地址類型和協議等等。該實現可以在 net_nacl.go 中找到。開始閱讀的一個好地方是 netFile,它是 fileImpl 接口的網絡套接字實現。
前端
Playground 前端是另一個簡單的程序(短於 100 行)。它從客戶端接收 HTTP 請求,向後端發出 RPC 請求,並進行一些緩存。
前端在提供 HTTP 處理程序 https://golang.org/compile. 處理程序需要一個 POST 請求,其中包含一個 body 字段(要運行的 Go 程序)和一個可選的 version 字段(對於大多數客戶端,應該是 “2”)。
當前端接收到編譯請求時,它首先檢查 memcache,看看它是否緩存了該源先前編譯的結果。如果找到,則返回緩存的響應。緩存可防止諸如 Go 主頁上的程序之類的流行程序使後端過載。如果沒有緩存的響應,前端會向後端發出 RPC 請求,將響應存儲在 memcache 中,解析播放事件,並將 JSON 對象作爲 HTTP 響應返回給客戶端(如上所述)。
客戶端
使用 Playground 的各個站點都共享一些通用的 JavaScript 代碼,用於設置用戶界面(代碼和輸出框、運行按鈕等)並與 Playground 前端通信。
此實現位於 go.tools 存儲庫中的 playground.js 文件中,該文件可以從 golang.org/x/tools/godoc/static 包導入。其中有些是乾淨的,有些是有點粗糙的,因爲它是合併了客戶端代碼的幾個不同實現的結果。
Playground 函數接受一些 HTML 元素,並將它們變成一個交互式 Playground 小部件。如果你想把 Playground 放在自己的網站上,你應該使用這個功能。
傳輸接口(未正式定義,這是 JavaScript)將用戶接口從與 web 前端對話的方式中抽象出來。HTTPTransport 是 Transport 的一種實現,它使用前面描述的基於 HTTP 的協議。SocketTransport 是另一個使用 WebSocket 的實現 。
爲了遵守同源策略,各種 web 服務器(例如 godoc)代理請求 / 編譯到位於的 Playground 服務 https://golang.org/compile. 通用的 golang.org/x/tools/playground 包可以進行代理。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/_cpSc0e86l18JbIqDoAFfQ