Go Playground 的底层实现
安德鲁・格朗德
2013 年 12 月 12 日
介绍#
2010 年 9 月,我们介绍了 Go Playground,该 Web 服务可编译并执行任意 Go 代码并返回程序输出。
如果您是 Go 程序员,那么您可能已经通过直接使用 Go Playground 使用了 Go Tour .org/),或运行 Go 文档中的可执行示例。
您可能还通过单击 talks.golang.org 幻灯片上的 “运行” 按钮之一或此博客上的帖子 (例如有关字符串的最新文章)。
在本文中,我们将研究如何实现游乐场并将其与这些服务集成。该实现涉及不同的操作系统环境和运行时,此处的描述假定您已熟悉使用 Go 进行系统编程。
概述#
游乐场服务包括三个部分:
- 在 Google 服务器上运行的后端。它接收 RPC 请求,使用 gc 工具链编译用户程序,执行用户程序,然后将程序输出 (或编译错误) 作为 RPC 响应返回。
- 在 Google App Engine 上运行的前端。它从客户端接收 HTTP 请求,并向后端发出相应的 RPC 请求。它还进行一些缓存。
- 一个实现用户界面并向前端发出 HTTP 请求的 JavaScript 客户端。
后端#
后端程序本身是微不足道的,因此我们在这里不讨论其实现。有趣的部分是我们如何在安全的环境中安全地执行任意用户代码,同时仍提供诸如时间,网络和文件系统之类的核心功能。
为了将用户程序与 Google 的基础架构隔离开,后端在 Native Client(或 “ NaCl”) 下运行它们,这是 Google 开发的一种允许安全的技术在 Web 浏览器中执行 x86 程序。后端使用 gc 工具链的特殊版本,该版本可生成 NaCl 可执行文件。
(此特殊工具链已合并到 Go 1.3 中。要了解更多信息,请阅读设计文档。)
NaCl 限制了程序可能消耗的 CPU 和 RAM 的数量,并且它阻止程序访问网络或文件系统。但是,这带来了问题。 Go 的并发性和网络支持是其主要优势之一,对许多程序而言,访问文件系统至关重要。为了有效地展示并发性,我们需要时间,并且展示网络和文件系统时,我们显然需要网络和文件系统。
尽管今天支持所有这些功能,但是在 2010 年发布的第一版 Playground 却一无所获。当前时间固定为 2009 年 11 月 10 日,时间。休眠
没有任何作用,os
和 net
程序包的大多数功能都被取消以返回 EINVALID
错误。
一年前,我们在操场上实施了假时间,以便睡眠程序能够正确运行。游乐场的最新更新引入了伪造的网络堆栈和伪造的文件系统,使游乐场的工具链类似于普通的 Go 工具链。以下各节将介绍这些功能。
伪装时间#
游乐场程序在可以使用的 CPU 时间和内存方面受到限制,但在可以使用的实时程度方面也受到限制。这是因为每个正在运行的程序都会消耗后端以及客户端与客户端之间任何有状态基础结构的资源。限制每个游乐场程序的运行时间可以使我们的服务更加可预测,并可以防止拒绝服务攻击。
但是,这些限制在运行使用时间的代码时变得令人窒息。 Go 并发模式对话通过使用诸如 [time.Sleep
] 之类的计时功能的示例演示了并发性 (https:///golang .org/pkg /time/#Sleep) 和 time.After
。在早期版本的操场上运行时,这些程序的睡眠不会产生任何效果,并且它们的行为会很奇怪 (有时是错误的)。
通过使用一个巧妙的技巧,我们可以使 Go 程序思考它正在睡眠,而实际上睡眠根本不需要时间。为了解释这个技巧,我们首先需要了解调度程序如何管理睡眠 goroutine。
当 goroutine 调用 time.Sleep
(或类似方法) 时,调度程序会将计时器添加到待处理的计时器堆中,并使 goroutine 进入睡眠状态。同时,一个特殊的计时器 goroutine 管理该堆。当计时器 goroutine 启动时,它通知调度程序在下一个挂起的计时器准备就绪并随后休眠时将其唤醒。唤醒时,它将检查哪些计时器已到期,唤醒适当的 goroutine,然后返回睡眠状态。
技巧是更改唤醒计时器 goroutine 的条件。我们修改调度程序以等待死锁,而不是在特定时间段之后将其唤醒。所有 goroutine 被阻塞的状态。
运行时的游乐场版本维护其自己的内部时钟。修改后的调度程序检测到死锁时,它将检查是否有任何计时器处于挂起状态。如果是这样,它将内部时钟提前到最早的计时器的触发时间,然后唤醒计时器 goroutine。执行继续,程序认为时间已经过去,而实际上睡眠几乎是瞬时的。
可以在 proc.c
和 [time.goc
](https:/// golang.org/cl/73110043)。
虚假时间解决了后端资源耗尽的问题,但是程序输出如何?看到一个无需任何时间即可正确进入睡眠状态的程序,这很奇怪。
以下程序每秒打印一次当前时间,然后在三秒钟后退出。尝试运行它。
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
}
}
}
这是如何运作的?它是后端,前端和客户端之间的协作。
我们捕获每次写入标准输出和标准错误的时间,并将其提供给客户端。然后,客户端可以在正确的时间 “回放” 写入,以便输出看起来就像程序在本地运行一样。
游乐场的运行时
软件包提供了一个特殊的 [write
函数](github.com/golang/go/blob/go1.3/sr...。 s#L54),在每次写入之前都包含一个小的 “播放标头”。回放头包括魔术字符串,当前时间和写入数据的长度。具有播放头的写入具有以下结构:
0 0 P B <8-byte time> <4-byte data length> <data>
上面程序的原始输出看起来像这样:
.00.00PB.11.74.ef.ed.e6.b3.2a.00.00.00.00.1e2009-11-10 23:00:01 +0000 UTC
.00.00PB.11.74.ef.ee.22.4d.f4.00.00.00.00.1e2009-11-10 23:00:02 +0000 UTC
.00.00PB.11.74.ef.ee.5d.e8.be.00.00.00.00.1e2009-11-10 23:00:03 +0000 UTC
前端将此输出解析为一系列事件,并将事件列表作为 JSON 对象返回给客户端:
{
"Errors": "",
"Events": [
{
"Delay": 1000000000,
"Message": "2009-11-10 23:00:01 +0000 UTC."
},
{
"Delay": 1000000000,
"Message": "2009-11-10 23:00:02 +0000 UTC."
},
{
"Delay": 1000000000,
"Message": "2009-11-10 23:00:03 +0000 UTC."
}
]
}
然后,JavaScript 客户端 (在用户的 Web 浏览器中运行) 将使用提供的延迟间隔播放事件。对于用户来说,该程序似乎是实时运行的。
伪造文件系统#
使用 Go 的 NaCl 工具链构建的程序无法访问本地计算机的文件系统。相反,syscall
程序包的文件相关功能 (Open
,Read
,Write
等) 在内存中运行由 syscall
程序包本身实现的文件系统。由于软件包 syscall
是 Go 代码和操作系统内核之间的接口,因此用户程序看到文件系统的方式与实际方式完全相同。
下面的示例程序将数据写入文件,然后将其内容复制到标准输出。尝试运行它。 (您也可以编辑它!)
func main() {
const filename = "/tmp/file.txt"
err := ioutil.WriteFile(filename, []byte("Hello, file system."), 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 文件加载到文件系统中的规定 (请参阅 [<< aaaa> unzip_nacl.go `](github.com/golang/go/blob/go1.3/sr... /pkg/syscall/unzip_nacl.go))。到目前为止,我们仅使用解压缩功能来提供运行标准库测试所需的数据文件,但是我们打算为 Playground 程序提供一组可在文档示例,博客文章和 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
] 的全局切片跟踪 (github.com/golang/go/blob/master/s...file
和每个文件<aaaa >提供一个实现[
fileImpl `](github.com/golang/go/blob/master/s...:
- 常规文件和设备 (例如 / dev / random
) 由 [fsysFile
] 表示 (github.com/golang/go/blob/master/s... syscall / fs_nacl.go#L58),
- 标准输入,输出和错误是 naclFile
的实例,系统调用以与实际文件进行交互 (这是游乐场程序与外界交互的唯一方法),
- 网络套接字具有自己的实现,将在下一节中讨论。
伪造网络#
像文件系统一样,游乐场的网络堆栈是由 syscall
软件包实现的进程内伪造。它允许游乐场项目使用环回接口 (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."))
}
网络接口比文件接口复杂,因此伪造网络的实现比伪造文件系统更大,更复杂。它必须模拟读取和写入超时,不同的地址类型和协议等等。
可以在 net_nacl.go
中找到实现。开始阅读的好地方是 netFile
,它是 fileImpl
接口。
前端#
运动场前端是另一个简单的程序 (少于 100 行)。它从客户端接收 HTTP 请求,向后端发出 RPC 请求,并进行一些缓存。
前端在 https://golang.org/compile
提供 HTTP 处理程序。处理程序期望 POST 请求带有 body
字段 (要运行的 Go 程序) 和可选的 version
字段 (对于大多数客户端,此字段应为 “ 2”
)。
前端收到编译请求时,它首先检查 memcache 以查看其是否已缓存该源的先前编译结果。如果找到,它将返回缓存的响应。缓存可防止诸如 Go home page 上的流行程序的后端过载。如果没有缓存的响应,则前端向后端发出 RPC 请求,将响应存储在内存缓存中,解析播放事件,然后将 JSON 对象作为 HTTP 响应返回给客户端 (如上所述)。
客户端#
使用游乐场的各个站点都共享一些通用的 JavaScript 代码,以设置用户界面 (代码和输出框,运行按钮等) 并与游乐场前端进行通信。
此实现位于 go.tools中的文件[
playground.js ](https://github.com/golang/tools/blob/master/godoc/static/playground.js)中。
存储库,可以从 golang.org/x/tools/godoc/static
包装。其中一些是干净的,而有些则有些笨拙,因为这是合并客户端代码的多个不同实现的结果。
playground
函数接受一些 HTML 元素并将它们变成交互式的游乐场小部件。如果要将游乐场放置在自己的站点上,则应使用此功能 (请参见下面的 “其他客户”)。
Transport
接口 (未正式定义,这是 JavaScript) 抽象了用户从对话方式到 Web 前端的界面。 HTTPTransport
是 Transport
的实现,前面介绍了基于 HTTP 的协议。 SocketTransport
是使用 WebSocket 的另一种实现 (请参见下面的 `` 离线播放 '') 。
为了遵守 same-origin policy,各种 Web 服务器 (例如,godoc) 通过以下请求代理 / compile
前往 https://golang.org/compile
的游乐场服务。常见的 golang.org/x/tools/playground
软件包执行此代理。
离线播放#
Go Tour 和 Present Tool 都可以脱机运行。这对于互联网连接受限的人或会议中的演讲者不能 (也不应 *) 依靠有效的互联网连接非常有用。
要脱机运行,这些工具会在本地计算机上运行自己版本的游乐场后端。后端使用没有上述修改的常规 Go 工具链,并使用 WebSocket 与客户端进行通信。
WebSocket 后端实现可在 golang.org/x/tools/playground/socket...
中找到包。 Inside Present 对话详细讨论了此代码。
其他客户#
游乐场服务不仅用于正式的 Go 项目 (Go by Example 是另一个实例),我们很高兴为您在自己的网站上使用它。我们只要求您首先与我们联系,在您的请求中使用唯一的用户代理 (以便我们识别您),并且您的服务对 Go 社区有利。
结论#
从 godoc 到导览,再到这个博客,游乐场已经成为我们 Go 文档故事的重要组成部分。随着伪文件系统和网络堆栈的最新添加,我们很高兴将学习材料扩展到这些领域。
但是,最终,游乐场只是冰山一角。计划在 Go 1.3 中使用 Native Client 支持,我们期待看到社区可以使用它做什么。
本文是 Go Advent 日历的第 12 部分,整个 12 月的一系列日常博客文章。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: