go之道--搭建高性能服务器

搭建一个后台服务器, 有很种方法与模式,c/c++使用epoll, nodejs的V8等都能实现。

今天我们来看看go server的模式。

同以往其他语言底层采用epoll模式略有不同,golang采用了goroutine(协程)+channel的模式。
go之道--搭建高性能服务器

routine: 在线程之上封装的协程,每个都有自己的堆栈。

channel:协程间通信使用的。

创建routine的命令是

go func()

我们来举个例子。

比如创建一个http服务:

func Index(w http.ResponseWriter, r *http.Request){
    log.Println("static:index_request,  ip:", r.RemoteAddr)
    io.WriteString(w, r.URL.Path)
     
}
func GetData(w http.ResponseWriter, r *http.Request){
    log.Println("static:index_request,  ip:", r.RemoteAddr)
    vars := mux.Vars(r)
    id := vars["id"]
    log.Println("get data, id:", id)
    io.WriteString(w, r.URL.Path)
}
 
func createServer() error{
    router := mux.NewRouter().StrictSlash(true)
    router.HandleFunc("/getData/{id}", GetData)
    router.HandleFunc("/", Index)
    http.ListenAndServe(":12345", router)
    return nil
}

router专门负责请求路由。

/请求只是负责打印请求链接。

/getData/{id}是一条查询数据请求。

看看内部对每条新请求的处理逻辑:

func (srv *Server) Serve(l net.Listener) error {
    defer l.Close()
    if fn := testHookServerServe; fn != nil {
        fn(srv, l)
    }
    var tempDelay time.Duration // how long to sleep on accept failure
    if err := srv.setupHTTP2_Serve(); err != nil {
        return err
    }
    // TODO: allow changing base context? can't imagine concrete
    // use cases yet.
    baseCtx := context.Background()
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    ctx = context.WithValue(ctx, LocalAddrContextKey, l.Addr())
    for {
        rw, e := l.Accept()
        if e != nil {
            if ne, ok := e.(net.Error); ok && ne.Temporary() {
                if tempDelay == 0 {
                    tempDelay = 5 * time.Millisecond
                } else {
                    tempDelay *= 2
                }
                if max := 1 * time.Second; tempDelay > max {
                    tempDelay = max
                }
                srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
                time.Sleep(tempDelay)
                continue
            }
            return e
        }
        tempDelay = 0
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve(ctx)
    }
}

可以看到最后一条语句为 go c.serve(ctx) ,意味着每条新链接都会被放进一条新routine里去执行,这样避免了各请求间的阻塞关系。

假设同一条请求有多个阻塞子请求,比如

getData调用2个请求以获得数据:

func GetDataA() (*A, error){
    time.Sleep(time.Second * 1)
    return &A{string("a")}, nil
}
func GetDataB() (*B, error){
    time.Sleep(time.Second * 0.5)
    return &B{string("b")}, nil
}
 
 
 
func GetData(w http.ResponseWriter, r *http.Request){
    log.Println("static:index_request,  ip:", r.RemoteAddr)
    vars := mux.Vars(r)
    id := vars["id"]
    log.Println("get data, id:", id)
    io.WriteString(w, r.URL.Path)
    a, _ := GetDataA()
    b, _ := GetDataB()
    log.Println("a:", a.a, ",b:", b.b)
}

一般情况下,会将GetDataA/GetDataB顺序执行,假设A函数占用1s, B函数占用0.5秒,则GetData调用时间将超过1.5秒。

在go routine下可以很好的解决问题。

func GetDataA(ca *chan A) error{
    time.Sleep(time.Second * 1)
    ca <- A{string("a")}
    return nil
}
func GetDataB(cb *chan B) error{
    time.Sleep(time.Second * 0.5)
    cb <- &B{string("b")}
    return nil
}
func GetData(w http.ResponseWriter, r *http.Request){
    log.Println("static:index_request,  ip:", r.RemoteAddr)
    vars := mux.Vars(r)
    id := vars["id"]
    log.Println("get data, id:", id)
    io.WriteString(w, r.URL.Path)
    var ca chan A
    var cb chan B
    go GetDataA(&ca)
    go GetDataB(&cb)
    a := <- ca
    b := <- cb
    log.Println("a:", a.a, ",b:", b.b)
}

这样GetDataA/GetDataB即可以并行执行,总共时间约1秒即可完成。

上面的 a := <- ca即表示a是接受channel的请求。

整条链路的流程:
go之道--搭建高性能服务器

现实工作中可能会有很多类似串行转并行的过程。

比如前端界现在流行的服务端渲染, 对于多个model数据的请求,采用routine获取,可以将时间长度最大缩短。

备注:

c++与golang高并发模式不同,之前我写过c/c++的高性能服务器文章:http://blog.csdn.net/xiaofei_hah0000/article/details/8743627

上一篇:认真的做羞羞的事 一颗种子的自我分享


下一篇:lnmp一键安装脚本