Golang后台开发初体验

转自:http://blog.csdn.net/cszhouwei/article/details/37740277

补充反馈

slice

既然聊到slice,就不得不提它的近亲array,这里不太想提值类型和引用类型的概念(个人觉得其实都是值类型),golang的array其实可以假想为C的struct类型,只是struct通过变量名来访问成员(如xxx.yyy),而array通过下标来访问成员(如xxx[3]),具体内存布局如下图所示:

图 1 golang的array内存布局

显然golang的array灵活性比较差,长度固定,这才有了slice,概念上有点类似于STL的vector,但是具体实现上还是有差距的,具体内存布局如下图所示:

图 2 golang的slice内存布局

slice类型存在len和cap的概念(与vector类似),这里有一点需要澄清:与vector不一样,slice的len并不能无限增长,cap就是它的天花板。比如s := make([]byte, 3, 5),后续不管s如何增长,它的len也不能超过5。

s:= make([]byte, 5)

s= s[2:4]

图 3 s[2:4]内存布局

从上图不难看出,其实slice操作并没有任何内存拷贝动作,仅仅是生成一份新的描述数据(len=2 cap=3);此时,如果执行s = s[:cap(s)],可以将s的len扩张到最大,如下图所示:

图 4 s[:cap(s)]内存布局

在golang里面,如果slice需要扩张到超出cap,只能创建新的slice,然后将现有数据copy过去,再指向新的slice,一般可以借助内置的append函数。

顺带一提,由于slice操作之后,新的对象存在指针指向真实的数据块内存,所以某些场景下,可能会导致大块内存无法被GC回收。

performance

不少tx都提到了深深的性能担忧,其实我本来并不喜欢过于纠结性能问题,毕竟追求极致的单机性能往往意义不大,不过既然提到了,我也上网找了点数据,供有兴趣的读者参考:

图 5 Go vs C (x64 quad-core)

图 6 go vs Java (x64 quad-core)

图 7 Go vs PHP (x64 quad-core)

这里仅列出golang和c、java、php的简单对比,详细的代码和数据大家可以登录http://benchmarksgame.alioth.debian.org/自行查看

garbage collection

(待续)

—————————————————————————————————————————————————————————————————

前言

犹记得去年靠着微信后台的强势宣传,coroutine在我司的C/C++后台界着实火了一把,当时我也顺势对中心的后台网络框架做了coroutine化改造,详见《当C/C++后台开发遇上Coroutine》,当时在文末我也提到了该实现的一些局限性,包括但不限于:

1.       所有的coroutine运行于同一线程空间,无法真正发挥CPU的多核性能

2.       非抢占式调度模式,单个coroutine的阻塞将导致整个server失去响应

与此同时,出身名门的Golang在国内技术圈已经声名鹊起,不乏许世伟等圈内大牛鼓吹其设计之优雅、简洁。本文并不会展开叙述Golang的语言细节,有兴趣的读者可以参阅官方文档(http://www.golang.org),自备梯子,你懂的!

并发与分布式

后台开发的嘴里,估计重复最多的字眼就是“并发”“分布式”云云,那么自我定位“互联网时代的C语言”的Golang又是如何处理的呢?

1.       并发执行的“执行体”:进程、线程、协程 …

多数语言在语法层面并不直接支持coroutine,而通过库的方式支持,正如上文所言,如果在这样的coroutine中调用同步IO操作,比如网络通信、文件读写,都会阻塞其它的并发执行coroutine。Golang在语言级别支持coroutine(goroutine),golang标准库提供的所有系统调用(包括同步IO操作),都会主动出让CPU给其它的goroutine,cool!

2.       执行体间的“通信”:同步/互斥、消息传递 …

并发编程模型主要有两个流派:“共享内存”和“消息传递”,我司不用说,显然是“共享内存”模型的铁杆粉丝。Erlang属于“消息传递”模型的代表,“消息乃进程间通信的唯一方式”。Golang同时支持这两种模型,但是推荐使用后者,即goroutine之间通过channel进行交互。Golang圈内流行一句话:“Don‘t communicate by sharing memory; share memory by communicating.”,大家感受一下。

综上所述,Golang的并发编程可以简单表述为:concurrency = goroutine + channel。关于并发,这里多提一句,“并发”不等于“并行”,这一点对于理解goroutine的并发执行还是挺关键的,推荐阅读《Concurrencyis not parallelism》。

业务场景

其实绝大多数后台Server的业务场景非常简单,基本可以描述为:某逻辑层Server收到前端请求REQ后,需要综合其它N个Server的信息,此时该Server有两种选择:

1.      串行处理

1)    Send request to ServerX andwait for response from ServerX

