理解Golang语句中的求值顺序

2 Comments

Golang在变量声明、初始化以及赋值语句上照比C语言有了许多改进:

a) 支持在同一行声明多个变量

var a, b, c int

b) 支持在同一行初始化多个变量(不同类型也可以)

var a, b, c = 5, "hello", 3.45
a, b, c := 5, "hello", 3.45 (short variable declaration)

c) 支持在同一行对多个变量进行赋值(在声明后且不同类型也可以)

a, b, c = 5, "hello", 3.45

这种语法糖我们是笑纳的,毕竟人生苦短,少写一行是一行啊^_^。

但这种语法糖却给我们带来了一些令人困惑的问题!比如下面这个就是Rob Pike在一个talk中slide(Go Course Day2)中的一个问题:

n0, n1 = n0 + n1, n0

or:

n0, n1 = op(n0,n1), n0

n0, n1的值在上述语句执行完毕后到底为多少呢?

显然这个问题涉及到Go语言的语句求值顺序(evaluation order)。求值序在任何一门编程语言中都是比较难缠的,很多情形下,语言规范给出的答案都是“undefined(未定义)” or "not specified" or “依赖实现”,尤其是对于哪些模棱两可的写法,就如Rob Pike给出的那个问题。

我们要想搞清楚Go中的求值顺序,我们需要求助于Go language specification,Go spec与Go发行版一起发布,你可以启动一个godoc web server(godoc -http=:6060,然后访问localhost:6060/ref/spec)查看go language specification。Go language specification专门有一个小节/ref/spec#Order_of_evaluation对求值顺序做了说明。

在Go specs中,有这样三点陈述:

1、变量声明(variable declaration)中的初始化表达式(initialization expressions)的求值顺序(evaluation order)由初始化依赖(initialization dependencies)决定;但对于初始化表达式内部的操作数的求值需要按照2中的顺序:从左到右;
2、在非变量初始化语句中,对表达式、赋值语句或返回语句中的操作数进行求值时,操作数中包含的函数(function)调用、方法(method)调用和通信操作(主要针对channel)将按语法从左到右的顺序求值。
3、赋值语句求值分为两个阶段,第一阶段是等号左边的index expressions、pointer indirections和等号右边的表达式中的操作数的求值顺序按照2中从左到右的顺序;第二阶段按从左到右的顺序对变量赋值。

下面我们就分别理解一下这三点。

一、变量声明中初始化表达式的求值顺序

带初始化表达式的变量声明的形式如下:

var a, b, c = expr1, expr2, expr3 //包级别或函数/方法内部

or 

a, b, c := expr1, expr2, expr3 //仅函数/方法内部

根据lang specs说明,求值顺序是由初始化依赖(initialization dependencies)规则决定的。那初始化依赖规则是什么呢?在Golang specs中也有专门章节说明:ref/spec#Package_initialization。

初始化依赖规则总结一下,大致有如下几条:

1、包中,包级别变量的初始化顺序按照声明先后的顺序,但如果某个变量(比如a)的初始化表达式中依赖其他变量(比如b),那么变量a的初始化顺序在变量b后面。
2、对于未初始化的,且不含有对应初始化表达式或其初始化表达式不依赖任何未初始化变量的变量,我们称之为"ready for initialization"变量。初始化就是按照声明顺序重复执行对下一个变量的初始化过程,直到没有"ready for initialization"变量为止。
3、如果初始化过程完毕后依然有变量处于未初始化状态,那程序有语法错误。
4、多个处于不同文件中的变量的声明顺序依赖编译器处理文件的顺序,先处理的文件中的变量的声明顺序先于后处理的文件中的所有变量。
5、依赖分析以包为单位执行,只有位于同一个包中的被依赖的变量、函数、方法才会被考虑。

规则是抽象难懂的,例子更直观易理解,我们看一个golang spec中的例子,并使用上述规则进行分析。实验环境:go 1.5, amd64,Darwin Kernel Version 13.1.0

//golang-statements-evaluating-order/example1.go
package main

import "fmt"

var (
    a = c + b
    b = f()
    c = f()
    d = 3
)

func f() int {
    d++
    return d
}

func main() {
    fmt.Println(a, b, c, d)
}

我们来分析一下程序执行后的a, b, c, d四个变量的结果值,不过不同的初始化顺序会导致结果值不同,因此分析四个变量的初始化顺序是至关重要的。

变量a, b, c, d的初始化过程如下:

1、根据规则,初始化按照变量声明先后顺序进行,因此先来分析变量a,a初始化表达式依赖b 和c;因此变量a的初始化次序排在b、c的后面;
2、按照a的初始化右值表达式,c、b在右值表达式中的出现顺序是c先于b;
3、c是否是一个ready for initialization变量呢?我们看到c依赖f这个函数,而f这个函数则依赖变量d的初始化,因此d排在c之前;
4、我们来看变量d,"d = 3",d未初始化且不含有初始化表达式,因此d是一个ready for initialization变量,我们可以从d开始初始化了。至此四个变量的初始化顺序排定 d -> c -> b -> a;(这块儿与spec中分析有差异,但从运行结果来看,应该是这个顺序;关于这个spec的issue参见#12369)
5、d初始化为3,此时已初始化变量集合[d=3];
6、接着初始化c:c = f(),因此c = 4(此时d=4),此时已初始化变量集合[c=4,d=4];
7、接下来轮到b:b = f(),因此b = 5 (此时d = 5),此时已初始化变量集合[b=5,c=4,d=5];
8、最后初始化a: a = c + b,在已初始化变量集合中我们可以找到b和c,因此a= 9,这样四个变量到此均已初始化;
9、经过分析:程序执行的结果应该是9,5,4,5。

我们来执行一下这个程序,验证一下我们的分析结果是否正确:

$go run example1.go
9 5 4 5

我们再来看一个例子,也是golang specs中的例子,我们稍作改造,并把它设定为example2:

//golang-statements-evaluating-order/example2.go
package main

import "fmt"

var a, b, c = f() + v(), g(), sqr(u()) + v()

func f() int {
    fmt.Println("calling f")
    return c
}

func g() int {
    fmt.Println("calling g")
    return a
}

func sqr(x int) int {
    fmt.Println("calling sqr")
    return x * x
}

func v() int {
    fmt.Println("calling v")
    return 1
}

func u() int {
    fmt.Println("calling u")
    return 2
}

func main() {
    fmt.Println(a, b, c)
}

同样根据变量初始化依赖规则对这个例子进行分析:

1、按照变量声明顺序,先初始化a:a= f() + v(),f()依赖变量c;v不依赖任何变量,因此变量c的初始化顺序应该在a变量前:c -> a。
2、分析c:c = sqr(u()) + v();u、sqr、v三个函数不依赖任何变量,因此c处于ready for initialization,于是对c进行初始化,函数执行顺序(从左到右)为:u() -> sqr() -> v(); 此时已初始化变量集合:[c = 5];
3、回到a:a = f() + v(),c初始化后,a也处理ready for initialization,于是对a初始化,函数执行顺序为:f() -> v(),此时已初始化变量集合:[c=5, a= 6];
4、按照变量声明次序,接下来轮到变量b:b= g(),而g()依赖a,a已经初始化完毕了,因此b也是ready for initialization,于是对b初始化,函数执行次序为:g(),至此已初始化变量集合:[c=5, a=6, b=6]。
5、经过分析:程序执行的结果应该是6,6,5。

我们来执行一下这个程序,验证一下我们的分析结果是否正确:

$go run example2.go
calling u
calling sqr
calling v
calling f
calling v
calling g
6 6 5

二、非变量初始化语句中的求值顺序

前面提到过:在非变量初始化语句中,对表达式、赋值语句或返回语句中的操作数进行求值时,操作数中包含的函数(function)调用、方法(method)调用和通信操作(主要针对channel)将按语法从左到右的顺序求值

我们同样来看一个例子:example3.go

//golang-statements-evaluating-order/example3.go
package main

import "fmt"

func f() int {
    fmt.Println("calling f")
    return 1
}

func g(a, b, c int) int {
    fmt.Println("calling g")
    return 2
}

func h() int {
    fmt.Println("calling h")
    return 3
}

func i() int {
    fmt.Println("calling i")
    return 1
}

func j() int {
    fmt.Println("calling j")
    return 1
}

func k() bool {
    fmt.Println("calling k")
    return true
}

func main() {
    var y = []int{11, 12, 13}
    var x = []int{21, 22, 23}

    var c chan int = make(chan int)
    go func() {
        c <- 1
    }()

    y[f()], _ = g(h(), i()+x[j()], <-c), k()

    fmt.Println(y)
}

y[f()], _ = g(h(), i()+x[j()], <-c), k() 这行语句是赋值语句,但赋值语句的操作数中包含函数调用、channel操作,按照规则,这些函数调用、channel操作按从左到右顺序估值。

1、按照从左到右顺序,第一个是y[f()]中的f();
2、接下来是g(),g()的参数列表依然是一个赋值操作,因此其涉及到的函数调用顺序为h(), i(),j(),<-c,因此实际上的顺序为h() –> i()–> j() –> c操作 -> g();
3、最后是k(),因此完整的调用顺序是:f()->
h() –> i()–> j() –> c操作 -> g() –> k()。

实际运行情况如下:

$go run example3.go
calling f
calling h
calling i
calling j
calling g
calling k
[11 2 13]

三、赋值语句的求值顺序

我们再回到前面Rob Pike那个问题:

n0, n1 = n0 + n1, n0

or:

n0, n1 = op(n0, n1), n0

这是一个赋值语句,根据规则3,我们对等号两端的表达式的操作数采用从左到右的求值顺序。

我们假定初值:
n0, n1 = 1, 2

1、第一阶段:等号两端表达式求值,上述问题中,只有右端有n0+n1和n0两个表达式,但表达式的操作数(n0,n1)都是初始化过后的了,因此直接将值带入,得到求值结果。求值后,语句可以看成:n0, n1 = 3, 1;
2、第二阶段:赋值。n0 =3, n1 = 1

//golang-statements-evaluating-order/example4.go
package main

import "fmt"

func example1() {
    n0, n1 := 1, 2
    n0, n1 = n0+n1, n0
    fmt.Println(n0, n1)
}

func op(a, b int) int {
    return a + b
}

func example2() {
    n0, n1 := 1, 2
    n0, n1 = op(n0, n1), n0
    fmt.Println(n0, n1)
}

func main() {
    example1()
    example2()
}

$go run example4.go
3 1
3 1

四、小结

虽说理解了规则,但实际工作中我们还是尽量不要写出像:"var a, b, c = f() + v(), g(), sqr(u()) + v()"这样复杂、难以让人理解的语句。必要的话,拆分成多行就好了,还可以增加些代码量(如果你的公司是以代码量为评价绩效指标之一的),得饶人处且饶人啊,烧脑的语句还是尽量避免为好。

以上实验代码在这里可以下载到。

五、参考资料

The Go Programming Language Specification (Version of August 5, 2015) 

Go程序调试、分析与优化

3 Comments

Brad FitzpatrickYAPC Asia 2015(Yet Another Perl Conference)上做了一次技术分享,题为:"Go Debugging, Profiling, and Optimization"。个人感觉这篇分享中价值最大的是BradFitz现场演示的一个有关如何对Go程序进行调试、分析和优化的 Demo,Brad将demo上传到了他个人在github.com的repo中,但不知为何,repo中的代码似乎与repo里talk.md中的说明不甚一致(btw,我并没有看video)。于 是打算在这里按照Brad的思路重新走一遍demo的演示流程(所有演示代码在这里可以下载到)。

一、实验环境

$uname -a
Linux pc-tony 3.13.0-61-generic #100~precise1-Ubuntu SMP Wed Jul 29 12:06:40 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux

注意:在Darwin或Windows下,profile的结果可能与这里有很大不同(甚至完全不一样的输出和瓶颈热点)。

$go version
go version go1.5 linux/amd64

$ go env
GOARCH="amd64"
GOBIN="/home1/tonybai/.bin/go15/bin"
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home1/tonybai/proj/GoProjects"
GORACE=""
GOROOT="/home1/tonybai/.bin/go15"
GOTOOLDIR="/home1/tonybai/.bin/go15/pkg/tool/linux_amd64"
GO15VENDOREXPERIMENT="1"
CC="gcc"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0"
CXX="g++"
CGO_ENABLED="1"

代码基于Brad的github.com/bradfitz/talk-yapc-asia-2015

二、待优化程序(step0)

待优化程序,也就是原始程序,我们放在step0中:

//go-debug-profile-optimization/step0/demo.go

package main

import (
    "fmt"
    "log"
    "net/http"
    "regexp"
)

var visitors int

func handleHi(w http.ResponseWriter, r *http.Request) {
    if match, _ := regexp.MatchString(`^\w*$`, r.FormValue("color")); !match {
        http.Error(w, "Optional color is invalid", http.StatusBadRequest)
        return
    }
    visitors++
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    w.Write([]byte("<h1 style='color: " + r.FormValue("color") +
        "'>Welcome!</h1>You are visitor number " + fmt.Sprint(visitors) + "!"))
}

func main() {
    log.Printf("Starting on port 8080")
    http.HandleFunc("/hi", handleHi)
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}

$go run demo.go
2015/08/25 09:42:35 Starting on port 8080

在浏览器输入:http://localhost:8080/hi

一切顺利的话,页面会显示:

Welcome!

You are visitor number 1!

三、添加测试代码

按照talk.md中的说明,brad repo中demo中根本没有测试代码(commit 2427d0faa12ed1fb05f1e6a1e69307c11259c2b2)。

于是我根据作者的意图,新增了demo_test.go,采用TestHandleHi_Recorder和TestHandleHi_TestServer对HandleHi进行测试:

//go-debug-profile-optimization/step0/demo_test.go
package main

import (
    "bufio"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
)

func TestHandleHi_Recorder(t *testing.T) {
    rw := httptest.NewRecorder()
    handleHi(rw, req(t, "GET / HTTP/1.0\r\n\r\n"))
    if !strings.Contains(rw.Body.String(), "visitor number") {
        t.Errorf("Unexpected output: %s", rw.Body)
    }
}

func req(t *testing.T, v string) *http.Request {
    req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(v)))
    if err != nil {
        t.Fatal(err)
    }
    return req
}

func TestHandleHi_TestServer(t *testing.T) {
    ts := httptest.NewServer(http.HandlerFunc(handleHi))
    defer ts.Close()
    res, err := http.Get(ts.URL)
    if err != nil {
        t.Error(err)
        return
    }
    if g, w := res.Header.Get("Content-Type"), "text/html; charset=utf-8"; g != w {
        t.Errorf("Content-Type = %q; want %q", g, w)
    }
    slurp, err := ioutil.ReadAll(res.Body)
    defer res.Body.Close()
    if err != nil {
        t.Error(err)
        return
    }
    t.Logf("Got: %s", slurp)
}

$ go test -v
=== RUN   TestHandleHi_Recorder
— PASS: TestHandleHi_Recorder (0.00s)
=== RUN   TestHandleHi_TestServer
— PASS: TestHandleHi_TestServer (0.00s)
    demo_test.go:45: Got: <h1 style='color: '>Welcome!</h1>You are visitor number 2!
PASS
ok      _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step0    0.007s

测试通过!

至此,step0使命结束。

四、Race Detector(竞态分析)

并发设计使得程序可以更好更有效的利用现代处理器的多核心。但并发设计很容易引入竞态,导致严重bug。Go程序中竞态就是当多个goroutine并发 访问某共享数据且未使用同步机制时,且至少一个goroutine进行了写操作。不过go工具自带race分析功能。在分析优化step0中demo代码 前,我们先要保证demo代码中不存在竞态。

工具的使用方法就是在go test后加上-race标志,在step0目录下:

