我為什麼從python轉向go

weixin_34377065發表於2015-05-16

應puppet大拿劉宇的邀請,我去西山居運維團隊做了一個簡短分享,談談為什麼我要將我們的專案從python轉向go。

坦白的講,在一幫python使用者面前講為什麼放棄python轉而用go其實是一件壓力蠻大的事情,語言之爭就跟vim和emacs之爭一樣,是一個永恆的無解話題,稍微不注意就可能導致粉絲強烈地反擊。所以我只會從我們專案實際情況出發,來講講為什麼我最終選擇了go。

為什麼放棄python

首先,我其實得說說為什麼我們會選擇python。在我加入企業快盤團隊之前,整個專案包括更早的金山快盤都是採用python進行開發的。至於為什麼這麼選擇,當時的架構師蔥頭告訴我,主要是因為python上手簡單,開發迅速。對於團隊裡面大部分完全沒服務端開發經驗的同學來說,python真的是一個很好的選擇。

python的簡單高效,我是深有體會的。當時私有云專案也就幾個程式設計師,但是我們要服務多家大型企業,進行定製化的開發,多虧了python,我們才能快速出活。後來企業快盤掛掉之後,我們啟動輕辦公專案,自然也使用python進行了原始版本的構建。

python雖然很強大,但我們在使用的時候也碰到了一些問題,主要由如下幾個方面:

  • 動態語言

    python是一門動態強型別語言。但是,仍然可能出現int + string這樣的執行時錯誤,因為對於一個變數,在寫程式碼的時候,我們有時候很容易就忘記這個變數到底是啥型別的了。

    在python裡面,可以允許同名函式的出現,後一個函式會覆蓋前一個函式,有一次我們系統一個很嚴重的錯誤就是因為這個導致的。

    上面說到的這些,靜態語言在編譯的時候就能幫我們檢測出來,而不需要等到執行時出問題才知道。雖然我們有很完善的測試用例,但總有case遺漏的情況。所以每次出現執行時錯誤,我心裡都想著如果能在編譯的時候就發現該多好。

  • 效能

    其實這個一直是很多人吐槽python的地方,但python有它適合乾的事情,硬是要用python進行一些高效能模組的開發,那也有點難為它了。

    python的GIL導致無法真正的多執行緒,大家可能會說我用多程式不就完了。但如果一些計算需要涉及到多程式互動,程式之間的通訊開銷也是不得不考慮的。

    無狀態的分散式處理使用多程式很方便,譬如處理http請求,我們就是在nginx後面掛載了200多個django server來處理http的,但這麼多個程式自然導致整體機器負載偏高。

    但即使我們使用了多個django程式來處理http請求,對於一些超大量請求,python仍然處理不過來。所以我們使用openresty,將高頻次的http請求使用lua來實現。可這樣又導致使用兩種開發語言,而且一些邏輯還得寫兩份不同的程式碼。

  • 同步網路模型

    django的網路是同步阻塞的,也就是說,如果我們需要訪問外部的一個服務,在等待結果返回這段時間,django不能處理任何其他的邏輯(當然,多執行緒的除外)。如果訪問外部服務需要很長時間,那就意味著我們的整個服務幾乎在很長一段時間完全不可用。

    為了解決這個問題,我們只能不斷的多開django程式,同時需要保證所有服務都能快速的處理響應,但想想這其實是一件很不靠譜的事情。

  • 非同步網路模型

    tornado的網路模型是非同步的,這意味著它不會出現django那樣因為外部服務不可用導致這個服務無法響應的問題。話說,比起django,我可是非常喜歡tornado的,小巧簡單,以前還寫過幾篇深入剖析tornado的文章了。

    雖然tornado是非同步的,但是python的mysql庫都不支援非同步,這也就意味著如果我們在tornado裡面訪問資料庫,我們仍然可能面臨因為資料庫問題造成的整個服務不可用。

    其實非同步模型最大的問題在於程式碼邏輯的割裂,因為是事件觸發的,所以我們都是通過callback進行相關處理,於是程式碼裡面就經常出現幹一件事情,傳一個callback,然後callback裡面又傳callback的情況,這樣的結果就是整個程式碼邏輯非常混亂。

    python沒有原生的協程支援,雖然可以通過gevent,greenlet這種的上patch方式來支援協程,但畢竟更改了python原始碼。另外,python的yield也可以進行簡單的協程模擬,但畢竟不能跨堆疊,侷限性很大,不知道3.x的版本有沒有改進。

  • 開發運維部署

    當我第一次使用python開發專案,我是沒成功安裝上專案需要的包的,光安裝成功mysql庫就弄了很久。後來,是一位同事將他整個python目錄打包給我用,我才能正常的將專案跑起來。話說,現在有了docker,是多麼讓人幸福的一件事情。

    而部署python服務的時候,我們需要在伺服器上面安裝一堆的包,光是這一點就讓人很麻煩,雖然可以通過puppet,salt這些自動化工具解決部署問題,但相比而言,靜態編譯語言只用扔一個二進位制檔案,可就方便太多了。

  • 程式碼失控

    python非常靈活簡單,寫c幾十行程式碼才能搞定的功能,python一行程式碼沒準就能解決。但是太簡單,反而導致很多同學無法對程式碼進行深層次的思考,對整個架構進行細緻的考量。來了一個需求,啪啪啪,鍵盤敲完開速實現,結果就是程式碼越來越混亂,最終導致了整個專案程式碼失控。

    雖然這也有我們自身的原因,譬如沒好的程式碼review機制,沒有好的專案規範,但個人感覺,如果一個程式設計師沒經過良好的編碼訓練,用python很容易就寫出爛的程式碼,因為太自由了。

    當然,我這裡並不是說用python無法進行大型專案的開發,豆瓣,dropbox都是很好的例子,只是在我們專案中,我們的python程式碼失控了。

