Golang后台开发初体验

原创 2014年07月13日 16:40:00

补充反馈

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,主要是很多看似繁琐但是不容忽视的很多点本文并未涵盖:配置读取、信号处理、日志记录等,这些就留给有心的读者继续探索了!

相关文章推荐

Go语言TCP网络编程(详细)

一、序言 Golang的主要 设计目标之一就是面向大规模后端服务程序,网络通信这块是服务端 程序必不可少也是至关重要的一部分。在日常应用中,我们也可以看到Go中的net以及其subdirectori...

go 语言之ReadFromUDP使用

今日阅读GORTP代码的时候,发现其代码中收取UDP网络数据后总是可以认为是一个RTP包,而我一开始认为该代码逻辑存在问题,即如果对方发送的是半个RTP包,那不就存在问题?当经过修改代码验证,当接受数...

第十四章进程间的通信

第十四章进程间的通信 管道的概念 管道是Linux / UNIX 系列中比较原始的进程间通信方式,他实现数据以一种数据流的方式在进程间流动。在系统中其相当于文件系统上的一个文件,来缓存所要...
  • laohan_
  • laohan_
  • 2013年03月22日 10:40
  • 2039

go后台和web编程

go的应用场景更多情况下应用于后台,也可以用于开发web。后台和web都是用到net和net/http模块,而开发web则还需要html/template模块。当然了实际使用中html/templat...

我为什么放弃Go语言

我为什么放弃Go语言?有好几次,当我想起来的时候,总是会问自己:这个决定是正确的吗?是明智和理性的吗?其实我一直在认真思考这个问题。开门见山地说,我当初放弃Go语言,就是因为两个“不爽”:第一,对Go...
  • liigo
  • liigo
  • 2014年04月14日 19:24
  • 329694

[Golang] 从零开始写Socket Server(1): Socket-Client框架

因为发现Golang这一块资料挺少的,接下来我会在Blog里把整个Server的Coding,还有遇到的坑都记录下来~ 这一章主要讲最基础的部分,即如何使用go语言code出一个使用Socket通信的...
  • ahlxt123
  • ahlxt123
  • 2015年08月06日 18:03
  • 14838

使用Golang 搭建http web服务器

这篇文章出现的理由是业务上需要创建一个Web Server。创建web是所有语言出现必须实现的功能之一了。在nginx+fastcgi+php广为使用的今天,这里我们不妨使用Go来进行web服务器的搭...

几种软负载均衡策略分析

版权声明:本文为Sunface原创文章,请随意转载,若有需要敬请联系CTO@188.com。同时欢迎大家加入Golang隐修会,QQ群894864,大神很多。 公司去年上了F5,好用是好...
  • zhq651
  • zhq651
  • 2016年04月03日 14:37
  • 1448

我在REST API应用网关负载均衡中加权轮循方法的实现

最近项目需要一个REST API应用网关,因此用GO写了一个,并简单地实现了加权轮循算法。 基本思路是初始化时生成一个队列,在每次请求到来,选取backend时,直接从队列里选取,不用实时计算。...
  • rariki
  • rariki
  • 2015年03月21日 19:36
  • 1254

golang并发编程

golang普通方法:package main import ( "fmt" ) func rand_generator_1() int { return rand.Int() } func...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Golang后台开发初体验
举报原因:
原因补充:

(最多只允许输入30个字)