$ go test -v -race
=== RUN   TestHandleHi_Recorder
— PASS: TestHandleHi_Recorder (0.00s)
=== RUN   TestHandleHi_TestServer
— PASS: TestHandleHi_TestServer (0.00s)
    demo_test.go:45: Got: <h1 style='color: '>Welcome!</h1>You are visitor number 2!
PASS
ok      _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step0    1.012s

-race通过做运行时分析做竞态分析,虽然不存在误报,但却存在实际有竞态,但工具没发现的情况。接下来我们改造一下测试代码,让test并发起来:

向step1(copy自step0)中demo_test.go中添加一个test method:

//go-debug-profile-optimization/step1/demo_test.go
… …

func TestHandleHi_TestServer_Parallel(t *testing.T) {
    ts := httptest.NewServer(http.HandlerFunc(handleHi))
    defer ts.Close()
    var wg sync.WaitGroup
    for i := 0; i < 2; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            res, err := http.Get(ts.URL)
            if err != nil {
                t.Error(err)
                return
            }
            if g, w := res.Header.Get("Content-Type"), "text/html; charset=utf-8"; g != w {
                t.Errorf("Content-Type = %q; want %q", g, w)
            }
            slurp, err := ioutil.ReadAll(res.Body)
            defer res.Body.Close()
            if err != nil {
                t.Error(err)
                return
            }
            t.Logf("Got: %s", slurp)
        }()
    }
    wg.Wait()
}
… …

执行竞态test:

$ go test -v -race
=== RUN   TestHandleHi_Recorder
— PASS: TestHandleHi_Recorder (0.00s)
=== RUN   TestHandleHi_TestServer
— PASS: TestHandleHi_TestServer (0.00s)
    demo_test.go:46: Got: <h1 style='color: '>Welcome!</h1>You are visitor number 2!
=== RUN   TestHandleHi_TestServer_Parallel
==================
WARNING: DATA RACE
Read by goroutine 22:
  _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step1.handleHi()
      /home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step1/demo.go:17 +0xf5
  net/http.HandlerFunc.ServeHTTP()
      /tmp/workdir/go/src/net/http/server.go:1422 +0×47
  net/http/httptest.(*waitGroupHandler).ServeHTTP()
      /tmp/workdir/go/src/net/http/httptest/server.go:200 +0xfe
  net/http.serverHandler.ServeHTTP()
      /tmp/workdir/go/src/net/http/server.go:1862 +0×206
  net/http.(*conn).serve()
      /tmp/workdir/go/src/net/http/server.go:1361 +0x117c

Previous write by goroutine 25:
  _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step1.handleHi()
      /home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step1/demo.go:17 +0×111
  net/http.HandlerFunc.ServeHTTP()
      /tmp/workdir/go/src/net/http/server.go:1422 +0×47
  net/http/httptest.(*waitGroupHandler).ServeHTTP()
      /tmp/workdir/go/src/net/http/httptest/server.go:200 +0xfe
  net/http.serverHandler.ServeHTTP()
      /tmp/workdir/go/src/net/http/server.go:1862 +0×206
  net/http.(*conn).serve()
      /tmp/workdir/go/src/net/http/server.go:1361 +0x117c

Goroutine 22 (running) created at:
  net/http.(*Server).Serve()
      /tmp/workdir/go/src/net/http/server.go:1910 +0×464

Goroutine 25 (running) created at:
  net/http.(*Server).Serve()
      /tmp/workdir/go/src/net/http/server.go:1910 +0×464
==================
— PASS: TestHandleHi_TestServer_Parallel (0.00s)
    demo_test.go:71: Got: <h1 style='color: '>Welcome!</h1>You are visitor number 3!
    demo_test.go:71: Got: <h1 style='color: '>Welcome!</h1>You are visitor number 4!
PASS
Found 1 data race(s)
exit status 66
FAIL    _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step1    1.023s

工具发现demo.go第17行:
        visitors++
是一处潜在的竞态条件。

visitors被多个goroutine访问但未采用同步机制。

既然发现了竞态条件,我们就需要fix it。有多种fix方法可选:

1、使用channel
2、使用Mutex
3、使用atomic

Brad使用了atomic:

//go-debug-profile-optimization/step1/demo.go
… …
var visitors int64 // must be accessed atomically

func handleHi(w http.ResponseWriter, r *http.Request) {
    if match, _ := regexp.MatchString(`^\w*$`, r.FormValue("color")); !match {
        http.Error(w, "Optional color is invalid", http.StatusBadRequest)
        return
    }
    visitNum := atomic.AddInt64(&visitors, 1)
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    w.Write([]byte("<h1 style='color: " + r.FormValue("color") +
        "'>Welcome!</h1>You are visitor number " + fmt.Sprint(visitNum) + "!"))
}
… …

再做一次测试:

$ go test -v -race
=== RUN   TestHandleHi_Recorder
— PASS: TestHandleHi_Recorder (0.00s)
=== RUN   TestHandleHi_TestServer
— PASS: TestHandleHi_TestServer (0.00s)
    demo_test.go:46: Got: <h1 style='color: '>Welcome!</h1>You are visitor number 2!
=== RUN   TestHandleHi_TestServer_Parallel
— PASS: TestHandleHi_TestServer_Parallel (0.00s)
    demo_test.go:71: Got: <h1 style='color: '>Welcome!</h1>You are visitor number 3!
    demo_test.go:71: Got: <h1 style='color: '>Welcome!</h1>You are visitor number 4!
PASS
ok      _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step1    1.020s

竞态条件被消除了!

至此,step1结束了使命!

五、CPU Profiling

要做CPU Profilling,我们需要benchmark数据,Go test提供benchmark test功能,我们只要写对应的Benchmark测试方法即可:

//go-debug-profile-optimization/step2/demo_test.go
… …
func BenchmarkHi(b *testing.B) {
    b.ReportAllocs()

    req, err := http.ReadRequest(bufio.NewReader(strings.NewReader("GET / HTTP/1.0\r\n\r\n")))
    if err != nil {
        b.Fatal(err)
    }

    for i := 0; i < b.N; i++ {
        rw := httptest.NewRecorder()
        handleHi(rw, req)
    }
}
… …

$ go test -v -run=^$ -bench=.
PASS
BenchmarkHi-4      100000         14808 ns/op        4961 B/op          81 allocs/op
ok      _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step2    1.648s

开始CPU Profiling:

$ go test -v -run=^$ -bench=^BenchmarkHi$ -benchtime=2s -cpuprofile=prof.cpu
PASS
BenchmarkHi-4      200000         14679 ns/op        4961 B/op          81 allocs/op
ok      _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step2    3.096s

执行完benchmark test后,step2目录下出现两个新文件prof.cpu和step2.test,这两个文件将作为后续go tool pprof的输入:
$ls
demo.go        demo_test.go    prof.cpu    step2.test*

使用go profile viewer工具:

$ go tool pprof step2.test prof.cpu
Entering interactive mode (type "help" for commands)
(pprof) top
1830ms of 3560ms total (51.40%)
Dropped 53 nodes (cum <= 17.80ms)
Showing top 10 nodes out of 133 (cum >= 1290ms)
      flat  flat%   sum%        cum   cum%
     480ms 13.48% 13.48%      980ms 27.53%  runtime.growslice
     360ms 10.11% 23.60%      700ms 19.66%  runtime.mallocgc
     170ms  4.78% 28.37%      170ms  4.78%  runtime.heapBitsSetType
     170ms  4.78% 33.15%      200ms  5.62%  runtime.scanblock
     120ms  3.37% 36.52%     1100ms 30.90%  regexp.makeOnePass.func2
     120ms  3.37% 39.89%      550ms 15.45%  runtime.newarray
     110ms  3.09% 42.98%      300ms  8.43%  runtime.makeslice
     110ms  3.09% 46.07%      220ms  6.18%  runtime.mapassign1
     100ms  2.81% 48.88%      100ms  2.81%  runtime.futex
      90ms  2.53% 51.40%     1290ms 36.24%  regexp.makeOnePass

(pprof) top –cum
0.18s of 3.56s total ( 5.06%)
Dropped 53 nodes (cum <= 0.02s)
Showing top 10 nodes out of 133 (cum >= 1.29s)
      flat  flat%   sum%        cum   cum%
         0     0%     0%      3.26s 91.57%  runtime.goexit
     0.02s  0.56%  0.56%      2.87s 80.62%  BenchmarkHi
         0     0%  0.56%      2.87s 80.62%  testing.(*B).launch
         0     0%  0.56%      2.87s 80.62%  testing.(*B).runN
     0.03s  0.84%  1.40%      2.80s 78.65%  step2.handleHi
     0.01s  0.28%  1.69%      2.46s 69.10%  regexp.MatchString
         0     0%  1.69%      2.24s 62.92%  regexp.Compile
         0     0%  1.69%      2.24s 62.92%  regexp.compile
     0.03s  0.84%  2.53%      1.56s 43.82%  regexp.compileOnePass
     0.09s  2.53%  5.06%      1.29s 36.24%  regexp.makeOnePass