2)    Send request to ServerY andwait for response from ServerY

3)    Send request to ServerZ andwait for response from ServerZ

4)    Send response to Client

2.      并行处理

1)    Send request to ServerX、ServerY、ServerZ allat once

2)    Wait for all responses and sendresponse to Client

注:为了简化后续讨论,这里我们假设所有前端请求之间相互独立,Per-Request-Per-Goroutine,不考虑Goroutine之间的复杂交互。

代码示例

朴素思路

Per-Request-Per-Goroutine,对于写惯异步Server的苦逼开发,想想都令人激动,大脑再也不用频繁切换于各种上下文,再也不用纠结复杂的状态机跳转,一切都显得如此自然。

package main  

import (
    "fmt"
    "net"
    "os"
)  

func main() {
    addr, err := net.ResolveUDPAddr("udp", ":6000")
    if err != nil {
        fmt.Println("net.ResolveUDPAddr fail.", err)
        os.Exit(1)
    }  

    conn, err := net.ListenUDP("udp", addr)
    if err != nil {
        fmt.Println("net.ListenUDP fail.", err)
        os.Exit(1)
    }
    defer conn.Close()  

    for {
        buf := make([]byte, 65535)
        rlen, remote, err := conn.ReadFromUDP(buf)
        if err != nil {
            fmt.Println("conn.ReadFromUDP fail.", err)
            continue
        }
        go handleConnection(conn, remote, buf[:rlen])
    }
}  

func handleConnection(conn *net.UDPConn, remote *net.UDPAddr, msg []byte) {
    service_addr, err := net.ResolveUDPAddr("udp", ":6001")
    if err != nil {
        fmt.Println("net.ResolveUDPAddr fail.", err)
        return
    }  

    service_conn, err := net.DialUDP("udp", nil, service_addr)
    if err != nil {
        fmt.Println("net.DialUDP fail.", err)
        return
    }
    defer service_conn.Close()  

    _, err = service_conn.Write([]byte("request servcie x"))
    if err != nil {
        fmt.Println("service_conn.Write fail.", err)
        return
    }  

    buf := make([]byte, 65535)
    rlen, err := service_conn.Read(buf)
    if err != nil {
        fmt.Println("service_conn.Read fail.", err)
        return
    }  

    conn.WriteToUDP(buf[:rlen], remote)
}

其实这个最朴素思路下的Server在绝大多数情况下都可以正常工作,而且运行良好,但是不难看出存在以下问题:

1.       延时(Latency):Server与后端Service之间采用短链接通信,对于UDP类无连接方式影响不大,但是对于TCP类有连接方式,开销还是比较客观的,增加了请求的响应延时

2.       并发(Concurrency):16位的端口号数量有限,如果每次后端交互都需要新建连接,理论上来说,同时请求后端Service的Goroutine数量无法超过65535这个硬性限制,在如今这个动辄“十万”“百万”高并发时代,最高6w并发貌似不太拿得出手

改进思路

使用过多线程并发模型的tx应该已经注意到,这两个问题在多线程模型中同样存在,只是不如golang如此突出:创建的线程数量一般是受控的,不会达到端口上限,但是goer显然不能满足于这个量级的并发度。

解决方法也很简单,既然短连接存在诸多弊端,使用长连接呗。那我们该如何利用golang提供的语言设施来具体实现呢?既然通信连接比较棘手,干脆抽取出独立的通信代理(conn-proxy),代理本身处理所有的网络通信细节(连接管理,数据收发等),具体的process-goroutine通过channel与communication-proxy进行交互(提交请求,等待响应等),如下图所示:

package main  

import (
    "fmt"
    "net"
    "os"
    "strconv"
    "time"
)  

type Request struct {
    isCancel bool
    reqSeq   int
    reqPkg   []byte
    rspChan  chan<- []byte
}  

func main() {
    addr, err := net.ResolveUDPAddr("udp", ":6000")
    if err != nil {
        fmt.Println("net.ResolveUDPAddr fail.", err)
        os.Exit(1)
    }  

    conn, err := net.ListenUDP("udp", addr)
    if err != nil {
        fmt.Println("net.ListenUDP fail.", err)
        os.Exit(1)
    }
    defer conn.Close()  

    reqChan := make(chan *Request, 1000)
    go connHandler(reqChan)  

    var seq int = 0
    for {
        buf := make([]byte, 1024)
        rlen, remote, err := conn.ReadFromUDP(buf)
        if err != nil {
            fmt.Println("conn.ReadFromUDP fail.", err)
            continue
        }
        seq++
        go processHandler(conn, remote, buf[:rlen], reqChan, seq)
    }
}  

