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日,时间。休眠没有任何作用,osnet程序包的大多数功能都被取消以返回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函数](https://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程序包的文件相关功能(OpenReadWrite等)在内存中运行由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 `](https://github.com/golang/go/blob/go1.3/sr... /pkg/syscall/unzip_nacl.go))。到目前为止,我们仅使用解压缩功能来提供运行标准库测试所需的数据文件,但是我们打算为Playground程序提供一组可在文档示例,博客文章和Go Tour中使用的文件。

可以在fs_nacl.gofd_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]的全局切片跟踪(https://github.com/golang/go/blob/master/src/syscall/fd_nacl.go#L17)。每个文件描述符对应一个file和每个文件<aaaa >提供一个实现[ fileImpl `](https://github.com/golang/go/blob/master/src/syscall/fd_nacl.go#L30)接口的值。接口有几种实现

-常规文件和设备(例如/ dev / random)由[fsysFile]表示(https://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前端的界面。 HTTPTransportTransport的实现,前面介绍了基于HTTP的协议。 SocketTransport是使用WebSocket的另一种实现(请参见下面的``离线播放'') 。

为了遵守same-origin policy,各种Web服务器(例如,godoc)通过以下请求代理/ compile前往https://golang.org/compile的游乐场服务。常见的golang.org/x/tools/playground软件包执行此代理。

离线播放

Go TourPresent 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月的一系列日常博客文章。

本文章首发在 LearnKu.com 网站上。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
上一篇 下一篇
贡献者:1
讨论数量: 0
发起讨论 只看当前版本


暂无话题~