(pprof) list handleHi
Total: 3.56s
ROUTINE ======================== handleHi in go-debug-profile-optimization/step2/demo.go
      30ms      2.80s (flat, cum) 78.65% of Total
         .          .      9:)
         .          .     10:
         .          .     11:var visitors int64 // must be accessed atomically
         .          .     12:
         .          .     13:func handleHi(w http.ResponseWriter, r *http.Request) {
         .      2.47s     14:    if match, _ := regexp.MatchString(`^\w*$`, r.FormValue("color")); !match {
         .          .     15:        http.Error(w, "Optional color is invalid", http.StatusBadRequest)
         .          .     16:        return
         .          .     17:    }
      10ms       20ms     18:    visitNum := atomic.AddInt64(&visitors, 1)
      10ms       90ms     19:    w.Header().Set("Content-Type", "text/html; charset=utf-8")
      10ms       20ms     20:    w.Write([]byte("<h1 style='color: " + r.FormValue("color") +
         .      200ms     21:        "'>Welcome!</h1>You are visitor number " + fmt.Sprint(visitNum) + "!"))
         .          .     22:}
         .          .     23:
         .          .     24:func main() {
         .          .     25:    log.Printf("Starting on port 8080")
         .          .     26:    http.HandleFunc("/hi", handleHi)
(pprof)

从top –cum来看,handleHi消耗cpu较大,而handleHi中,又是MatchString耗时最长。

六、第一次优化

前面已经发现MatchString较为耗时,优化手段:让正则式仅编译一次(step3):

// go-debug-profile-optimization/step3/demo.go

… …
var visitors int64 // must be accessed atomically

var rxOptionalID = regexp.MustCompile(`^\d*$`)

func handleHi(w http.ResponseWriter, r *http.Request) {
    if !rxOptionalID.MatchString(r.FormValue("color")) {
        http.Error(w, "Optional color is invalid", http.StatusBadRequest)
        return
    }

    visitNum := atomic.AddInt64(&visitors, 1)
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    w.Write([]byte("<h1 style='color: " + r.FormValue("color") +
        "'>Welcome!</h1>You are visitor number " + fmt.Sprint(visitNum) + "!"))
}
… …

运行一下bench:

$ go test -bench=.
PASS
BenchmarkHi-4     1000000          1678 ns/op         720 B/op           9 allocs/op
ok      _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step3    1.710s

对比之前在step2中运行的bench结果:

$ go test -v -run=^$ -bench=.
PASS
BenchmarkHi-4      100000         14808 ns/op        4961 B/op          81 allocs/op
ok      _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step2    1.648s

耗时相同,但优化后的bench运行了100w次,而之前的Bench运行10w次,相当于性能提高10倍。

再看看cpu prof结果:

$ go test -v -run=^$ -bench=^BenchmarkHi$ -benchtime=3s -cpuprofile=prof.cpu
PASS
BenchmarkHi-4     3000000          1640 ns/op         720 B/op           9 allocs/op
ok      _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step3    6.540s

$ go tool pprof step3.test prof.cpu
Entering interactive mode (type "help" for commands)
(pprof) top –cum 30
2.74s of 8.07s total (33.95%)
Dropped 72 nodes (cum <= 0.04s)
Showing top 30 nodes out of 103 (cum >= 0.56s)
      flat  flat%   sum%        cum   cum%
         0     0%     0%      7.17s 88.85%  runtime.goexit
     0.05s  0.62%  0.62%      6.21s 76.95%  step3.BenchmarkHi
         0     0%  0.62%      6.21s 76.95%  testing.(*B).launch
         0     0%  0.62%      6.21s 76.95%  testing.(*B).runN
     0.06s  0.74%  1.36%      4.96s 61.46%  step3.handleHi
     1.15s 14.25% 15.61%      2.35s 29.12%  runtime.mallocgc
     0.02s  0.25% 15.86%      1.63s 20.20%  runtime.systemstack
         0     0% 15.86%      1.53s 18.96%  net/http.Header.Set
     0.06s  0.74% 16.60%      1.53s 18.96%  net/textproto.MIMEHeader.Set
     0.09s  1.12% 17.72%      1.22s 15.12%  runtime.newobject
     0.05s  0.62% 18.34%      1.09s 13.51%  fmt.Sprint
     0.20s  2.48% 20.82%         1s 12.39%  runtime.mapassign1
         0     0% 20.82%      0.81s 10.04%  runtime.mcall
     0.01s  0.12% 20.94%      0.79s  9.79%  runtime.schedule
     0.05s  0.62% 21.56%      0.76s  9.42%  regexp.(*Regexp).MatchString
     0.09s  1.12% 22.68%      0.71s  8.80%  regexp.(*Regexp).doExecute
     0.01s  0.12% 22.80%      0.71s  8.80%  runtime.concatstring5
     0.20s  2.48% 25.28%      0.70s  8.67%  runtime.concatstrings
         0     0% 25.28%      0.69s  8.55%  runtime.gosweepone
     0.05s  0.62% 25.90%      0.69s  8.55%  runtime.mSpan_Sweep
         0     0% 25.90%      0.68s  8.43%  runtime.bgsweep
     0.04s   0.5% 26.39%      0.68s  8.43%  runtime.newarray
     0.01s  0.12% 26.52%      0.67s  8.30%  runtime.goschedImpl
     0.01s  0.12% 26.64%      0.65s  8.05%  runtime.gosched_m
         0     0% 26.64%      0.65s  8.05%  runtime.gosweepone.func1
     0.01s  0.12% 26.77%      0.65s  8.05%  runtime.sweepone
     0.28s  3.47% 30.24%      0.62s  7.68%  runtime.makemap
     0.17s  2.11% 32.34%      0.59s  7.31%  runtime.heapBitsSweepSpan
     0.02s  0.25% 32.59%      0.58s  7.19%  fmt.(*pp).doPrint
     0.11s  1.36% 33.95%      0.56s  6.94%  fmt.(*pp).printArg

handleHi耗时有一定下降。

七、Mem Profiling

在step3目录下执行bench,获取mem分配数据:

$ go test -v -run=^$ -bench=^BenchmarkHi$ -benchtime=2s -memprofile=prof.mem
PASS
BenchmarkHi-4     2000000          1657 ns/op         720 B/op           9 allocs/op
ok      _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step3    5.002s

使用pprof工具分析mem:

$ go tool pprof –alloc_space step3.test prof.mem
Entering interactive mode (type "help" for commands)
(pprof) top
2065.91MB of 2067.41MB total (99.93%)
Dropped 14 nodes (cum <= 10.34MB)
      flat  flat%   sum%        cum   cum%
 1076.35MB 52.06% 52.06%  1076.35MB 52.06%  net/textproto.MIMEHeader.Set
  535.54MB 25.90% 77.97%  2066.91MB   100%  step3.BenchmarkHi
  406.52MB 19.66% 97.63%  1531.37MB 74.07%  step3.handleHi
   47.50MB  2.30% 99.93%    48.50MB  2.35%  fmt.Sprint
         0     0% 99.93%  1076.35MB 52.06%  net/http.Header.Set
         0     0% 99.93%  2066.91MB   100%  runtime.goexit
         0     0% 99.93%  2066.91MB   100%  testing.(*B).launch
         0     0% 99.93%  2066.91MB   100%  testing.(*B).runN

(pprof) top -cum
2065.91MB of 2067.41MB total (99.93%)
Dropped 14 nodes (cum <= 10.34MB)
      flat  flat%   sum%        cum   cum%
  535.54MB 25.90% 25.90%  2066.91MB   100%  step3.BenchmarkHi
         0     0% 25.90%  2066.91MB   100%  runtime.goexit
         0     0% 25.90%  2066.91MB   100%  testing.(*B).launch
         0     0% 25.90%  2066.91MB   100%  testing.(*B).runN
  406.52MB 19.66% 45.57%  1531.37MB 74.07%  step3.handleHi
         0     0% 45.57%  1076.35MB 52.06%  net/http.Header.Set
 1076.35MB 52.06% 97.63%  1076.35MB 52.06%  net/textproto.MIMEHeader.Set
   47.50MB  2.30% 99.93%    48.50MB  2.35%  fmt.Sprint

(pprof) list handleHi
Total: 2.02GB
     ROUTINE =========step3.handleHi in step3/demo.go
  406.52MB     1.50GB (flat, cum) 74.07% of Total
         .          .     17:        http.Error(w, "Optional color is invalid", http.StatusBadRequest)
         .          .     18:        return
         .          .     19:    }
         .          .     20:
         .          .     21:    visitNum := atomic.AddInt64(&visitors, 1)
         .     1.05GB     22:    w.Header().Set("Content-Type", "text/html; charset=utf-8")
         .          .     23:    w.Write([]byte("<h1 style='color: " + r.FormValue("color") +
  406.52MB   455.02MB     24:        "'>Welcome!</h1>You are visitor number " + fmt.Sprint(visitNum) + "!"))
         .          .     25:}
         .          .     26:
         .          .     27:func main() {
         .          .     28:    log.Printf("Starting on port 8080")
         .          .     29:    http.HandleFunc("/hi", handleHi)
(pprof)

可以看到handleHi22、23两行占用了较多内存。

八、第二次优化

第二次优化的方法:
1、删除w.Header().Set这行
2、用fmt.Fprintf替代w.Write

第二次优化的代码在step4目录中:

// go-debug-profile-optimization/step4/demo.go
… …
func handleHi(w http.ResponseWriter, r *http.Request) {
    if !rxOptionalID.MatchString(r.FormValue("color")) {
        http.Error(w, "Optional color is invalid", http.StatusBadRequest)
        return
    }

    visitNum := atomic.AddInt64(&visitors, 1)
    fmt.Fprintf(w, "<html><h1 stype='color: \"%s\"'>Welcome!</h1>You are visitor number %d!", r.FormValue("color"), visitNum)
}
… …

执行一遍pprof:

$ go test -v -run=^$ -bench=^BenchmarkHi$ -benchtime=2s -memprofile=prof.mem
PASS
BenchmarkHi-4     2000000          1428 ns/op         304 B/op           6 allocs/op
ok      _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step4    4.343s

$ go tool pprof –alloc_space step4.test prof.mem
Entering interactive mode (type "help" for commands)
(pprof) top
868.06MB of 868.56MB total (99.94%)
Dropped 5 nodes (cum <= 4.34MB)
      flat  flat%   sum%        cum   cum%
  559.54MB 64.42% 64.42%   868.06MB 99.94%  step4.BenchmarkHi
  219.52MB 25.27% 89.70%   219.52MB 25.27%  bytes.makeSlice
      89MB 10.25% 99.94%   308.52MB 35.52%  step4.handleHi
         0     0% 99.94%   219.52MB 25.27%  bytes.(*Buffer).Write
         0     0% 99.94%   219.52MB 25.27%  bytes.(*Buffer).grow
         0     0% 99.94%   219.52MB 25.27%  fmt.Fprintf
         0     0% 99.94%   219.52MB 25.27%  net/http/httptest.(*ResponseRecorder).Write
         0     0% 99.94%   868.06MB 99.94%  runtime.goexit
         0     0% 99.94%   868.06MB 99.94%  testing.(*B).launch
         0     0% 99.94%   868.06MB 99.94%  testing.(*B).runN
(pprof) top –cum
868.06MB of 868.56MB total (99.94%)
Dropped 5 nodes (cum <= 4.34MB)
      flat  flat%   sum%        cum   cum%
  559.54MB 64.42% 64.42%   868.06MB 99.94%  step4.BenchmarkHi
         0     0% 64.42%   868.06MB 99.94%  runtime.goexit
         0     0% 64.42%   868.06MB 99.94%  testing.(*B).launch
         0     0% 64.42%   868.06MB 99.94%  testing.(*B).runN
      89MB 10.25% 74.67%   308.52MB 35.52%  step4.handleHi
         0     0% 74.67%   219.52MB 25.27%  bytes.(*Buffer).Write
         0     0% 74.67%   219.52MB 25.27%  bytes.(*Buffer).grow
  219.52MB 25.27% 99.94%   219.52MB 25.27%  bytes.makeSlice
         0     0% 99.94%   219.52MB 25.27%  fmt.Fprintf
         0     0% 99.94%   219.52MB 25.27%  net/http/httptest.(*ResponseRecorder).Write
(pprof) list handleHi
Total: 868.56MB
ROUTINE ============ step4.handleHi in step4/demo.go
      89MB   308.52MB (flat, cum) 35.52% of Total
         .          .     17:        http.Error(w, "Optional color is invalid", http.StatusBadRequest)
         .          .     18:        return
         .          .     19:    }
         .          .     20:
         .          .     21:    visitNum := atomic.AddInt64(&visitors, 1)
      89MB   308.52MB     22:    fmt.Fprintf(w, "<html><h1 stype='color: \"%s\"'>Welcome!</h1>You are visitor number %d!", r.FormValue("color"), visitNum)
         .          .     23:}
         .          .     24:
         .          .     25:func main() {
         .          .     26:    log.Printf("Starting on port 8080")
         .          .     27:    http.HandleFunc("/hi", handleHi)
(pprof)

可以看出内存占用大幅减少。

九、Benchcmp

golang.org/x/tools中有一个工具:benchcmp,可以给出两次bench的结果对比。

github.com/golang/tools是golang.org/x/tools的一个镜像。安装benchcmp步骤:

1、go get -u github.com/golang/tools
2、mkdir -p $GOPATH/src/golang.org/x
3、mv $GOPATH/src/github.com/golang/tools $GOPATH/src/golang.org/x
4、go install golang.org/x/tools/cmd/benchcmp

我们分别在step2、step3和step4下执行如下命令:

$ go-debug-profile-optimization/step2$ go test -bench=. -memprofile=prof.mem | tee mem.2
PASS
BenchmarkHi-4      100000         14786 ns/op        4961 B/op          81 allocs/op
ok      _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step2    1.644s

go-debug-profile-optimization/step3$ go test -bench=. -memprofile=prof.mem | tee mem.3
PASS
BenchmarkHi-4     1000000          1662 ns/op         720 B/op           9 allocs/op
ok      _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step3    1.694s

go-debug-profile-optimization/step4$ go test -bench=. -memprofile=prof.mem | tee mem.4
PASS
BenchmarkHi-4     1000000          1428 ns/op         304 B/op           6 allocs/op
ok      _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step4    1.456s

利用benchcmp工具对比结果(benchcmp old new):

$ benchcmp step3/mem.3 step4/mem.4
benchmark         old ns/op     new ns/op     delta
BenchmarkHi-4     1662          1428          -14.08%

benchmark         old allocs     new allocs     delta
BenchmarkHi-4     9              6              -33.33%

benchmark         old bytes     new bytes     delta
BenchmarkHi-4     720           304           -57.78%

$ benchcmp step2/mem.2 step4/mem.4
benchmark         old ns/op     new ns/op     delta
BenchmarkHi-4     14786         1428          -90.34%

benchmark         old allocs     new allocs     delta
BenchmarkHi-4     81             6              -92.59%

benchmark         old bytes     new bytes     delta
BenchmarkHi-4     4961          304           -93.87%

可以看出优化后,内存分配大幅减少,gc的时间也随之减少。

十、内存来自哪

我们在BenchmarkHi中清理每次handleHi执行后的内存:

//step5/demo_test.go
… …
func BenchmarkHi(b *testing.B) {
    b.ReportAllocs()

    req, err := http.ReadRequest(bufio.NewReader(strings.NewReader("GET / HTTP/1.0\r\n\r\n")))
    if err != nil {
        b.Fatal(err)
    }

    for i := 0; i < b.N; i++ {
        rw := httptest.NewRecorder()
        handleHi(rw, req)
        reset(rw)
    }
}

func reset(rw *httptest.ResponseRecorder) {
    m := rw.HeaderMap
    for k := range m {
        delete(m, k)
    }
    body := rw.Body
    body.Reset()
    *rw = httptest.ResponseRecorder{
        Body:      body,
        HeaderMap: m,
    }
}

… …
$ go test -v -run=^$ -bench=^BenchmarkHi$ -benchtime=2s -memprofile=prof.mem
PASS
BenchmarkHi-4     2000000          1518 ns/op         304 B/op           6 allocs/op
ok      _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step5    4.577s

$ go tool pprof –alloc_space step5.test prof.mem
Entering interactive mode (type "help" for commands)
(pprof) top –cum 10
290.52MB of 291.52MB total (99.66%)
Dropped 14 nodes (cum <= 1.46MB)
      flat  flat%   sum%        cum   cum%
         0     0%     0%   291.02MB 99.83%  runtime.goexit
  179.01MB 61.41% 61.41%   290.52MB 99.66%  step5.BenchmarkHi
         0     0% 61.41%   290.52MB 99.66%  testing.(*B).launch
         0     0% 61.41%   290.52MB 99.66%  testing.(*B).runN
   26.50MB  9.09% 70.50%   111.51MB 38.25%  step5.handleHi
         0     0% 70.50%    85.01MB 29.16%  bytes.(*Buffer).Write
         0     0% 70.50%    85.01MB 29.16%  bytes.(*Buffer).grow
   85.01MB 29.16% 99.66%    85.01MB 29.16%  bytes.makeSlice
         0     0% 99.66%    85.01MB 29.16%  fmt.Fprintf
         0     0% 99.66%    85.01MB 29.16%  net/http/httptest.(*ResponseRecorder).Write
(pprof) list handleHi
Total: 291.52MB
ROUTINE ======================== _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step5.handleHi in /home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step5/demo.go
   26.50MB   111.51MB (flat, cum) 38.25% of Total
         .          .     17:        http.Error(w, "Optional color is invalid", http.StatusBadRequest)
         .          .     18:        return
         .          .     19:    }
         .          .     20:
         .          .     21:    visitNum := atomic.AddInt64(&visitors, 1)
   26.50MB   111.51MB     22:    fmt.Fprintf(w, "<html><h1 stype='color: \"%s\"'>Welcome!</h1>You are visitor number %d!", r.FormValue("color"), visitNum)
         .          .     23:}
         .          .     24:
         .          .     25:func main() {
         .          .     26:    log.Printf("Starting on port 8080")
         .          .     27:    http.HandleFunc("/hi", handleHi)
(pprof)

内存从300MB降到111MB。内存来自哪?看到list handleHi,fmt.Fprintf分配了111.51MB。

我们来看这一行代码:
fmt.Fprintf(w, "<h1 style='color: %s'>Welcome!</h1>You are visitor number %d!",
               r.FormValue("color"), num)

fmt.Fprintf的manual:

$ go doc fmt.Fprintf
func Fprintf(w io.Writer, format string, a …interface{}) (n int, err error)

    Fprintf formats according to a format specifier and writes to w. It returns
    the number of bytes written and any write error encountered.

这里回顾一下Go type在runtime中的内存占用:

A Go interface is 2 words of memory: (type, pointer).
A Go string is 2 words of memory: (base pointer, length)
A Go slice is 3 words of memory: (base pointer, length, capacity)

每次调用fmt.Fprintf,参数以value值形式传入函数时,程序就要为每个变参分配一个占用16bytes的empty interface,然后用传入的类型初始化该interface value。这就是这块累计分配内存较多的原因。

十一、消除所有内存分配

下面的优化代码可能在实际中并不需要,但一旦真的成为瓶颈,可以这么做:

//go-debug-profile-optimization/step6/demo.go
… …
var bufPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func handleHi(w http.ResponseWriter, r *http.Request) {
    if !rxOptionalID.MatchString(r.FormValue("color")) {
        http.Error(w, "Optional color is invalid", http.StatusBadRequest)
        return
    }

    visitNum := atomic.AddInt64(&visitors, 1)
    buf := bufPool.Get().(*bytes.Buffer)
    defer bufPool.Put(buf)
    buf.Reset()
    buf.WriteString("<h1 style='color: ")
    buf.WriteString(r.FormValue("color"))
    buf.WriteString("'>Welcome!</h1>You are visitor number ")
    b := strconv.AppendInt(buf.Bytes(), int64(visitNum), 10)
    b = append(b, '!')
    w.Write(b)
}

… …

$  go test -v -run=^$ -bench=^BenchmarkHi$ -benchtime=2s -memprofile=prof.mem
PASS
BenchmarkHi-4     5000000           780 ns/op         192 B/op           3 allocs/op
ok      _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step6    4.709s

 go tool pprof –alloc_space step6.test prof.mem
Entering interactive mode (type "help" for commands)
(pprof) top –cum 10
1.07GB of 1.07GB total (  100%)
Dropped 5 nodes (cum <= 0.01GB)
      flat  flat%   sum%        cum   cum%
    1.07GB   100%   100%     1.07GB   100%  step6.BenchmarkHi
         0     0%   100%     1.07GB   100%  runtime.goexit
         0     0%   100%     1.07GB   100%  testing.(*B).launch
         0     0%   100%     1.07GB   100%  testing.(*B).runN

$ go test -bench=. -memprofile=prof.mem | tee mem.6
PASS
BenchmarkHi-4     2000000           790 ns/op         192 B/op           3 allocs/op
ok      _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step6    2.401s

$ benchcmp step5/mem.5 step6/mem.6
benchmark         old ns/op     new ns/op     delta
BenchmarkHi-4     1513          790           -47.79%

benchmark         old allocs     new allocs     delta
BenchmarkHi-4     6              3              -50.00%

benchmark         old bytes     new bytes     delta
BenchmarkHi-4     304           192           -36.84%

可以看到handleHi已经不在top列表中了。benchcmp结果也显示内存分配又有大幅下降!

十二、竞争(Contention)优化

为handleHi编写一个Parallel benchmark test:

//go-debug-profile-optimization/step7/demo_test.go
… …
func BenchmarkHiParallel(b *testing.B) {
    r, err := http.ReadRequest(bufio.NewReader(strings.NewReader("GET / HTTP/1.0\r\n\r\n")))
    if err != nil {
        b.Fatal(err)
    }

    b.RunParallel(func(pb *testing.PB) {
        rw := httptest.NewRecorder()
        for pb.Next() {
            handleHi(rw, r)
            reset(rw)
        }
    })
}
… …

执行测试,并分析结果:

$ go test -bench=Parallel -blockprofile=prof.block
PASS
BenchmarkHiParallel-4     5000000           305 ns/op
ok      _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step7    1.947s

$ go tool pprof step7.test  prof.block
Entering interactive mode (type "help" for commands)
(pprof) top –cum 10
3.68s of 3.72s total (98.82%)
Dropped 29 nodes (cum <= 0.02s)
Showing top 10 nodes out of 20 (cum >= 1.84s)
      flat  flat%   sum%        cum   cum%
         0     0%     0%      3.72s   100%  runtime.goexit
     1.84s 49.46% 49.46%      1.84s 49.46%  runtime.chanrecv1
         0     0% 49.46%      1.84s 49.46%  main.main
         0     0% 49.46%      1.84s 49.46%  runtime.main
         0     0% 49.46%      1.84s 49.46%  testing.(*M).Run
         0     0% 49.46%      1.84s 49.43%  testing.(*B).run
         0     0% 49.46%      1.84s 49.43%  testing.RunBenchmarks
         0     0% 49.46%      1.84s 49.36%  step7.BenchmarkHiParallel
     1.84s 49.36% 98.82%      1.84s 49.36%  sync.(*WaitGroup).Wait
         0     0% 98.82%      1.84s 49.36%  testing.(*B).RunParallel
(pprof) list BenchmarkHiParallel
Total: 3.72s
ROUTINE ====== step7.BenchmarkHiParallel in step7/demo_test.go
         0      1.84s (flat, cum) 49.36% of Total
         .          .    113:        rw := httptest.NewRecorder()
         .          .    114:        for pb.Next() {
         .          .    115:            handleHi(rw, r)
         .          .    116:            reset(rw)
         .          .    117:        }
         .      1.84s    118:    })
         .          .    119:}
