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()
} conn, err := net.ListenUDP("udp", addr)
if err != nil {
fmt.Println("net.ListenUDP fail.", err)
os.Exit()
}
defer conn.Close() for {
buf := make([]byte, )
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, )
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()
} conn, err := net.ListenUDP("udp", addr)
if err != nil {
fmt.Println("net.ListenUDP fail.", err)
os.Exit()
}
defer conn.Close() reqChan := make(chan *Request, )
go connHandler(reqChan) var seq int =
for {
buf := make([]byte, )
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, )
reqChan <- &Request{false, seq, []byte(strconv.Itoa(seq)), rspChan}
select {
case rsp := <-rspChan:
fmt.Println("recv rsp. rsp=%v", string(rsp))
case <-time.After( * 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()
} conn, err := net.DialUDP("udp", nil, addr)
if err != nil {
fmt.Println("net.DialUDP fail.", err)
os.Exit()
}
defer conn.Close() sendChan := make(chan []byte, )
go sendHandler(conn, sendChan) recvChan := make(chan []byte, )
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, )
rlen, err := conn.Read(buf)
if err != nil || rlen <= {
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,主要是很多看似繁琐但是不容忽视的很多点本文并未涵盖:配置读取、信号处理、日志记录等,这些就留给有心的读者继续探索了!
Golang后台开发初体验的更多相关文章
- Node.js后台开发初体验
Node.js是什么 Node.js是一个Javascript运行环境(runtime),发布于2009年5月,由Ryan Dahl开发,实质时对Chrome V8引擎进行了封装 Node.js安装 ...
- #使用abp框架与vue一步一步写我是月老的小工具(2) 后台搭建初体验
#使用abp框架与vue一步一步写我是月老的小工具(2) 后台搭建初体验 一.续上前言 关于这个小玩意的产品思考,假设我暂时把他叫我是月老热心人 这是一个没有中心的关系链,每个人进入以后都是以自己为中 ...
- Xamarin.iOS开发初体验
aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKwAAAA+CAIAAAA5/WfHAAAJrklEQVR4nO2c/VdTRxrH+wfdU84pW0
- Microsoft IoT Starter Kit 开发初体验-反馈控制与数据存储
在上一篇文章<Microsoft IoT Starter Kit 开发初体验>中,讲述了微软中国发布的Microsoft IoT Starter Kit所包含的硬件介绍.开发环境搭建.硬件 ...
- IOS开发初体验
IOS开发初体验 搭建开发环境 不多说什么了,开发环境的搭建太简单了,上App Store搜索XCode下载就行了,说多了都是眼泪 创建第一个IOS项目--HolleIOS 创建工程 选择工程创建位置 ...
- 中文代码示例之NW.js桌面应用开发初体验
先看到了NW.js(应该是前身node-webkit的缩写? 觉得该起个更讲究的名字, 如果是NorthWest之意的话, logo(见下)里的指南针好像也没指着西北啊)和Electron的比较文章: ...
- Online开发初体验——Jeecg-Boot 在线配置图表
Online开发——初体验(在线配置图表) 01 通过JSON数据,快速配置图形报表 02 通过SQL数据,快速配置图形报表 03 图表模板配置,实现不同数据源图表合并展示 04 图表布局,支持单排. ...
- Apache Beam入门及Java SDK开发初体验
1 什么是Apache Beam Apache Beam是一个开源的统一的大数据编程模型,它本身并不提供执行引擎,而是支持各种平台如GCP Dataflow.Spark.Flink等.通过Apache ...
- ThinkPHP -- 开发初体验及其几个配置文件的介绍
ThinkPHP是一款不错的轻量级的PHP+MVC框架,它吸取了Ruby On Rails的特性,不仅将Model.View.Controller分开,而且实现了ORM.模板标签等高级特性. 开 ...
随机推荐
- javascript之反柯里化(uncurrying)
在JavaScript中,当我们调用对象的某个方法时,其实不用去关心该对象原本是否被设计为拥有这个方法,这是动态类型语言的特点.可以通过反柯里化(uncurrying)函数实现,让一个对象去借用一个原 ...
- CentOS下安装docker,docker-compose
1.查看系统发行版本: lsb_release -a 2.安装docker:Docker 是一个开源的应用容器引擎,可以让开发者打包他们的应用以及依赖包到一个轻量级.可移植的容器中,然后发布到任何流行 ...
- vue2 过渡动画
<body> <div id="app"> <transition name="move"> // transition里面 ...
- ubuntu:在ubuntu上安装vmware12
在ubuntu上安装vmware12 下载vmware12 https://pan.baidu.com/s/1i5BQEmL 官方的 密匙 5A02H-AU243-TZJ49-GTC7K-3C ...
- chrome 49 版本bug: flex父元素设置flex:1 , 子元素用height:100%无法充满父元素
1 <div class="container"> <div class="item"> <div class="ite ...
- 《DSP using MATLAB》示例 Example 9.11
代码: %% ------------------------------------------------------------------------ %% Output Info about ...
- C#典型案例及分析
1.简单工厂计算器
- StreamSets 部署 Pipelines 到 SDC Edge
可以使用如下方法: 下载edge 运行包并包含pipeline定义文件. 直接发布到edge 设备. 在data colelctor 机器配置并配置了edge server 地址(主要需要网络可访问) ...
- 转 HTTP.SYS 详解
http.sys 是一个位于Win2003和WinXP SP2中的操作系统核心组件, 能够让任何应用程序通过它提供的接口,以http协议进行信息通讯. 温馨提示:如果用户不慎删除了该驱动文件,不用担心 ...
- 适合Centos Web服务器的iptables规则
适合Centos Web服务器的iptables规则IPT="/sbin/iptables"$IPT --delete-chain$IPT --flush$IPT -P INPUT ...