func processHandler(conn *net.UDPConn, remote *net.UDPAddr, msg []byte, reqChan chan<- *Request, seq int) {
    rspChan := make(chan []byte, 1)
    reqChan <- &Request{false, seq, []byte(strconv.Itoa(seq)), rspChan}
    select {
    case rsp := <-rspChan:
        fmt.Println("recv rsp. rsp=%v", string(rsp))
    case <-time.After(2 * time.Second):
        fmt.Println("wait for rsp timeout.")
        reqChan <- &Request{isCancel: true, reqSeq: seq}
        conn.WriteToUDP([]byte("wait for rsp timeout."), remote)
        return
    }  

    conn.WriteToUDP([]byte("all process succ."), remote)
}  

func connHandler(reqChan <-chan *Request) {
    addr, err := net.ResolveUDPAddr("udp", ":6001")
    if err != nil {
        fmt.Println("net.ResolveUDPAddr fail.", err)
        os.Exit(1)
    }  

    conn, err := net.DialUDP("udp", nil, addr)
    if err != nil {
        fmt.Println("net.DialUDP fail.", err)
        os.Exit(1)
    }
    defer conn.Close()  

    sendChan := make(chan []byte, 1000)
    go sendHandler(conn, sendChan)  

    recvChan := make(chan []byte, 1000)
    go recvHandler(conn, recvChan)  

    reqMap := make(map[int]*Request)
    for {
        select {
        case req := <-reqChan:
            if req.isCancel {
                delete(reqMap, req.reqSeq)
                fmt.Println("CancelRequest recv. reqSeq=%v", req.reqSeq)
                continue
            }
            reqMap[req.reqSeq] = req
            sendChan <- req.reqPkg
            fmt.Println("NormalRequest recv. reqSeq=%d reqPkg=%s", req.reqSeq, string(req.reqPkg))
        case rsp := <-recvChan:
            seq, err := strconv.Atoi(string(rsp))
            if err != nil {
                fmt.Println("strconv.Atoi fail. err=%v", err)
                continue
            }
            req, ok := reqMap[seq]
            if !ok {
                fmt.Println("seq not found. seq=%v", seq)
                continue
            }
            req.rspChan <- rsp
            fmt.Println("send rsp to client. rsp=%v", string(rsp))
            delete(reqMap, req.reqSeq)
        }
    }
}  

func sendHandler(conn *net.UDPConn, sendChan <-chan []byte) {
    for data := range sendChan {
        wlen, err := conn.Write(data)
        if err != nil || wlen != len(data) {
            fmt.Println("conn.Write fail.", err)
            continue
        }
        fmt.Println("conn.Write succ. data=%v", string(data))
    }
}  

func recvHandler(conn *net.UDPConn, recvChan chan<- []byte) {
    for {
        buf := make([]byte, 1024)
        rlen, err := conn.Read(buf)
        if err != nil || rlen <= 0 {
            fmt.Println(err)
            continue
        }
        fmt.Println("conn.Read succ. data=%v", string(buf))
        recvChan <- buf[:rlen]
    }
}  

继续进化

上述版本的Communication-Proxy只能算toy实现,实际生产环境中,后端Service往往会提供一些独特的接入方式(如我司的CMLB、L5、多IP等),此时,Communication-Proxy需要实现诸如“负载均衡”“容灾切换”等功能,涉及具体接入场景,这里不再一一赘述。通过上面的例子,相信大家很容易借助goroutine+channel进行相应建模。

小结

本文对于golang如何实现一般后台业务Server进行了简单介绍,基于goroutine和channel实现了toy_server,之所以将其定位于toy,主要是很多看似繁琐但是不容忽视的很多点本文并未涵盖:配置读取、信号处理、日志记录等,这些就留给有心的读者继续探索了!

时间: 2024-10-10 14:52:19

Golang后台开发初体验的相关文章

Xamarin.iOS开发初体验

Xamarin是一个跨平台开发框架,这一框架的特点是支持用C#开发IOS.Android.Windows Phone和Mac应用,这套框架底层是用Mono实现的. Mono是一款基于.NET框架的开源工程,包含C#语言编译器.CLR运行时和一组类库,能运行于Windows.Linux.Unix.Mac OS和Solaris.对于.NET程序员来说,Xamarin是走向安卓.iOS.Mac跨平台开发的神器,不仅能用熟悉的C#来开发,还能使用Visual Studio作为IDE.本文内容是Xamar

Microsoft IoT Starter Kit 开发初体验-反馈控制与数据存储