ROUTINE ==== step7.BenchmarkHiParallel.func1 in step7/demo_test.go
         0    43.02ms (flat, cum)  1.16% of Total
         .          .    110:    }
         .          .    111:
         .          .    112:    b.RunParallel(func(pb *testing.PB) {
         .          .    113:        rw := httptest.NewRecorder()
         .          .    114:        for pb.Next() {
         .    43.02ms    115:            handleHi(rw, r)
         .          .    116:            reset(rw)
         .          .    117:        }
         .          .    118:    })
         .          .    119:}
(pprof) list handleHi
Total: 3.72s
ROUTINE =====step7.handleHi in step7/demo.go
         0    43.02ms (flat, cum)  1.16% of Total
         .          .     18:        return new(bytes.Buffer)
         .          .     19:    },
         .          .     20:}
         .          .     21:
         .          .     22:func handleHi(w http.ResponseWriter, r *http.Request) {
         .    43.01ms     23:    if !rxOptionalID.MatchString(r.FormValue("color")) {
         .          .     24:        http.Error(w, "Optional color is invalid", http.StatusBadRequest)
         .          .     25:        return
         .          .     26:    }
         .          .     27:
         .          .     28:    visitNum := atomic.AddInt64(&visitors, 1)
         .     2.50us     29:    buf := bufPool.Get().(*bytes.Buffer)
         .          .     30:    defer bufPool.Put(buf)
         .          .     31:    buf.Reset()
         .          .     32:    buf.WriteString("<h1 style='color: ")
         .          .     33:    buf.WriteString(r.FormValue("color"))
         .          .     34:    buf.WriteString("'>Welcome!</h1>You are visitor number ")
(pprof)

handleHi中MatchString这块是一个焦点,这里耗时较多。

优化方法(step8):

//go-debug-profile-optimization/step8/demo.go
… …
var colorRxPool = sync.Pool{
    New: func() interface{} { return regexp.MustCompile(`\w*$`) },
}

func handleHi(w http.ResponseWriter, r *http.Request) {
    if !colorRxPool.Get().(*regexp.Regexp).MatchString(r.FormValue("color")) {
        http.Error(w, "Optional color is invalid", http.StatusBadRequest)
        return
    }

    visitNum := atomic.AddInt64(&visitors, 1)
    buf := bufPool.Get().(*bytes.Buffer)
    defer bufPool.Put(buf)
    buf.Reset()
    buf.WriteString("<h1 style='color: ")
    buf.WriteString(r.FormValue("color"))
    buf.WriteString("'>Welcome!</h1>You are visitor number ")
    b := strconv.AppendInt(buf.Bytes(), int64(visitNum), 10)
    b = append(b, '!')
    w.Write(b)
}
… …

测试执行与分析:

$ go test -bench=Parallel -blockprofile=prof.block
PASS
BenchmarkHiParallel-4      100000         19190 ns/op
ok      _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step8    2.219s

$ go tool pprof step8.test  prof.block
Entering interactive mode (type "help" for commands)
(pprof) top –cum 10
4.22s of 4.23s total (99.69%)
Dropped 28 nodes (cum <= 0.02s)
Showing top 10 nodes out of 12 (cum >= 2.11s)
      flat  flat%   sum%        cum   cum%
         0     0%     0%      4.23s   100%  runtime.goexit
     2.11s 49.90% 49.90%      2.11s 49.90%  runtime.chanrecv1
         0     0% 49.90%      2.11s 49.89%  main.main
         0     0% 49.90%      2.11s 49.89%  runtime.main
         0     0% 49.90%      2.11s 49.89%  testing.(*M).Run
         0     0% 49.90%      2.11s 49.86%  testing.(*B).run
         0     0% 49.90%      2.11s 49.86%  testing.RunBenchmarks
         0     0% 49.90%      2.11s 49.79%  step8.BenchmarkHiParallel
     2.11s 49.79% 99.69%      2.11s 49.79%  sync.(*WaitGroup).Wait
         0     0% 99.69%      2.11s 49.79%  testing.(*B).RunParallel
(pprof) list BenchmarkHiParallel
Total: 4.23s
ROUTINE ======step8.BenchmarkHiParallel in step8/demo_test.go
         0      2.11s (flat, cum) 49.79% of Total
         .          .    113:        rw := httptest.NewRecorder()
         .          .    114:        for pb.Next() {
         .          .    115:            handleHi(rw, r)
         .          .    116:            reset(rw)
         .          .    117:        }
         .      2.11s    118:    })
         .          .    119:}
ROUTINE ======step8.BenchmarkHiParallel.func1 in step8/demo_test.go
         0    11.68ms (flat, cum)  0.28% of Total
         .          .    110:    }
         .          .    111:
         .          .    112:    b.RunParallel(func(pb *testing.PB) {
         .          .    113:        rw := httptest.NewRecorder()
         .          .    114:        for pb.Next() {
         .    11.68ms    115:            handleHi(rw, r)
         .          .    116:            reset(rw)
         .          .    117:        }
         .          .    118:    })
         .          .    119:}
(pprof) list handleHi
Total: 4.23s
ROUTINE ======step8.handleHi in step8/demo.go
         0    11.68ms (flat, cum)  0.28% of Total
         .          .     21:var colorRxPool = sync.Pool{
         .          .     22:    New: func() interface{} { return regexp.MustCompile(`\w*$`) },
         .          .     23:}
         .          .     24:
         .          .     25:func handleHi(w http.ResponseWriter, r *http.Request) {
         .     5.66ms     26:    if !colorRxPool.Get().(*regexp.Regexp).MatchString(r.FormValue("color")) {
         .          .     27:        http.Error(w, "Optional color is invalid", http.StatusBadRequest)
         .          .     28:        return
         .          .     29:    }
         .          .     30:
         .          .     31:    visitNum := atomic.AddInt64(&visitors, 1)
         .     6.02ms     32:    buf := bufPool.Get().(*bytes.Buffer)
         .          .     33:    defer bufPool.Put(buf)
         .          .     34:    buf.Reset()
         .          .     35:    buf.WriteString("<h1 style='color: ")
         .          .     36:    buf.WriteString(r.FormValue("color"))
         .          .     37:    buf.WriteString("'>Welcome!</h1>You are visitor number ")
(pprof)

优化后,MatchString从43ms降到5.66ms。

Golang技术幻灯片的查看方法

2 Comments

随着go 1.5的发布,golang在世界各地日益受到欢迎,golang技术鼓吹者在世界各地举办各种级别的技术会议(从GopherCon大会到小小的meetup),并在会议上分享自己的技术心得和技术想法。

Golang相关的技术幻灯片有多种格式,以.ppt, .pdf和.slide为主。ppt、pdf自然不必多说,需要直接下载查看。

.slide是随着golang诞生而出现的一种present格式,Go核心开发成员似乎十分喜欢以这种格式分享Go语言。在Golang官方,几乎所有技术会议的talk幻灯片均是以.slide形式提供的。

.slide文件通过web服务查看,目前似乎尚无本地工具可以render slide文件。

目前已知的render .slide文件的服务包括:
- talks.golang.org
- go-talks.appspot.com

talks.golang.org是golang官方的服务,用于查看Go core team发表的各次技术演讲的幻灯片资料,按年份归档。

其他Go开发者用.slide形式编写的文件可以放在自己的github.com repo中,并使用go-talks.appspot.com这个第三方服务render。

比如:Dave Cheney将自己的performance-without-the-event-loop.slide存放在 github.com/davecheney/presentations下,那我们就可以通过如下url查看该slide render后的形式:

http://go-talks.appspot.com/github.com/davecheney/presentations/performance-without-the-event-loop.slide

不过由于appspot.com是Go appengine托管服务,国内无法访问,因此前期搭建了一个go-talks的镜像go-talks.tonybai.com,国内程序员可以无需fanqiang就可以访问(由于go-talks.tonybai.com托管主机内存不大,常常出现超时甚至crash现象,望谅解)。

因此要想看到上述slide,可以访问:

http://go-talks.tonybai.com/github.com/davecheney/presentations/performance-without-the-event-loop.slide

对于talks.golang.org上的slide,比如:

http://talks.golang.org/2015/gogo.slide

如果无法fanqiang又如何访问呢?这样行么?

http://go-talks.tonybai.com/talks.golang.org/2015/gogo.slide

结果告诉我们这样是不行的。那如何访问呢?

好在talks.golang.org上的slide都放在了github.com上,repo为https://github.com/golang/talks,上述那个gogo.slide,我们可以通过:

http://go-talks.tonybai.com/github.com/github.com/golang/talks/2015/gogo.slide访问。

补充

“相濡以沫”网友在评论中给出了一种在本地查看.slide的方法:

1、go get -u golang.org/x/tools/cmd/present //需翻墙
2、go install golang.org/x/tools/cmd/present,将present可执行程序放入$GOBIN或$GOPATH/bin中
3、下载你要查看的.slide,比如go get github.com/golang/talks,cd到talks所在目录,执行./present,你会看到如下结果:

$present
2015/08/23 19:34:51 Open your web browser and visit http://127.0.0.1:3999

打开浏览器,如果要查看当前目录下的2015/tricks.slide,则在浏览器里输入:http://127.0.0.1:3999/2015/tricks.slide即可查看该.slide文件。

weed-fs使用简介

No Comments

weed-fs,全名Seaweed-fs,是一种用golang实现的简单且高可用的分布式文件系统。该系统的目标有二:

- 存储billions of files
- serve the files fast

weed-fs起初是为了搞一个基于Fackbook的Haystack论文的实现,Haystack旨在优化Fackbook内部图片存储和获取。后在这个基 础上,weed-fs作者又增加了若干feature,形成了目前的weed-fs。

这里并不打算深入分析weed-fs源码,仅仅是从黑盒角度介绍weed-fs的使用,发掘weed-fs的功能、长处和不足。

一、weed-fs集群简介

weed-fs集群的拓扑(Topology)由DataCenter、Rack(机架)、Machine(或叫Node)组成。最初版本的weed-fs应该可以通 过配置文件来描述整个集群的拓扑结构,配置文件采用xml格式,官方给出的样例如下:

<Configuration>
  <Topology>
    <DataCenter name="dc1">
      <Rack name="rack1">
        <Ip>192.168.1.1</Ip>
      </Rack>
    </DataCenter>
    <DataCenter name="dc2">
      <Rack name="rack1">
        <Ip>192.168.1.2</Ip>
      </Rack>
      <Rack name="rack2">
        <Ip>192.168.1.3</Ip>
        <Ip>192.168.1.4</Ip>
      </Rack>
    </DataCenter>
  </Topology>
</Configuration>

但目前的版本中,该配置文件在help说明中被置为“Deprecating!”了:

$weed master -help

-conf="/etc/weedfs/weedfs.conf": Deprecating! xml configuration file

0.70版本的weed-fs在Master中维护集群拓扑,master会根据master与master、volume与master的连接 情况实时合成拓扑结构了。

weed-fs自身可以在两种模式下运行,一种是Master,另外一种则是Volume。集群的维护以及强一致性的保证由master们保 证,master间通过raft协议实现强一致性。Volume是实际管理和存储数据的运行实例。数据的可靠性则可以通过weed-fs提供的 replication机制保证。

weed-fs提供了若干种replication策略(rack – 机架,一个逻辑上的概念):

000 no replication, just one copy
001 replicate once on the same rack
010 replicate once on a different rack in the same data center
100 replicate once on a different data center
200 replicate twice on two other different data center
110 replicate once on a different rack, and once on a different data center

选择数据更可靠的策略,则会带来一些性能上的代价,这始终是一个权衡的问题。

更多的细节以及Scaling、数据迁移等方面,下面将逐一说明。

二、weed-fs集群的启动

为了实验方便,我们定义了一个weed-fs集群拓扑:

三个master:
    master1 – localhost:9333
    master2 – localhost:9334
    master3 – localhost:9335

    replication策略:100(即在另外一个不同的datacenter中复制一份)

三个volume:
         volume1 – localhost:8081  dc1
    volume2 – localhost:8082  dc1
    volume3 – localhost:8083  dc2

集群启动首先启动master们,启动顺序: master1、master2、master3:

master1:

$ weed -v=3 master -port=9333 -mdir=./m1 -peers=localhost:9333,localhost:9334,localhost:9335 -defaultReplication=100
I0820 14:37:17 07606 file_util.go:20] Folder ./m1 Permission: -rwxrwxr-x
I0820 14:37:17 07606 topology.go:86] Using default configurations.
I0820 14:37:17 07606 master_server.go:59] Volume Size Limit is 30000 MB
I0820 14:37:17 07606 master.go:69] Start Seaweed Master 0.70 beta at 0.0.0.0:9333
I0820 14:37:17 07606 raft_server.go:50] Starting RaftServer with IP:localhost:9333:
I0820 14:37:17 07606 raft_server.go:74] Joining cluster: localhost:9333,localhost:9334,localhost:9335
I0820 14:37:17 07606 raft_server.go:134] Attempting to connect to: http://localhost:9334/cluster/join
I0820 14:37:17 07606 raft_server.go:139] Post returned error:  Post http://localhost:9334/cluster/join: dial tcp 127.0.0.1:9334: connection refused
I0820 14:37:17 07606 raft_server.go:134] Attempting to connect to: http://localhost:9335/cluster/join
I0820 14:37:17 07606 raft_server.go:139] Post returned error:  Post http://localhost:9335/cluster/join: dial tcp 127.0.0.1:9335: connection refused
I0820 14:37:17 07606 raft_server.go:78] No existing server found. Starting as leader in the new cluster.
I0820 14:37:17 07606 master_server.go:93] [ localhost:9333 ] I am the leader!

I0820 14:37:52 07606 raft_server_handlers.go:16] Processing incoming join. Current Leader localhost:9333 Self localhost:9333 Peers map[]
I0820 14:37:52 07606 raft_server_handlers.go:20] Command:{"name":"localhost:9334","connectionString":"http://localhost:9334"}
I0820 14:37:52 07606 raft_server_handlers.go:27] join command from Name localhost:9334 Connection http://localhost:9334

I0820 14:38:02 07606 raft_server_handlers.go:16] Processing incoming join. Current Leader localhost:9333 Self localhost:9333 Peers map[localhost:9334:0xc20800f730]
I0820 14:38:02 07606 raft_server_handlers.go:20] Command:{"name":"localhost:9335","connectionString":"http://localhost:9335"}
I0820 14:38:02 07606 raft_server_handlers.go:27] join command from Name localhost:9335 Connection http://localhost:9335