上面提到的都是我們在實際專案中使用python遇到的問題,雖然最終都解決了,但是讓我愈發的覺得,隨著專案複雜度的增大,流量效能壓力的增大,python並不是一個很好的選擇。

為什麼選擇go

說完了python,現在來說說為什麼我們選擇go。其實除了python,我們也有其他的選擇,java,php,lua(openresty),但最終我們選擇了go。

雖然java和php都是最好的程式語言(大家都這麼爭的),但我更傾向一門更簡單的語言。而openresty,雖然效能強悍,但lua仍然是動態語言,也會碰到前面說的動態語言一些問題。最後,前金山許式偉用的go,前快盤架構師蔥頭也用的go,所以我們很自然地選擇了go。

go並不是完美,一堆值得我們吐槽的地方。

  • error,好吧,如果有語言潔癖的同學可能真的受不了go的語法,尤其是約定的最後一個返回值是error。專案裡面經常會充斥這樣的程式碼:

    if _, err := w.Write(data1); err != nil {
        returun err
    }
    if _, err := w.Write(data2); err != nil {
        returun err
    }
    

    難怪有個梗是對於一個需求,java的程式設計師在寫配置的時候,go程式設計師已經寫了大部分程式碼,但是當java的程式設計師寫完的時候,go程式設計師還在寫err != nil

    這方面,errors-are-values倒是推薦了一個不錯的解決方案。

  • 包管理,go的包管理太弱了,只有一個go get,也就是如果不小心更新了一個外部庫,很有可能就導致現有的程式碼編譯不過了。雖然已經有很多開源方案,譬如godep以及現在才出來的gb等,但畢竟不是官方的。貌似google也是通過vendor機制來管理第三方庫的。希望go 1.5或者之後的版本能好好處理下這個問題。

  • GC,java的GC發展20年了,go才這麼點時間,gc鐵定不完善。所以我們仍然不能隨心所欲的寫程式碼,不然在大請求量下面gc可能會卡頓整個服務。所以有時候,該用物件池,記憶體池的一定要用,雖然程式碼醜了點,但好歹效能上去了。

  • 泛型,雖然go有inteface,但泛型的缺失會讓我們在實現一個功能的時候寫大量的重複程式碼,譬如int32和int64型別的sort,我們得為分別寫兩套程式碼,好冗餘。go 1.4之後有了go generate的支援,但這種的仍然需要自己根據go的AST庫來手動寫相關的parser,難度也挺大的。雖然也有很多開源的generate實現,但畢竟不是官方的。

當然還有很多值得吐槽的地方,就不一一列舉了,但是go仍舊有它的優勢。

  • 靜態語言,強型別。靜態編譯能幫我們檢查出來大量的錯誤,go的強型別甚至變態到不支援隱式的型別轉換。雖然寫程式碼感覺很彆扭,但減少了犯錯的可能。
  • gofmt,應該這是我知道的第一個官方提供統一格式化程式碼工具的語言了。有了gofmt,大家的程式碼長一個樣了,也就沒有花括號到底放到結尾還是新開一行這種蛋疼的程式碼風格討論了。因為大家的程式碼風格一樣,所以看go的程式碼很容易。
  • 天生的並行支援,因為goroutine以及channel,用go寫分散式應用,寫併發程式異常的容易。沒有了蛋疼的callback導致的程式碼邏輯割裂,程式碼邏輯都是順序的。
  • 效能,go的效能可能趕不上c,c++以及openresty,但真的也挺強悍的。在我們的專案中,現在單機就部署了一個go的程式,就完全能夠勝任以前200個python程式乾的事情,而且CPU和MEM佔用更低。
  • 運維部署,直接編譯成二進位制,扔到伺服器上面就成,比python需要安裝一堆的環境那是簡單的太多了。當然,如果有cgo,我們也需要將對應的動態庫給扔過去。
  • 開發效率,雖然go是靜態語言,但我個人感覺開發效率真的挺高,直覺上面跟python不相上下。對於我個人來說,最好的例子就是我用go快速開發了非常多的開源元件,譬如ledisdb,go-mysql等,而這些最開始的版本都是在很短的時間裡面完成的。對於我們專案來說,我們也是用go在一個月就重構完成了第一個版本,併發布。

實際專案中一些Go Tips

到現在為止,我們幾乎所有的服務端專案都已經轉向go,當然在使用的時候也遇到了一些問題,列出來算是經驗分享吧。

  • godep,我們使用godep進行第三方庫管理,但是godep我碰到的最大的坑就是build tag問題,如果一個檔案有build tag,godep很有可能就會忽略這個檔案。
  • IO deadline,如果能自己在應用層處理的都自己處理,go的deadline內部是timer來控制,但timer內部採用一個array來實現的heap,全域性共用一個鎖,如果大併發量,並且timer數量過多,timeout變動太頻繁,很容易就引起效能問題。
  • GC,這個前面也說了,多用記憶體池,物件池,另外,我還發現,如果物件的生命週期跟goroutine一致,對效能的提升也不錯,也在go的group問過相關問題,大家猜測可能是因為一些物件其實是在goroutine的8k棧上面分配的,所以一起回收沒有額外GC了。
  • Go gob,如果要做RPC服務,gob並不是一個很好的選擇,首先就跟python的pickle不通用,然後為了做不同系統的資料傳入,任何包都必須帶上型別的詳細資訊,size太大。go裡面現在還沒一套官方的RPC方案,gRPC貌似有上位的可能。

總結

雖然我現在選擇了go,但是並不表示我以後不會嘗試其他的語言。語言沒有好壞,能幫我解決問題的就是好語言。但至少在很長的一段時間,我都會用go來進行開發。Let' go!!!

相關文章