Golang後台開發初體驗

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

補充反饋

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 mainimport ("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 mainimport ("fmt""net""os""strconv""time")type Request struct {isCancel boolreqSeq   intreqPkg   []byterspChan  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 = 0for {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] = reqsendChan <- req.reqPkgfmt.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 <- rspfmt.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,主要是很多看似繁瑣但是不容忽視的很多點本文並未涵蓋:配置讀取、訊號處理、日誌記錄等,這些就留給有心的讀者繼續探索了!

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.