master2:

$ weed -v=3 master -port=9334 -mdir=./m2 -peers=localhost:9333,localhost:9334,localhost:9335 -defaultReplication=100
I0820 14:37:52 07616 file_util.go:20] Folder ./m2 Permission: -rwxrwxr-x
I0820 14:37:52 07616 topology.go:86] Using default configurations.
I0820 14:37:52 07616 master_server.go:59] Volume Size Limit is 30000 MB
I0820 14:37:52 07616 master.go:69] Start Seaweed Master 0.70 beta at 0.0.0.0:9334
I0820 14:37:52 07616 raft_server.go:50] Starting RaftServer with IP:localhost:9334:
I0820 14:37:52 07616 raft_server.go:74] Joining cluster: localhost:9333,localhost:9334,localhost:9335
I0820 14:37:52 07616 raft_server.go:134] Attempting to connect to: http://localhost:9333/cluster/join
I0820 14:37:52 07616 raft_server.go:179] Post returned status:  200

master3:

$ weed -v=3 master -port=9335 -mdir=./m3 -peers=localhost:9333,localhost:9334,localhost:9335 -defaultReplication=100
I0820 14:38:02 07626 file_util.go:20] Folder ./m3 Permission: -rwxrwxr-x
I0820 14:38:02 07626 topology.go:86] Using default configurations.
I0820 14:38:02 07626 master_server.go:59] Volume Size Limit is 30000 MB
I0820 14:38:02 07626 master.go:69] Start Seaweed Master 0.70 beta at 0.0.0.0:9335
I0820 14:38:02 07626 raft_server.go:50] Starting RaftServer with IP:localhost:9335:
I0820 14:38:02 07626 raft_server.go:74] Joining cluster: localhost:9333,localhost:9334,localhost:9335
I0820 14:38:02 07626 raft_server.go:134] Attempting to connect to: http://localhost:9333/cluster/join
I0820 14:38:03 07626 raft_server.go:179] Post returned status:  200

master1启动后,发现其他两个peer master尚未启动,于是将自己选为leader。master2、master3启动后,加入到以master1为leader的 master集群。

接下来我们来启动volume servers:

volume1:

$ weed -v=3 volume -port=8081 -dir=./v1 -mserver=localhost:9333 -dataCenter=dc1
I0820 14:44:29 07642 file_util.go:20] Folder ./v1 Permission: -rwxrwxr-x
I0820 14:44:29 07642 store.go:225] Store started on dir: ./v1 with 0 volumes max 7
I0820 14:44:29 07642 volume.go:136] Start Seaweed volume server 0.70 beta at 0.0.0.0:8081
I0820 14:44:29 07642 volume_server.go:70] Volume server bootstraps with master localhost:9333
I0820 14:44:29 07642 list_masters.go:18] list masters result :{"IsLeader":true,"Leader":"localhost:9333","Peers":["localhost:9334","localhost:9335"]}
I0820 14:44:29 07642 store.go:65] current master nodes is nodes:[localhost:9334 localhost:9335 localhost:9333 localhost:9333], lastNode:3

volume server的启动大致相同,volume2和volume3的输出日志这里就不详细列出了。

volume2:

$weed -v=3 volume -port=8082 -dir=./v2 -mserver=localhost:9334 -dataCenter=dc1

volume3:

$weed -v=3 volume -port=8083 -dir=./v3 -mserver=localhost:9335 -dataCenter=dc2

三个volume server启动后,我们在leader master(9333)上能看到如下日志:

I0820 14:44:29 07606 node.go:208] topo adds child dc1
I0820 14:44:29 07606 node.go:208] topo:dc1 adds child DefaultRack
I0820 14:44:29 07606 node.go:208] topo:dc1:DefaultRack adds child 127.0.0.1:8081
I0820 14:47:09 07606 node.go:208] topo:dc1:DefaultRack adds child 127.0.0.1:8082
I0820 14:47:21 07606 node.go:208] topo adds child dc2
I0820 14:47:21 07606 node.go:208] topo:dc2 adds child DefaultRack
I0820 14:47:21 07606 node.go:208] topo:dc2:DefaultRack adds child 127.0.0.1:8083

至此,整个weed-fs集群已经启动了。初始启动后的master会在-mdir下建立一些目录和文件:

$ ls m1
conf  log  snapshot

但volume在-dir下没有做任何操作,volume server会在第一次写入数据时建立相应的.idx文件和.dat文件。

三、基本操作:存储、获取和删除文件

创建一个hello.txt文件,内容为"hello weed-fs!",用于我们测试weed-fs的基本操作。weed-fs提供了HTTP REST API接口,我们可以很方便的使用其基本功能(这里客户端使用curl)。

1、存储

我们来将hello.txt文件存储在weed-fs文件系统中,我们通过master提供的submit API接口来完成这一操作:

$ curl -F file=@hello.txt http://localhost:9333/submit
{"fid":"6,01fc4a422c","fileName":"hello.txt","fileUrl":"127.0.0.1:8082/6,01fc4a422c","size":39}

我们看到master给我们返回了一行json数据,其中:

fid是一个逗号分隔的字符串,按照repository中文档的说明,这个字符串应该由volume id, key uint64和cookie code构成。其中逗号前面的6就是volume id, 01fc4a422c则是key和cookie组成的串。fid是文件hello.txt在集群中的唯一ID。后续查看、获取以及删除该文件数据都需要使 用这个fid。

fileUrl是该文件在weed-fs中的一个访问地址(非唯一哦),这里是127.0.0.1:8082/6,01fc4a422c,可以看出weed-fs在volume server2上存储了一份hello.txt的数据。

这一存储操作引发了物理volume的创建,我们可以看到volume server的-dir下发生了变化,多了很多.idx和.dat文 件:

$ ls v1 v2 v3
v1:
3.dat  3.idx  4.dat  4.idx  5.dat  5.idx

v2:
1.dat  1.idx  2.dat  2.idx  6.dat  6.idx

v3:
1.dat  1.idx  2.dat  2.idx  3.dat  3.idx  4.dat  4.idx  5.dat  5.idx  6.dat  6.idx

并且这个创建过程是在master leader的控制之下的:

I0820 15:06:02 07606 volume_growth.go:204] Created Volume 3 on topo:dc1:DefaultRack:127.0.0.1:8081
I0820 15:06:02 07606 volume_growth.go:204] Created Volume 3 on topo:dc2:DefaultRack:127.0.0.1:8083

我们从文件的size可以看出,hello.txt文件被存储在了v2和v3下的id为6的卷(6.dat和6.idx)中:

v2:
-rw-r–r– 1 tonybai tonybai  104  8月20 15:06 6.dat
-rw-r–r– 1 tonybai tonybai   16  8月20 15:06 6.idx

v3:
-rw-r–r– 1 tonybai tonybai  104  8月20 15:06 6.dat
-rw-r–r– 1 tonybai tonybai   16  8月20 15:06 6.idx

v2和v3中的6.dat是一模一样的,6.idx也是一样的(后续在做数据迁移时,这点极其重要)。

2、获取

前面提到master给我们返回了一个fid:6,01fc4a422c以及fileUrl":"127.0.0.1:8082/6,01fc4a422c"。

通过这个fileUrl,我们可以获取到hello.txt的数据:

$ curl http://127.0.0.1:8082/6,01fc4a422c
hello weed-fs!

根据我们的replication策略,hello.txt应该还存储在v3下,我们换成8083这个volume,应该也可以得到 hello.txt数据:

$ curl http://127.0.0.1:8083/6,01fc4a422c
hello weed-fs!

如果我们通过volume1 (8081)查,应该得不到数据:

$ curl http://127.0.0.1:8081/6,01fc4a422c
<a href="http://127.0.0.1:8082/6,01fc4a422c">Moved Permanently</a>.

这里似乎是重定向了。我们给curl加上重定向处理选项再试一次:

$ curl -L  http://127.0.0.1:8081/6,01fc4a422c
hello weed-fs!

居然也能得到相应数据,从volume1的日志来看,volume1也能获取到hello.txt的正确地址,并将返回重定向请求,这样curl 就能从正确的machine上获取数据了。

如果我们通过master来获取hello.txt数据,会是什么结果呢?

$ curl -L  http://127.0.0.1:9335/6,01fc4a422c
hello weed-fs!

同样master返回重定向地址,curl从volume节点获取到正确数据。我们看看master是如何返回重定向地址的?

$ curl   http://127.0.0.1:9335/6,01fc4a422c
<a href="http://127.0.0.1:8082/6,01fc4a422c">Moved Permanently</a>.
$ curl   http://127.0.0.1:9335/6,01fc4a422c
<a href="http://127.0.0.1:8083/6,01fc4a422c">Moved Permanently</a>.

可以看到master会自动均衡负载,轮询式的返回8082和8083。0.70版本以前,通过非leader master是无法得到正确结果的,只能通过leader master得到,0.70版本fix了这个问题。

3、删除

通过fileUrl地址直接删除hello.txt:

$ curl -X DELETE http://127.0.0.1:8082/6,01fc4a422c
{"size":39}

操作成功后,我们再来get一下hello.txt:

$ curl -i  http://127.0.0.1:8082/6,01fc4a422c
HTTP/1.1 404 Not Found
Date: Thu, 20 Aug 2015 08:13:28 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8

$ curl -i -L  http://127.0.0.1:9335/6,01fc4a422c
HTTP/1.1 301 Moved Permanently
Content-Length: 69
Content-Type: text/html; charset=utf-8
Date: Thu, 20 Aug 2015 08:13:56 GMT
Location: http://127.0.0.1:8082/6,01fc4a422c

HTTP/1.1 404 Not Found
Date: Thu, 20 Aug 2015 08:13:56 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8

可以看出,无论是直接通过volume还是间接通过master都无法获取到hello.txt了,hello.txt被成功删除了。

不过删除hello.txt后,volume server下的数据文件的size却并没有随之减小,别担心,这就是weed-fs的处理方法,这些数据删除后遗留下来的空洞需要手工清除(对数据文件 进行手工紧缩):

$ curl "http://localhost:9335/vol/vacuum"
{"Topology":{"DataCenters":[{"Free":8,"Id":"dc1","Max":14,"Racks":[{"DataNodes":[{"Free":4,"Max":7,"PublicUrl":"127.0.0.1:8081","Url":"127.0.0.1:8081","Volumes":3},{"Free":4,"Max":7,"PublicUrl":"127.0.0.1:8082","Url":"127.0.0.1:8082","Volumes":3}],”Free”:8,”Id”:”DefaultRack”,”Max”:14}]},{“Free”:1,”Id”:”dc2″,”Max”:7,”Racks”:[{"DataNodes":[{"Free":1,"Max":7,"PublicUrl":"127.0.0.1:8083","Url":"127.0.0.1:8083","Volumes":6}],”Free”:1,”Id”:”DefaultRack”,”Max”:7}]}],”Free”:9,”Max”:21,”layouts”:[{"collection":"","replication":"100","ttl":"","writables":[1,2,3,4,5,6]}]},"Version":"0.70 beta"}

紧缩后,你再查看v1, v2, v3下的文件size,真的变小了。

四、一致性(consistency)

在分布式系统中,“一致性”是永恒的难题。weed-fs支持replication,其多副本的数据一致性需要保证。

weed-fs理论上采用了是一种“强一致性”的策略,即:

存储文件时,当多个副本都存储成功后,才会返回成功;任何一个副本存储失败,此次存储操作则返回失败。
删除文件时,当所有副本都删除成功后,才返回成功;任何一个副本删除失败,则此次删除操作返回失败。

我们来验证一下weed-fs是否做到了以上两点:

1、存储的一致性保证

我们先将volume3停掉(即dc2),这样在replication 策略为100时,向weed-fs存储hello.txt时会发生如下结果:

$ curl -F file=@hello.txt http://localhost:9333/submit
{"error":"Cannot grow volume group! Not enough data node found!"}

master根据100策略,需要在dc2选择一个volume存储hello.txt的副本,但dc2所有machine都down掉了,因此 没有存储空间,于是master认为此次操作无法继续进行,返回失败。这点符合存储一致性的要求。

2、删除的一致性保证

恢复dc2,将hello.txt存入:

$ curl -F file=@hello.txt http://localhost:9333/submit
{"fid":"6,04dce94a72","fileName":"hello.txt","fileUrl":"127.0.0.1:8082/6,04dce94a72","size":39}

再次停掉dc2,之后尝试删除hello.txt(通过master删除):

$ curl -L  -X DELETE http://127.0.0.1:9333/6,04dce94a72
{"error":"Deletion Failed."}

虽然返回的是delete failed,但从8082上的日志来看,似乎8082已经将hello.txt删除了:

I0820 17:32:20 07653 volume_server_handlers_write.go:53] deleting Cookie:3706276466, Id:4, Size:0, DataSize:0, Name: , Mime:

我们再从8082获取一下hello.txt:

$ curl  http://127.0.0.1:8082/6,04dce94a72

结果是什么也没有返回。

从8082日志来看:

I0820 17:33:24 07653 volume_server_handlers_read.go:53] read error: File Entry Not Found. Needle 70 Memory 0 /6,04dce94a72

hello.txt的确被删除了!

这时将dc2(8083)重新启动!我们尝试从8083获取hello.txt:

$ curl  http://127.0.0.1:8083/6,04dce94a72
hello weed-fs!

8083上的hello.txt依旧存在,可以被读取。

再试试通过master来获取hello.txt:

$ curl  -L http://127.0.0.1:9333/6,04dce94a72
$ curl  -L http://127.0.0.1:9333/6,04dce94a72
hello weed-fs!

结果是有时能返回hello.txt内容,有时不行。显然这是与master的自动负载均衡有关,返回8082这个重定向地址,则curl无法得 到结果;但若返回8083这个重定向地址,我们就可以得到hello.txt的内容。

这样来看,目前weed-fs的删除操作还无法保证强一致性。weed-fs github.com上已有若干issues(#172,#179,#182)是关于这个问题的。在大数据量(TB、PB级别)的情况下,这种不一致性最 大的问题是导致storage leak,即空间被占用而无法回收,volume将被逐个逐渐占满,期待后续的解决方案吧。

五、目录支持

weed-fs还支持像传统文件系统那样,将文件放在目录下管理,并通过文件路径对文件进行存储、获取和删除操作。weed-fs对目录的支持是 通过另外一个server实现的:filer server。也就是说如果想拥有对目录的支持,则必须启动一个(或若干个) filer server,并且所有的操作都要通过filer server进行。

$ weed filer -port=8888 -dir=./f1 -master=localhost:9333 -defaultReplicaPlacement=100
I0820 22:09:40 08238 file_util.go:20] Folder ./f1 Permission: -rwxrwxr-x
I0820 22:09:40 08238 filer.go:88] Start Seaweed Filer 0.70 beta at port 8888

1、存储

$curl -F "filename=@hello.txt" "http://localhost:8888/foo/"
{"name":"hello.txt","size":39}

2、获取

$ curl http://localhost:8888/foo/hello.txt
hello weed-fs!

3、查询目录文件列表

$ curl "http://localhost:8888/foo/?pretty=y"
{
  "Directory": "/foo/",
  "Files": [
    {
      "name": "hello.txt",
      "fid": "6,067281a126"
    }
  ],
  "Subdirectories": null
}

4、删除

$ curl -X DELETE http://localhost:8888/foo/hello.txt
{"error":""}

再尝试获取hello.txt:

$curl http://localhost:8888/foo/hello.txt
返回空。hello.txt已被删除。

5、filer server

weed filer server是单点,我们再来启动一个filer server。