在上一篇文章<Microsoft IoT Starter Kit 开发初体验>中,讲述了微软中国发布的Microsoft IoT Starter Kit所包含的硬件介绍.开发环境搭建.硬件设置.Azure IoT Hub的连接.程序的编译.下载和调试.PowerBI数据的展现.在这篇文章中,将会详细讲述Cloud to Device的消息反馈控制以及如何通过Stream Analytics将数据存储到Azure Storage Table,以方便数据后期的利用. 1. 反馈控制 上一篇文章中,

程序开发初体验

程序开发初体验 一.预估与实际 PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟) Planning 计划 20 10 ? Estimate ? 估计这个任务需要多少时间 20 10 Development 开发 370 380 ? Analysis ? 需求分析 (包括学习新技术) 30 20 ? Design Spec ? 生成设计文档 60 20 ? Design Review ? 设计复审 10 10 ? Coding St

linux 驱动模块开发初体验

2020-02-09 关键字: 在嵌入式 Linux 开发中,驱动程序通常都是用 C语言 来编写的,并经编译后生成为目标文件,即 '.o' 文件.随后又可在编译系统时以两种形式打包成系统镜像文件: 1.uImage 即内核的二进制文件.这种形式是直接将内核驱动程序打包进系统文件中.这种形式的驱动程序将会在内核加载时运行,即随系统启动而运行.这种形式的驱动预置在一定程度上会影响系统的开机耗时. 2.ko 文件 即 kernel object,这种形式是将驱动程序以独立的模块文件存在于系统中.这种形

Microsoft IoT Starter Kit 开发初体验

1. 引子 今年6月底,在上海举办的中国国际物联网大会上,微软中国面向中国物联网社区推出了Microsoft IoT Starter Kit ,并且免费开放1000套的申请.申请地址为:http://aka.ms/iotkits,目前仍然有效.当时一开放申请,我就在线填写了申请表,接下来就是长长的等待.相信很多朋友都是一样,在经过几个月的等待之后,终于拿到了这个开发套件,而有些朋友估计还在等待中.因为官方是一个月处理并邮寄一批,速度不是很快.但是,在经过了一段时间使用以后,我可以说,如果朋友们期

前端开发初体验

决定成为一名优秀的前端工程师,已经有三个月时间了.在这三个月的时间里我零零散散的看着书,并没有自己做一个项目,工作中我主要负责服务器端的代码,所以基本上很少接触前端代码,只是用一些JQuery和html,都是很简单的实现.所以对前端开发并没有切身的体验. 在大学期间,自己做项目时,前段后台都是自己写,感觉前端没有难度,就是写html,css布局嘛,偶尔用js实现一下提示信息的显示什么的.主要精力都是放在Java开发上.可是工作之后感觉其实前端并没有那么简单,前端直接决定用户的体验,甚至影响用户是

Adobe Html5 Extension开发初体验

一.背景介绍 Adobe公司出品的多媒体处理软件产品线较多,涵盖了音视频编辑.图像处理.平面设计.影视后期等领域.为了扩展软件的功能,Adobe公司为开发者提供了两种方式来增加软件的功能:分别是插件(Plugin)和扩展(Extension).去年利用官方提供的SDK开发过两款Premiere插件,分别用于导入自定义格式的多媒体文件和视频流预览.近来体验了一下Adobe Extension的开发. Adobe Plugin一般用于提供更靠近底层的功能.官方出于效率的考虑,提供的插件SDK是基于C

Visual Studio 2015 移动跨平台开发初体验

微软换了新 CEO 后变化很大,对我们团队最有利的消息就是 Visual Studio 2015 支持移动应用跨平台开发. 还记不记得很早之前,Xamarin 宣布与微软成为合作伙伴的消息.显然,Xamarin 得到了来自微软的大力支持,而微软则直接将 Xamain 融合进 Visual Studio 2015,以扭转它在移动领域的颓势. 也许你还担心这里面是否有大坑,是否还不够成熟,我现在还无法回答你,不过我相信微软和 Xamarin 会很快解决这些问题,尤其是微软,它有足够的动机去让 Vis

Hybird App ( 混合模式移动应用)开发初体验

最近1,2个月一直都尝试开发一款hybird app,遇到了很多问题,谈谈自己的体会. Hybird app (混合模式移动应用),它利用例如安卓端webview组件+HTML5内嵌的方式混合的方式开发的移动应用, 好处显而易见,由于内嵌的是Html5, 所以跨平台,扩展性,开发成本都是很不错的优势. Hybird App拥有很多从开发工具到打包发布的解决方案,比较出名的是来自Adobe的phonegap, 国内有AppCan,这2种解决方案都有比较好的工具平台.这次我采用的的是Appcan,