$ weed filer -port=8889 -dir=./f2 -master=localhost:9333 -defaultReplicaPlacement=100
I0821 13:47:52 08973 file_util.go:20] Folder ./f2 Permission: -rwxrwxr-x
I0821 13:47:52 08973 filer.go:88] Start Seaweed Filer 0.70 beta at port 8889

两个filer节点间是否有协调呢?我们来测试一下:我们从8888存储一个文件,然后从8889获取这个文件:

$ curl -F "filename=@hello.txt" "http://localhost:8888/foo/"
{"name":"hello.txt","size":39}
$ curl http://localhost:8888/foo/hello.txt
hello weed-fs!
$ curl http://localhost:8889/foo/hello.txt

从测试结果来看,二者各自独立工作,并没有任何联系,也就是说没有共享“文件full path”到"fid"的索引关系。默认情况下 filer server都是工作在standalone模式下的。

weed-fs官方给出了filer的集群方案,即使用redis或Cassandra作为后端,在多个filer节点间共享“文件full path”到"fid"的索引关系。

我们启动一个redis-server(2.8.21),监听在默认的6379端口。用下面命令重启两个filer server节点:

$ weed filer -port=8888 -dir=./f1 -master=localhost:9333 -defaultReplicaPlacement=100 -redis.server=localhost:6379
$ weed filer -port=8889 -dir=./f2 -master=localhost:9333 -defaultReplicaPlacement=100 -redis.server=localhost:6379

重复一下上面的测试步骤:
$ curl -F "filename=@hello.txt" "http://localhost:8888/foo/"
{"name":"hello.txt","size":39}

$ curl http://localhost:8889/foo/hello.txt
hello weed-fs!

可以看到从8888存储的文件,可以被从8889获取到。

我们删除这个文件:
$ curl -X DELETE http://localhost:8889/foo/hello.txt
{"error":"Invalid fileId "}

提示error,但实际上文件已经被删除了!这块可能是个小bug(#183)。

虽然filer是集群了,但其后端的redis依旧是单点,如果考虑高可靠性,redis显然也要做好集群。

六、Collection

Collection,顾名思义是“集合”,在weed-fs中,它指的是物理volume的集合。前面我们在存储文件时并没有指定 collection,因此weed-fs采用默认collection(空)。如果我们指定集合,结果会是什么样子呢?

$ curl -F file=@hello.txt "http://localhost:9333/submit?collection=picture"
{"fid":"7,0c4f5dc90f","fileName":"hello.txt","fileUrl":"127.0.0.1:8083/7,0c4f5dc90f","size":39}

$ ls v1 v2 v3
v1:
3.dat  3.idx  4.dat  4.idx  5.dat  5.idx  picture_7.dat  picture_7.idx
v2:
1.dat  1.idx  2.dat  2.idx  6.dat  6.idx
v3:
1.dat  1.idx  2.dat  2.idx  3.dat  3.idx  4.dat  4.idx  5.dat  5.idx  6.dat  6.idx  picture_7.dat  picture_7.idx

可以看出volume server在自己的-dir下面建立了一个collection名字为prefix的idx和dat文件,上述例子中hello.txt被分配到 8081和8083两个volume server上,因此这两个volume server各自建立了picture_7.dat和picture_7.idx。以picture为前缀的idx和dat文件只是用来存放存储在 collection=picture的文件数据,其他数据要么存储在默认collection中,要么存储在其他名字的collection 中。

collection就好比为Windows下位驱动器存储卷起名。比如C:叫"系统盘",D叫“程序盘”,E叫“数据盘”。这里各个 volume server下的picture_7.dat和picture_7.idx被起名为picture卷。如果还有video collection,那么它可能由各个volume server下的video_8.dat和video_8.idx。

不过由于默认情况下,weed volume的默认-max="7",因此在实验环境下每个volume server最多在-dir下建立7个物理卷(七对.idx和.dat)。如果此时我还想建立video卷会怎么样呢?

$ curl -F file=@hello.txt "http://localhost:9333/submit?collection=video"
{"error":"Cannot grow volume group! Not enough data node found!"}

volume server们返回失败结果,提示无法再扩展volume了。这时你需要重启各个volume server,将-max值改大,比如100。

比如:$weed -v=3 volume -port=8083 -dir=./v3 -mserver=localhost:9335 -dataCenter=dc2 -max=100

重启后,我们再来建立video collection:

$ curl -F file=@hello.txt "http://localhost:9333/submit?collection=video"
{"fid":"11,0ee98ca54d","fileName":"hello.txt","fileUrl":"127.0.0.1:8083/11,0ee98ca54d","size":39}

$ ls v1 v2 v3
v1:
3.dat  4.dat  5.dat  picture_7.dat  video_10.dat  video_11.dat  video_12.dat  video_13.dat  video_9.dat
3.idx  4.idx  5.idx  picture_7.idx  video_10.idx  video_11.idx  video_12.idx  video_13.idx  video_9.idx

v2:
1.dat  1.idx  2.dat  2.idx  6.dat  6.idx  video_8.dat  video_8.idx

v3:
1.dat  2.dat  3.dat  4.dat  5.dat  6.dat  picture_7.dat  video_10.dat  video_11.dat  video_12.dat  video_13.dat  video_8.dat  video_9.dat
1.idx  2.idx  3.idx  4.idx  5.idx  6.idx  picture_7.idx  video_10.idx  video_11.idx  video_12.idx  video_13.idx  video_8.idx  video_9.idx

可以看到每个datacenter的volume server一次分配了6个volume作为video collection的存储卷。

七、伸缩(Scaling)

对于分布式系统来说,Scaling是不得不考虑的问题,也是极为常见的操作。

1、伸(scale up)

weed-fs对“伸"的支持是很好的,我们分角色说。

【master】
    master间采用的是raft协议,增加一个master,对于集群来说是最最基本的操作:

$weed -v=3 master -port=9336 -mdir=./m4 -peers=localhost:9333,localhost:9334,localhost:9335,localhost:9336 -defaultReplication=100
I0821 15:45:47 12398 file_util.go:20] Folder ./m4 Permission: -rwxrwxr-x
I0821 15:45:47 12398 topology.go:86] Using default configurations.
I0821 15:45:47 12398 master_server.go:59] Volume Size Limit is 30000 MB
I0821 15:45:47 12398 master.go:69] Start Seaweed Master 0.70 beta at 0.0.0.0:9336
I0821 15:45:47 12398 raft_server.go:50] Starting RaftServer with IP:localhost:9336:
I0821 15:45:47 12398 raft_server.go:74] Joining cluster: localhost:9333,localhost:9334,localhost:9335,localhost:9336
I0821 15:45:48 12398 raft_server.go:134] Attempting to connect to: http://localhost:9333/cluster/join
I0821 15:45:49 12398 raft_server.go:179] Post returned status:  200

新master节点启动后,会通过raft协议自动加入到以9333为leader的master集群中。

【volume】

和master一样,volume本身就是靠master管理的,volume server之间没有什么联系,增加一个volume server要做的就是启动一个新的volume server就好了:

$ weed -v=3 volume -port=8084 -dir=./v4 -mserver=localhost:9335 -dataCenter=dc2
I0821 15:48:21 12412 file_util.go:20] Folder ./v4 Permission: -rwxrwxr-x
I0821 15:48:21 12412 store.go:225] Store started on dir: ./v4 with 0 volumes max 7
I0821 15:48:21 12412 volume.go:136] Start Seaweed volume server 0.70 beta at 0.0.0.0:8084
I0821 15:48:21 12412 volume_server.go:70] Volume server bootstraps with master localhost:9335
I0821 15:48:22 12412 list_masters.go:18] list masters result :
I0821 15:48:22 12412 list_masters.go:18] list masters result :{"IsLeader":true,"Leader":"localhost:9333","Peers":["localhost:9334","localhost:9335","localhost:9336"]}
I0821 15:48:22 12412 store.go:65] current master nodes is nodes:[localhost:9334 localhost:9335 localhost:9336 localhost:9333 localhost:9333], lastNode:4
I0821 15:48:22 12412 volume_server.go:82] Volume Server Connected with master at localhost:9333

新volume server节点启动后,同样会自动加入集群,后续master就会自动在其上存储数据了。

【filer】

前面已经谈到了,无论是standalone模式,还是distributed模式,filter都可以随意增减,这里就不再重复赘述了。

2、缩(scale down)

master的缩是极其简单的,只需将相应节点shutdown即可;如果master是leader,则其他master会检测到leader shutdown,并自动重新选出新leader。不过在leader选举的过程中,整个集群的服务将短暂停止,直到leader选出。

filer在standalone模式下,谈伸缩是毫无意义的;对于distributed模式下,filter节点和master节点缩的方法 一致,shutdown即可。

唯一的麻烦就是volume节点,因为数据存储在volume节点下,我们不能简单的停掉volume,我们需要考虑在不同 replication策略下是否可以做数据迁移,如何做数据迁移。这就是下一节我们要详细描述的。

八、数据迁移

下面我们就来探讨一下weed-fs的volume数据迁移问题。

1、000复制策略下的数据迁移

为方便测试,我简化一下实验环境(一个master+3个volume):

master:

$ weed -v=3 master -port=9333 -mdir=./m1 -defaultReplication=000

volume:

$ weed -v=3 volume -port=8081 -dir=./v1 -mserver=localhost:9333 -dataCenter=dc1
$ weed -v=3 volume -port=8082 -dir=./v2 -mserver=localhost:9333 -dataCenter=dc1
$ weed -v=3 volume -port=8083 -dir=./v3 -mserver=localhost:9333 -dataCenter=dc1

和之前一样,启动后,v1,v2,v3目录下面是空的,卷的创建要等到第一份数据存入时。000策略就是没有副本的策略,你存储的文件在 weed-fs中只有一份数据。

我们上传一份文件:

$ curl -F filename=@hello1.txt "http://localhost:9333/submit"
{"fid":"1,01655ab58e","fileName":"hello1.txt","fileUrl":"127.0.0.1:8081/1,01655ab58e","size":40}

$ ll v1 v2 v3

v1:
-rw-r–r– 1 tonybai tonybai  104  8 21 21:31 1.dat
-rw-r–r– 1 tonybai tonybai   16  8 21 21:31 1.idx
-rw-r–r– 1 tonybai tonybai    8  8 21 21:31 4.dat
-rw-r–r– 1 tonybai tonybai    0  8 21 21:31 4.idx
-rw-r–r– 1 tonybai tonybai    8  8 21 21:31 7.dat
-rw-r–r– 1 tonybai tonybai    0  8 21 21:31 7.idx

v2:
-rw-r–r– 1 tonybai tonybai    8  8 21 21:31 2.dat
-rw-r–r– 1 tonybai tonybai    0  8 21 21:31 2.idx
-rw-r–r– 1 tonybai tonybai    8  8 21 21:31 3.dat
-rw-r–r– 1 tonybai tonybai    0  8 21 21:31 3.idx
-rw-r–r– 1 tonybai tonybai    8  8 21 21:31 6.dat
-rw-r–r– 1 tonybai tonybai    0  8 21 21:31 6.idx

v3:
-rw-r–r– 1 tonybai tonybai    8  8 21 21:31 5.dat
-rw-r–r– 1 tonybai tonybai    0  8 21 21:31 5.idx

可以看到hello1.txt被存储在v1下,同时可以看出不同的物理卷分别存放在不同节点下(由于不需要do replication)。

在这种情况(000)下,如果要将v1数据迁移到v2或v3中,只需将v1停掉,将v1下的文件mv到v2或v3中,重启volume server2或volume server3即可。

2、001复制策略下的数据迁移

001复制策略是weed-fs默认的复制策略,weed-fs会为每个文件在同Rack下复制一个副本。我们还利用上面的环境,不过需要停掉 weed-fs,清空目录下的文件,重启后使用,别忘了-defaultReplication=001。

我们连续存储三个文件:

$ curl -F filename=@hello1.txt "http://localhost:9333/submit"
{"fid":"2,01ea84980d","fileName":"hello1.txt","fileUrl":"127.0.0.1:8082/2,01ea84980d","size":40}

$ curl -F filename=@hello2.txt "http://localhost:9333/submit"
{"fid":"1,027883baa8","fileName":"hello2.txt","fileUrl":"127.0.0.1:8083/1,027883baa8","size":40}

$ curl -F filename=@hello3.txt "http://localhost:9333/submit"
{"fid":"6,03220f577e","fileName":"hello3.txt","fileUrl":"127.0.0.1:8081/6,03220f577e","size":40}

可以看出三个文件分别被存储在vol2, vol1和vol6中,我们查看一下v1, v2, v3中的文件情况:

$ ll v1 v2 v3
v1:
-rw-r–r– 1 tonybai tonybai  104  8 21 22:00 1.dat
-rw-r–r– 1 tonybai tonybai   16  8 21 22:00 1.idx
-rw-r–r– 1 tonybai tonybai    8  8 21 21:56 3.dat
-rw-r–r– 1 tonybai tonybai    0  8 21 21:56 3.idx
-rw-r–r– 1 tonybai tonybai    8  8 21 21:56 4.dat
-rw-r–r– 1 tonybai tonybai    0  8 21 21:56 4.idx
-rw-r–r– 1 tonybai tonybai  104  8 21 22:02 6.dat
-rw-r–r– 1 tonybai tonybai   16  8 21 22:02 6.idx

v2:
-rw-r–r– 1 tonybai tonybai  104  8 21 21:56 2.dat
-rw-r–r– 1 tonybai tonybai   16  8 21 21:56 2.idx
-rw-r–r– 1 tonybai tonybai    8  8 21 21:56 5.dat
-rw-r–r– 1 tonybai tonybai    0  8 21 21:56 5.idx

v3:
-rw-r–r– 1 tonybai tonybai  104  8 21 22:00 1.dat
-rw-r–r– 1 tonybai tonybai   16  8 21 22:00 1.idx
-rw-r–r– 1 tonybai tonybai  104  8 21 21:56 2.dat
-rw-r–r– 1 tonybai tonybai   16  8 21 21:56 2.idx
-rw-r–r– 1 tonybai tonybai    8  8 21 21:56 3.dat
-rw-r–r– 1 tonybai tonybai    0  8 21 21:56 3.idx
-rw-r–r– 1 tonybai tonybai    8  8 21 21:56 4.dat
-rw-r–r– 1 tonybai tonybai    0  8 21 21:56 4.idx
-rw-r–r– 1 tonybai tonybai    8  8 21 21:56 5.dat
-rw-r–r– 1 tonybai tonybai    0  8 21 21:56 5.idx
-rw-r–r– 1 tonybai tonybai  104  8 21 22:02 6.dat
-rw-r–r– 1 tonybai tonybai   16  8 21 22:02 6.idx

假设我们现在要shutdown v3,将v3数据迁移到其他volume server,我们有3种做法:

1) 不迁移
2) 将v3下的所有文件mv到v2或v1中
3) 将v3下的所有文件先后覆盖到v1和v2中

我们来逐个分析每种做法的后果:

1)  不迁移

001策略下,每份数据有两个copy,v3中的数据其他两个v1+v2总是有的,因此即便不迁移,v1+v2中也会有一份数据copy。你可以 测试一下当shutdown volume3后:

$ curl -L "http://localhost:9333/2,01ea84980d"
hello weed-fs1!
$ curl -L "http://localhost:9333/1,027883baa8"
hello weed-fs2!
$ curl -L "http://localhost:9333/6,03220f577e"
hello weed-fs3!

针对每一份文件,你都可以多get几次,都会得到正确的结果。但此时的不足也很明显,那就是存量数据不再拥有另外一份备份。

2) 将v3下的所有文件mv到v2或v1中

还是根据001策略,将v3数据mv到v2或v1中,结果会是什么呢,这里就以v3 mv到 v1举例:
 
 - 对于v1和v3都有的卷id,比如1,两者的文件1.idx和1.dat是一模一样的。这是001策略决定的。但一旦迁移后,系统中的数据就由2份变 成1份了
 - 对于v1有,而v3没有的,那自然不必说了。
 - 对于v1没有,而v3有的,mv过去就成为了v1的数据。

为此,这种做法依旧不够完美。

3)将v3下的所有文件覆盖到v1和v2中

结合上面的方法,只有此种迁移方式才能保证迁移后,系统中的数据不丢失,且每个都是按照001策略所说的2份,这才是正确的方法。

我们来测试一下:

   – 停掉volume3;
   – 停掉volume1,将v3下的文件copy到v1下,启动volume1
   – 停掉volume2,将v3下的文件copy到v2下,启动volume2

$ curl  "http://localhost:9333/6,03220f577e"
<a href="http://127.0.0.1:8081/6,03220f577e">Moved Permanently</a>.

$ curl  "http://localhost:9333/6,03220f577e"
<a href="http://127.0.0.1:8082/6,03220f577e">Moved Permanently</a>.

可以看到,master返回了重定向地址8081和8082,说明8083迁移到8082上的数据也生效了。

3、100复制策略下的数据迁移

测试环境稍作变化:

master:

$ weed -v=3 master -port=9333 -mdir=./m1 -defaultReplication=100

volume:

$ weed -v=3 volume -port=8081 -dir=./v1 -mserver=localhost:9333 -dataCenter=dc1
$ weed -v=3 volume -port=8082 -dir=./v2 -mserver=localhost:9333 -dataCenter=dc1
$ weed -v=3 volume -port=8083 -dir=./v3 -mserver=localhost:9333 -dataCenter=dc2

和之前一样,我们上传三份文件:

$ curl -F filename=@hello1.txt "http://localhost:9333/submit"
{"fid":"4,01d937dd30","fileName":"hello1.txt","fileUrl":"127.0.0.1:8083/4,01d937dd30","size":40}

$ curl -F filename=@hello2.txt "http://localhost:9333/submit"
{"fid":"2,025efbef14","fileName":"hello2.txt","fileUrl":"127.0.0.1:8082/2,025efbef14","size":40}

$ curl -F filename=@hello3.txt "http://localhost:9333/submit"
{"fid":"2,03be936488","fileName":"hello3.txt","fileUrl":"127.0.0.1:8082/2,03be936488","size":40}

$ ll v1 v2 v3
-rw-r–r– 1 tonybai tonybai    8  8 21 22:58 3.dat
-rw-r–r– 1 tonybai tonybai    0  8 21 22:58 3.idx
-rw-r–r– 1 tonybai tonybai  104  8 21 22:58 4.dat
-rw-r–r– 1 tonybai tonybai   16  8 21 22:58 4.idx

v2:
-rw-r–r– 1 tonybai tonybai    8  8 21 22:58 1.dat
-rw-r–r– 1 tonybai tonybai    0  8 21 22:58 1.idx
-rw-r–r– 1 tonybai tonybai  200  8 21 22:59 2.dat
-rw-r–r– 1 tonybai tonybai   32  8 21 22:59 2.idx
-rw-r–r– 1 tonybai tonybai    8  8 21 22:58 5.dat
-rw-r–r– 1 tonybai tonybai    0  8 21 22:58 5.idx
-rw-r–r– 1 tonybai tonybai    8  8 21 22:58 6.dat
-rw-r–r– 1 tonybai tonybai    0  8 21 22:58 6.idx

v3:
-rw-r–r– 1 tonybai tonybai    8  8 21 22:58 1.dat
-rw-r–r– 1 tonybai tonybai    0  8 21 22:58 1.idx
-rw-r–r– 1 tonybai tonybai  200  8 21 22:59 2.dat
-rw-r–r– 1 tonybai tonybai   32  8 21 22:59 2.idx
-rw-r–r– 1 tonybai tonybai    8  8 21 22:58 3.dat
-rw-r–r– 1 tonybai tonybai    0  8 21 22:58 3.idx
-rw-r–r– 1 tonybai tonybai  104  8 21 22:58 4.dat
-rw-r–r– 1 tonybai tonybai   16  8 21 22:58 4.idx
-rw-r–r– 1 tonybai tonybai    8  8 21 22:58 5.dat
-rw-r–r– 1 tonybai tonybai    0  8 21 22:58 5.idx
-rw-r–r– 1 tonybai tonybai    8  8 21 22:58 6.dat
-rw-r–r– 1 tonybai tonybai    0  8 21 22:58 6.idx

由于100策略是在不同DataCenter中各保持一份copy,因此数据的迁移不应该在数据中心间进行,而同一数据中心内的迁移又回归到了 “000”策略的情形。

其他策略的分析方式也是如此,这里就不长篇大论了。

九、Benchmark

在HP ProLiant DL380 G4, Intel(R) Xeon(TM) CPU 3.60GHz 4核,6G内存的机器(非SSD硬盘)上,执行benchmark test:

$ weed benchmark -server=localhost:9333

This is SeaweedFS version 0.70 beta linux amd64

———— Writing Benchmark ———-
Concurrency Level:      16
Time taken for tests:   831.583 seconds
Complete requests:      1048576
Failed requests:        0
Total transferred:      1106794545 bytes
Requests per second:    1260.94 [#/sec]
Transfer rate:          1299.75 [Kbytes/sec]

Connection Times (ms)
              min      avg        max      std
Total:        2.2      12.5       1118.4      9.3

Percentage of the requests served within a certain time (ms)
   50%     11.4 ms
   66%     13.3 ms
   75%     14.8 ms
   80%     15.9 ms
   90%     19.2 ms
   95%     22.6 ms
   98%     27.4 ms
   99%     31.2 ms
  100%    1118.4 ms

———— Randomly Reading Benchmark ———-
Concurrency Level:      16
Time taken for tests:   151.480 seconds
Complete requests:      1048576
Failed requests:        0
Total transferred:      1106791113 bytes
Requests per second:    6922.22 [#/sec]
Transfer rate:          7135.28 [Kbytes/sec]

Connection Times (ms)
              min      avg        max      std
Total:        0.1      2.2       116.7      3.9

Percentage of the requests served within a certain time (ms)
   50%      1.6 ms
   66%      2.1 ms
   75%      2.5 ms
   80%      2.8 ms
   90%      3.7 ms
   95%      4.8 ms
   98%      7.4 ms
   99%     11.1 ms
  100%    116.7 ms

这个似乎比作者在mac笔记本(SSD)上性能还要差些,当然此次我们用的策略是100,并且这个服务器上还运行着其他程序。但即便如此,感觉weed-fs还是有较大优化的空间的。

作者在官网上将weed-fs与其他分布式文件系统如Ceph,hdfs等做了简要对比,强调了weed-fs相对于其他分布式文件系统的优点。

十、其它

weed-fs使用google glog,因此所有log的级别设置以及log定向的方法均与glog一致。

weed-fs提供了backup命令,用来在同机上备份volume server上的数据。

weed-fs没有提供官方client包,但在wiki上列出多种第三方client包(各种语言),就Go client包来看,似乎还没有特别理想的。

weed-fs目前还没有web console,只能通过命令行进行操作。

使用weed-fs时,别忘了将open files no limit调大,否则可能会导致volume server crash。

十一、小结

weed-fs为想寻找开源分布式文件系统的朋友们提供了一个新选择。尤其是在存储大量小图片时,weed-fs自身就是基于haystack这一优化图 片存储的论文的。另外weed-fs使用起来的确十分简单,分分钟就可以建立起一个分布式系统,部署容易,几乎不需要什么配置。但weed-fs目前最大 的问题似乎是没有重量级的使用案例,自身也还有不少不足,但希望通过这篇文章能让更多人认识weed-fs,并使用weed-fs,帮助改善weed-fs吧。

godep支持Go 1.5 vendor

No Comments

Go 1.5 vendor/实验特性出炉后,市面上的go第三方包依赖和管理工具显然都无法与之兼容,除了修改代码,别无它法。市场占有率最大的godep做出了表 率,目前其最新版本(go get github.com/tools/godep)已经初步支持了这一实验特性,即在GO15VENDOREXPERIMENT=1时,将使用vendor 目录(而不是Godeps目录)存放copy的第三方包,并在godep go build时不再rewrite GOPATH就可以实现利用vendor下第三方包的构建。下面我们就用例子来验证一下Godep对vendor的支持。

一、升级godep到最新版本

如果要用到go 1.5 vendor,那么godep要升级(go get -u github.com/tools/godep;go build github.com/tools/godep)到当前的最新版本“commit d8799f112f6c8dfe1e56142831bc3bb5c8796a0e”。最新版本兼容老版本的功能,同时提供对go 1.5 vendor支持,两者之间转换的开关就是环境变量:GO15VENDOREXPERIMENT

GO15VENDOREXPERIMENT没有被set时,godep沿用以前的方式;当GO15VENDOREXPERIMENT = 1时,godep将用vendor替代Godeps目录以存放第三方包,同时go save将无法使用-r命令行选项(-r选项用于重写源码中的import path):

$ godep save -r
godep: flag -r is incompatible with the vendoring experiment

二、例子

下面是一个godep的例子(go 1.5 beta3),例子的目录结构如下:

$(GOPATH)/src/tonybai.com/
     ├── app
     │   └── main.go
     └── foolib
         └── foolib.go

//foolib.go
package foo

import "fmt"

func Hello() {
    fmt.Println("Hello from foolib")
}

//main.go
package main

import "tonybai.com/foolib"

func main() {
    foo.Hello()
}

如果GO15VENDOREXPERIMENT没有被set时,godep的各种命令将按之前的方式执行。

$ godep save
$ godep go build

$(GOPATH)/src/tonybai.com/
├── Godeps
│   ├── Godeps.json
│   ├── Readme
│   └── _workspace
│       └── src
│           └── tonybai.com
│               └── foolib
│                   └── foolib.go
├── app*
└── main.go

$./app
Hello from foolib

godep将第三方包放在Godeps/_workspace/src下面。godep go build会rewrite GOPATH以实现使用_workspace下面的第三方包来构建的目的。

如果GO15VENDOREXPERIMENT = 1,那么godep会按照新的方式执行各种命令:
$ godep save
$ godep go build

$(GOPATH)/src/tonybai.com/
├── Godeps
│   ├── Godeps.json
│   └── Readme
├── app*
├── main.go
└── vendor
    └── tonybai.com
        └── foolib
            └── foolib.go

可以看出godep建立vendor目录来存放第三方包,Godeps目录依然保留,但只是存放Godeps.json,以保存些第三方包的meta信息:
//Godeps.json
{
    "ImportPath": "tonybai.com/app",
    "GoVersion": "go1.5beta3",
    "Deps": [
        {
            "ImportPath": "tonybai.com/foolib",
            "Rev": "7f2f94dc589ba9e053ef13b3b01fa327c27bf161"
        }
    ]
}

三、迁移

由于godep前后的两种工作模式并不兼容,因此大量存量的使用godep的repo,如果想使用Go 1.5 vendor,那么在升级到Go 1.5之后需要做一些迁移工作。godep没有提供自动的迁移工具,目前只能手动迁移,godep github主页上给出了手动迁移的命令步骤:

$ unset GO15VENDOREXPERIMENT
$ godep restore

//如果之前使用了godep save -r,那么下面这行命令将自动undo rewritten import。
$ godep save ./…

$ rm -rf Godeps
$ export GO15VENDOREXPERIMENT=1
$ godep save ./…
# You should see your Godeps/_workspace/src files "moved" to vendor/.

理解Go 1.5 vendor

No Comments

Go 1.5中(目前最新版本go1.5beta3)加入了一个experimental feature: vendor/。这个feature不是Go 1.5的正式功能,但却是Go Authors们在解决Go被外界诟病的包依赖管理的道路上的一次重要尝试。目前关于Go vendor机制的资料有限,主要的包括如下几个:

1、Russ Cox在Golang-dev group上的一个名 为"proposal: external packages" topic上的reply。
2、Go 1.5beta版发布后Russ Cox根据上面topic整理的一个doc
3、medium.com上一篇名为“Go 1.5 vendor/ experiment"的文章。

但由于Go 1.5稳定版还未发布(最新消息是2015.8月中旬发布),因此估计真正采用vendor的repo尚没有。但既然是Go官方解决方案,后续从 expreimental变成official的可能性就很大(Russ的初步计划:如果试验顺利,1.6版本默认 GO15VENDOREXPERIMENT="1";1.7中将去掉GO15VENDOREXPERIMENT环境变量)。因此对于Gophers们,搞 清楚vendor还是很必要的。本文就和大家一起来理解下vendor这个新feature。

一、vendor由来

Go第三方包依赖和管理的问题由来已久,民间知名的解决方案就有godepgb等。这次Go team在推出vendor前已经在Golang-dev group上做了长时间的调研,最终Russ Cox在Keith Rarick的proposal的基础上做了改良,形成了Go 1.5中的vendor。

Russ Cox基于前期调研的结果,给出了vendor机制的群众意见基础:
    – 不rewrite gopath
    – go tool来解决
    – go get兼容
    – 可reproduce building process

并给出了vendor机制的"4行"诠释:

If there is a source directory d/vendor, then, when compiling a source file within the subtree rooted at d, import "p" is interpreted as import "d/vendor/p" if that exists.

When there are multiple possible resolutions,the most specific (longest) path wins.

The short form must always be used: no import path can  contain “/vendor/” explicitly.

Import comments are ignored in vendored packages.

这四行诠释在group中引起了强烈的讨论,短小精悍的背后是理解上的不小差异。我们下面逐一举例理解。

二、vendor基本样例

Russ Cox诠释中的第一条是vendor机制的基础。粗犷的理解就是如果有如下这样的目录结构:

d/
   vendor/
          p/
           p.go
   mypkg/
          main.go

如果mypkg/main.go中有"import p",那么这个p就会被go工具解析为"d/vendor/p",而不是$GOPATH/src/p。

现在我们就来复现这个例子,我们在go15-vendor-examples/src/basic下建立如上目录结构(其中go15-vendor-examples为GOPATH路径):

$ls -R
d/

./d:
mypkg/    vendor/

./d/mypkg:
main.go

./d/vendor:
p/

./d/vendor/p:
p.go

其中main.go代码如下:

//main.go
package main

import "p"

func main() {
    p.P()
}

p.go代码如下:

//p.go
package p

import "fmt"

func P() {
    fmt.Println("P in d/vendor/p")
}

在未开启vendor时,我们编译d/mypkg/main.go会得到如下错误结果:

$ go build main.go
main.go:3:8: cannot find package "p" in any of:
    /Users/tony/.bin/go15beta3/src/p (from $GOROOT)
    /Users/tony/OpenSource/github.com/experiments/go15-vendor-examples/src/p (from $GOPATH)

错误原因很显然:go编译器无法找到package p,d/vendor下的p此时无效。

这时开启vendor:export GO15VENDOREXPERIMENT=1,我们再来编译执行一次:
$go run main.go
P in d/vendor/p

开启了vendor机制的go tool在d/vendor下找到了package p。

也就是说拥有了vendor后,你的project依赖的第三方包统统放在vendor/下就好了。这样go get时会将第三方包同时download下来,使得你的project无论被下载到那里都可以无需依赖目标环境而编译通过(reproduce the building process)。

三、嵌套vendor

那么问题来了!如果vendor中的第三方包中也包含了vendor目录,go tool是如何choose第三方包的呢?我们来看看下面目录结构(go15-vendor-examples/src/embeded):

d/
   vendor/
          p/
            p.go
          q/
            q.go
            vendor/
               p/
                 p.go
   mypkg/
          main.go

embeded目录下出现了嵌套vendor结构:main.go依赖的q包本身还有一个vendor目录,该vendor目录下有一个p包,这样我们就有了两个p包。到底go工具会选择哪个p包呢?显然为了验证一些结论,我们源文件也要变化一下:

d/vendor/p/p.go的代码不变。

//d/vendor/q/q.go
package q

import (
    "fmt"
    "p"
)

func Q() {
    fmt.Println("Q in d/vendor/q")
    p.P()
}

//d/vendor/q/vendor/p/p.go
package p

import "fmt"

func P() {
    fmt.Println("P in d/vendor/q/vendor/p")
}

//mypkg/main.go
package main

import (
    "p"
    "q"
)

func main() {
    p.P()
    fmt.Println("")
    q.Q()
}

目录和代码编排完毕,我们就来到了见证奇迹的时刻了!我们执行一下main.go:

$go run main.go
P in d/vendor/p

Q in d/vendor/q
P in d/vendor/q/vendor/p

可以看出main.go中最终引用的是d/vendor/p,而q.Q()中调用的p.P()则是d/vendor/q/vendor/p包的实现。go tool到底是如何在嵌套vendor情况下选择包的呢?我们回到Russ Cox关于vendor诠释内容的第二条:

   When there are multiple possible resolutions,the most specific (longest) path wins.

这句话很简略,但却引来的巨大争论。"longest path wins"让人迷惑不解。如果仅仅从字面含义来看,上面main.go的执行结果更应该是:

P in d/vendor/q/vendor/p

Q in d/vendor/q
P in d/vendor/q/vendor/p

d/vendor/q/vendor/p可比d/vendor/p路径更long,但go tool显然并未这么做。它到底是怎么做的呢?talk is cheap, show you the code。我们粗略翻看一下go tool的实现代码:

在$GOROOT/src/cmd/go/pkg.go中有一个方法vendoredImportPath,这个方法在go tool中广泛被使用

// vendoredImportPath returns the expansion of path when it appears in parent.
// If parent is x/y/z, then path might expand to x/y/z/vendor/path, x/y/vendor/path,
// x/vendor/path, vendor/path, or else stay x/y/z if none of those exist.
// vendoredImportPath returns the expanded path or, if no expansion is found, the original.
// If no expansion is found, vendoredImportPath also returns a list of vendor directories
// it searched along the way, to help prepare a useful error message should path turn
// out not to exist.
func vendoredImportPath(parent *Package, path string) (found string, searched []string)

这个方法的doc讲述的很清楚,这个方法返回所有可能的vendor path,以parentpath为x/y/z为例:

x/y/z作为parentpath输入后,返回的vendorpath包括:
   
x/y/z/vendor/path
x/y/vendor/path
x/vendor/path
vendor/path

这么说还不是很直观,我们结合我们的embeded vendor的例子来说明一下,为什么结果是像上面那样!go tool是如何resolve p包的!我们模仿go tool对main.go代码进行编译(此时vendor已经开启)。

根据go程序的package init顺序,go tool首先编译p包。如何找到p包呢?此时的编译对象是d/mypkg/main.go,于是乎parent = d/mypkg,经过vendordImportPath处理,可能的vendor路径为:

d/mypkg/vendor
d/vendor

但只有d/vendor/下存在p包,于是go tool将p包resolve为d/vendor/p,于是下面的p.P()就会输出:
P in d/vendor/p

接下来初始化q包。与p类似,go tool对main.go代码进行编译,此时的编译对象是d/mypkg/main.go,于是乎parent = d/mypkg,经过vendordImportPath处理,可能的vendor路径为:

d/mypkg/vendor
d/vendor

但只有d/vendor/下存在q包,于是乎go tool将q包resolve为d/vendor/q,由于q包自身还依赖p包,于是go tool继续对q中依赖的p包进行选择,此时go tool的编译对象变为了d/vendor/q/q.go,parent = d/vendor/q,于是经过vendordImportPath处理,可能的vendor路径为:

d/vendor/q/vendor
d/vendor/vendor
d/vendor

存在p包的路径包括:

d/vendor/q/vendor/p
d/vendor/p

此时按照Russ Cox的诠释2:choose longest,于是go tool选择了d/vendor/q/vendor/p,于是q.Q()中的p.P()输出的内容就是:
"
P in d/vendor/q/vendor/p"

如果目录结构足够复杂,这个resolve过程也是蛮繁琐的,但按照这个思路依然是可以分析出正确的包的。

另外vendoredImportPath传入的parent x/y/z并不是一个绝对路径,而是一个相对于$GOPATH/src的路径。

BTW,上述测试样例代码在这里可以下载到。

四、第三和第四条

最难理解的第二条已经pass了,剩下两条就比较好理解了。

The short form must always be used: no import path can  contain “/vendor/” explicitly.

这条就是说,你在源码中不用理会vendor这个路径的存在,该怎么import包就怎么import,不要出现import "d/vendor/p"的情况。vendor是由go tool隐式处理的。

Import comments are ignored in vendored packages.

go 1.4引入了canonical imports机制,如:

package pdf // import "rsc.io/pdf"

如果你引用的pdf不是来自rsc.io/pdf,那么编译器会报错。但由于vendor机制的存在,go tool不会校验vendor中package的import path是否与canonical import路径是否一致了。

五、问题

根据小节三中的分析,对于vendor中包的resolving过程类似是一个recursive(递归)过程。

main.go中的p使用d/vendor/p;而q.go中的p使用的是d/vendor/q/vendor/p,这样就会存在一个问题:一个工程中存 在着两个版本的p包,这也许不会带来问题,也许也会是问题的根源,但目前来看从go tool的视角来看似乎没有更好的办法。Russ Cox期望大家良好设计工程布局,作为lib的包不携带vendor更佳。

这样一个project内的所有vendor都集中在顶层vendor里面。就像下面这样:

d/
    vendor/   
            q/
            p/
            … …
    mypkg1
            main.go
    mypkg2
            main.go
    … …

另外Go vendor不支持第三方包的版本管理,没有类似godep的Godeps.json这样的存储包元信息的文件。不过目前已经有第三方的vendor specs放在了github上,之前Go team的Brad Fizpatrick也在Golang-dev上征集过类似的方案,不知未来vendor是否会支持。

六、vendor vs. internal

在golang-dev有人提到:有了vendor,internal似乎没用了。这显然是混淆了internal和vendor所要解决的问题。

internal故名思议:内部包,不是对所有源文件都可见的。vendor是存储和管理外部依赖包,更类似于external,里面的包都是copy自 外部的,工程内所有源文件均可import vendor中的包。另外internal在1.4版本中已经加入到go核心,是不可能轻易去除的,虽然到目前为止我们还没能亲自体会到internal 包的作用。

在《Go 1.5中值得关注的几个变化》一文中我提到过go 1.5 beta1似乎“不支持”internal,beta3发布后,我又试了试看beta3是否支持internal包。

结果是beta3中,build依旧不报错。但go list -json会提示错误:
"DepsErrors": [
        {
            "ImportStack": [
                "otherpkg",
                "mypkg/internal/foo"
            ],
            "Pos": "",
            "Err": "use of internal package not allowed"
        }
    ]

难道真的要到最终go 1.5版本才会让internal包发挥作用?

制作go-talks.appspot.com应用镜像

No Comments

Go语言号称面向工程:对工程目录组织、代码风格(gofmt)、文档(生成)都制定的相应的“标准”,并提供了相应的工具帮助开发者满足这些工程specs。

gofmt用于格式化代码,形成统一代码风格。
godoc.org用于查看标准库或repo的doc。
go-talks.appspot.com则是用来查看go slide。

godocgo-talks这种以服务形式提供文档查看的形式不得不说是golang的又一创新。

这几年Golang的开发者们是非常勤奋的,为了推广Golang,他们撰写博客,编写文档,并四处布道,积累下许多有价值的文档,这些文档多以 Gopher所特有的present格式存在着,这些 present格式的文档以.slide、.article或.ext为后缀,通过go-talks.appspot.com提供的present渲染服 务浏览,并且支持github.com repo中的slide文件。Go开发者们只需要将自己写好的slide文件存放在自己github.com上的repo中,就可以随时随地在世界各地打 开这类present文件为大家布道了。

不过来到中国大陆后,事情就没那么顺利了,因为appspot.com在大陆是无法直接访问的,你懂得哦。为了观看这些大牛的slide,内地的Go程序员只能四处寻找出(fan)国(qiang)工具,但这毕竟不是十分方便。

上周末@开发者头条分享了“why Go is fast? [Slide] High performance servers without the event loop (Golang)”这个Dave Cheney在O'Reilly OSCON上分享的Go slide,但因为链接被qiang,无法直接观看。于是就想到能不能制作一个go-talks.appspot.com的镜像站点,让国内Go程序员也 能享受些福利呢?于是乎我就开始了镜像制作的探索过程。

一、在本地搭建go-talks.appspot.com镜像

present格式类似于markup,是一种标记语言,只是present格式更多用来制作slide。

golang.org/x/tools/present提供了present文件格式的解析库,最初本以为需要从头开始写server,并利用 present库解析,写模板和javascript实现类似翻页等功能呢。但后来居然在gddo repo,也就是godoc.org的源码工程中找到了go-talks.appsport.com站点的源码: talksapp。

不过talksapp是运行在google app engine上的应用,要将其直接运行在standalone server上是否可行呢?是否需要改造?这些都是未知数,不过有了源码自然是很好的。我们先来试试这个程序是否能在本地运行起来。

首先下载gddo repo:

$go get github.com/golang/gddo/
$cd $GOPATH/src/github.com/golang/gddo/talksapp

talksapp的主页文档似乎有些out-dated,我并没有找到config.go.template。   

但按照文档要求,需要下载Go App Engine SDK,这个需要搭梯子。在https://cloud.google.com/appengine/downloads#Google_App_Engine_SDK_for_Go页面根据您的平台版本下载最新Go SDK版本。解压后,先放在那里不动。

根据talksapp文档,第三步就应该是sh setup.sh。setup.sh中get两个repo均在qiang外,需要梯子才能下载。

setup.sh正确执行之后,我们用go_appengine下dev_appserver.py来运行talksapp:

$dev_appserver.py ~/Test/GoToolsProjects/src/github.com/golang/gddo/talksapp
INFO     2015-07-27 08:25:09,076 api_server.py:205] Starting API server at: http://localhost:51801
INFO     2015-07-27 08:25:09,080 dispatcher.py:197] Starting module "default" running at: http://localhost:8080
INFO     2015-07-27 08:25:09,083 admin_server.py:118] Starting admin server at: http://localhost:8000
/Users/tony/Test/GoToolsProjects/src/appengine/google/appengine/tools/devappserver2/mtime_file_watcher.py:115: UserWarning: There are too many files in your application for changes in all of them to be monitored. You may have to restart the development server to see some changes to your files.
  'There are too many files in your application for '
ERROR    2015-07-27 08:25:11,941 http_runtime.py:380] bad runtime process port ['']
2015/07/27 08:25:11 secret.json needs to define ClientID and ClientSecret

使用浏览器访问localhost:8080,得到的页面中也只是有些错误日志,日志与上面最后两行相同。从错误日志来看,似乎需要配置一下secret.json这个文件,至少ClientID和ClientSecret不能为空。

我就随意配置两个值(这两个值似乎应该是github.com的账号和密码,用于OAuth2,如果随意配置无法成功,那建议配置上真实的账号和密码),看看是否可以访问:

{
    "ClientID": "xx",
    "ClientSecret": "yy"
}

这回再执行talksapp就不再报错了。用浏览器访问localhost:8080, go-talks的页面顺利正常显示出来!看来在本地是可以运行的哦!

我们再来测试一下访问github.com上的一个slide,地址如下:

http://localhost:8080/github.com/gophercon/2015-talks/Dmitry_Vyukov_-_Go_Dynamic_Tools/tools.slide

加载有些慢,有些时候提示:
  
   canceled: Deadline exceeded (timeout)

试了几次后,居然加载成功了!又试了几个slide,除了有些慢,都是成功的。看来talksapp是可以在standalone主机上运行的。

二、在vps上部署go-talks镜像

虽然在本机上可以正常浏览Golang大牛们的slide的了,但毕竟放在local上不是很方便,离开这台机器又无法访问了。广大内地go程序员们依旧 生活在“水深火热”中,在“分享经济”兴起的今天,我想也力所能及的做些贡献吧。于是想到了将这个镜像部署到我的blog vps上,这样大家就可以自由浏览golang slide了。

我的vps放在了DigitalOcean上(Ubuntu 14.04 server amd64),配置较低,平时仅仅作为blog托管主机。不过放一个go-talks镜像应该还是可以满足的,也可以更充分“压榨”一下DO的资源。

于是乎,我就按照上面的步骤将talksapp安装在了vps上。考虑到talksapp作为一个守护进程,又安装了supervisor对其进行管理:

/etc/supervisor/conf.d/go-talks.conf
[program:go-talks]
environment=GOROOT=/root/.bin/go142
environment=GOPATH=/root/go-talks
directory=/root/go-talks/src/github.com/golang/gddo/talksapp
command=/root/go-talks/go_appengine/goapp serve
autostart=true
autorestart=true
startsecs=3

这里没有使用dev_appserver.py,而是用了两位一个程序goapp,通过在talksapp目录下执行goapp serve来启动这个"GAE"服务。现在vps上启动了localhost:8080服务,但外面的人还是无法访问到这个服务。

如果要对外发布这个服务,我需要一个域名,考虑到自己已有的blog域名,为了快速开通服务,我添加了一个二级域名:go-talks.tonybai.com,模仿go-talks.appspot.com。

我们还需要调整一下apache2 server。原先的apache2 server只是为blog(wordpress)提供服务,现在我们需要将go-talks.tonybai.com映射到主机内部的8080端口服务 上,这就需要开启apache2的反向代理功能,对apache2也不是很熟悉,于是在网上找到了一段配置,补充到/etc/apache2 /apache2.conf中:

<VirtualHost *:80>
    ServerName go-talks.tonybai.com
    ProxyPreserveHost On
    ProxyRequests Off
    ProxyPass / http://localhost:8080/
    ProxyPassReverse / http://localhost:8080/
</VirtualHost>

Include /etc/phpmyadmin/apache.conf

重启apache2,出现下面错误:

root@tonybai:/etc/apache2# sudo service apache2 restart
 * Restarting web server apache2          [fail]
 * The apache2 configtest failed.
Output of config test was:
AH00526: Syntax error on line 85 of /etc/apache2/apache2.conf:
Invalid command 'ProxyPreserveHost', perhaps misspelled or defined by a module not included in the server configuration
Action 'configtest' failed.
The Apache error log may have more information.

似乎是反向代理需要更多apache2 module才能运行,于是:

sudo a2enmod proxy
sudo a2enmod proxy_http

再重启apache2,这回ok了。

在DNS服务商内已经添加了go-talks.tonybai.com这个域名,但由于国内DNS生效时间较慢,为了测试服务是否ok,我修改了 hosts文件,手动将go-talks.tonybai.com指向vps的公网地址。接下来访问go-talks.tonybai.com这个地址, 镜像制作成功了! 又测试了几个slide,均正确生成!速度稍慢,那是因为vps的一般延迟都在2600ms左右。

我的VPS性能不高,大家访问时也许会感觉较慢,但有胜于无!

最后再重申一下go-talks.tonybai.com的使用方法:

如果某个分享链接为:go-talks.appspot.com/xxx/yy/zz/foo.slide,那么将该地址替换为:go- talks.tonybai.com/xxx/yy/zz/foo.slide即可。也就是将appspot换成tonybai,其他不变。

该服务已经利用监控宝监控起来了,如果出现问题(比如网络或资源不足的问题),我会及时处理。但这里不保证100%可用哦!希望大家友好使用,不要拍砖!

Older Entries