閱讀505 返回首頁    go 阿裏雲 go 技術社區[雲棲]


記一次從Rails至Golang的接口遷移

背景

我們有部分業務邏輯比較複雜的線上項目是由Rails框架快速開發而來的,但其中的部分API(Restful)代碼需要服務於幾十萬同時在線的物聯網設備。隨著設備量的不斷增加, 對這部分代碼的性能需求就越來越高。 在高峰時段, 業務所在服務器節點經常出現Passenger隊列擁塞的情況, 非常影響服務質量 -- 不僅僅是這個高頻API業務, 而且也會影響其他低頻API的業務。 所以需要把這部分代碼單獨提取出來, 用更高效的方式來實現。

遷移前麵對的問題:

  • 需要拆分的高頻API比較獨立,並且基本是讀數據庫(極少寫)
  • 需要做到無縫遷移, 不能中斷線上業務的運行
  • API訪問了大量的MySQL數據表,Rails的數據模型(Active Record)如何遷移
  • 如何測試 - 測試代碼的遷移,以及線上測試

為何選擇Golang

運行時高效,低內存。擁有活躍的社區,以及非常多的三方開源庫。也考慮過使用Openresty(nginx + lua),運行效率更高。 但相對於Golang來說, Openresty的社區不夠活躍, 也找不到可以快速替換Rails的數據模型的方法,一句一句的拚SQL,開發效率極低,代碼維護也比較困難。

遷移步驟

確定需要使用的開源軟件

這一步非常重要。 如果沒有開源代碼的支撐,什麼都自己實現,要做到快速開發上線,是極不現實的。由於大量開源軟件的存在,當前大部分軟件的開發的前提之一就是評估和測試各種可能要用到的開源軟件。

從我們的要遷移的項目來說, 需要一個HTTP服務框架,數據層方麵需要訪問Redis以及Mysql數據庫。

  • HTTP服務框架 Golang自帶的net/http包已經足夠好,但是最終還是選擇了使用Gin(github.com/gin-gonic/gin),和net/http一樣的輕量高效。從架構上來看,Gin類似於Rails使用的Rack中間件。
  • Redis客戶端 github.com/garyburd/redigo/redis,長久以來一直使用,習慣了。
  • Mysql Driver github.com/go-sql-driver/mysql,也沒什麼可選的。

由於遷移工作量最大的部分在數據模型上麵,所以需要一個數據模型框架(ORM)能夠支撐快速的開發。清單包含了Golang當前比較流行的ORM框架。

在gorm,gorp,upper/db與sqlboiler中,最終選擇了sqlboiler。初步選擇sqlboiler的原因是其文檔中有這麼一句“While attempting to migrate a legacy Rails database, we realized how much ActiveRecord benefitted us in terms of development velocity. Coming over to the Go database/sql package after using ActiveRecord feels extremely repetitive, super long-winded and down-right boring.” 並且sqlboiler的文檔有一份看起來還不錯的benchmark報告。由此可見開源軟件的文檔有多麼重要,絲毫不遜於代碼本身,甚至比代碼更重要,畢竟大部分人是看臉的。

生成數據模型

通過sqlboiler命令行工具可以非常容易的將現有Mysql的數據表轉換為數據模型(通過模板生成訪問數據表的GO代碼),使用命令前需要配置~/.config/sqlboiler/sqlboiler.tom,讓sqlboiler能夠訪問數據庫和數據表。

sqlboiler -w tbl1,tbl2,tbl3,tbl4,tbl5 mysql

該命令生成一個models文件夾, 裏麵包含了訪問tbl1,tbl2,tbl3,tbl4,tbl5這些表的代碼,以及測試代碼。現在我們已經擁有了一個的Mysql數據接入層了。

使用這些生成代碼的風格如下:

db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/example_db?parseTime=true")
if err != nil {
    panic(fmt.Sprintf("can not connect to mysql: %s", err))
}
db.SetMaxOpenConns(5)
db.SetMaxIdleConns(3)
db.SetConnMaxLifetime(3 * time.Minute)

boil.SetDB(db)
users, err := models.UsersG().All()
users, err := models.UsersG(qm.Where("age > ?", 30), qm.Limit(5), qm.Offset(6)).All()
shop, err := models.ShopsG(qm.InnerJoin("router on router.shop_id = shops.id"), qm.Where("router.sn = ?", sn)).One()

更多細節可以參見文檔。

補全數據模型

前麵提到,訪問數據庫的代碼是根據模板生成的,功能很單一。在組合複雜功能的時候需要對模型進行擴展, 其實遷移數據模型大部分的工作量都在這裏。sqlboiler文檔中建議了三種方法。個人比較喜歡第3種風格,示例如下:

package modext

type ShopExt struct {
    M  *models.Shop
    ar *models.AuthenticationResource
    sn string
}

func (s *ShopExt) BusinessHours() (string, string) {
    if s.M == nil || !s.M.BusinessHours.Valid {
        return "", ""
    }

    h := string(s.M.BusinessHours.String)
    hs := strings.Split(h, "-")
    if len(hs) == 2 {
        return hs[0], hs[1]
    }
    return "", ""
}

...

對比下Rails的代碼, 代碼量明顯增加(錯誤處理, 異常處理等), 通常一行Rails代碼,用Golang重寫需要十多行。

class Shop < ActiveRecord::Base
  ...
  def start_business_hours
    business_hours.to_s.split('-')[0].to_s
  end
  ...
end

sqlboiler的缺點

  • 隻有顯式設置外鍵的表,才會生成關聯模型。我們現有Rails數據庫,完全沒有用到外鍵, 關聯查詢基本依靠手動的JOIN和多次查詢,而不能像Rails可以設置belongs_to,has_one,has_many
  • 不支持查詢緩存,如果某些數據在一次請求中需要多次查詢,需要顯式將它的引用緩存起來, 比如上麵例子中的 ar *models.AuthenticationResource,以減少數據庫查詢。
  • 當前不支持在線對數據表做增加列的操作,我們自己打了個patch來解決這個問題。如果要使用這個補丁,可以將sqlboiler作為vendor package。

測試代碼遷移

按Golang的風格寫測試代碼就可以了,利用Golang版本的fixtures可以快速遷移現有測試數據,但要注意它與Rails版本並不完全兼容。

線上測試和部署

對於遷移後的代碼最好先做線上測試,再灰度上線,以確保舊代碼和新代碼的平穩過渡。如果前端部署了nginx作為API gateway,這個問題會非常容易解決。部署環境如下:

                                |--- node of old code
                        |-SLB1->|--- node of old code
                        |       |--- node of old code
SLB ---> API GW(nginx)--|
                        |       |--- node of new code
                        |-SLB2->|--- node of new code
                                |--- node of new code

首先,我們可以主動模擬客戶端的請求同時訪問SLB1和SLB2,完成AB測試。

小貼士:

對於JSON返回值的比較,可以使用reflect.DeepEqual,數組類型需要先排序再比較

比較直觀的線上工具可以使用https://jsondiff.com。

其次, 可修改前端nginx的分發權重,做灰度上線。 比如, 設置10%的流量到新業務,如果一切如常,再逐步提高權重,直至全部流量導入新模塊。

最後下線舊模塊,完成切換。

遷移後的效果

本地壓力測試顯示,使用同樣的redis和mysql配置,用10倍於Rails版本的流量對Golang版本進行壓測, CPU占用約為Rails版本的40%, 內存占用僅為20%。

Golang版本上線後,如果處理每秒大約250的請求數(涉及大約10個關聯表查詢),總共耗費的CPU接近0.8個核, 內存100M,非常的環保。由於該功能從Rails服務中移除,剩餘Rails代碼在忙時也不會再報Passenger隊列擁塞的告警。

結論

  • 負載能力大幅提升,資源占用大幅下降,完全符合我們追求高效的目標。
  • 首次遷移因為需要評估三方軟件,需要寫大量的Go代碼來擴展數據模型,以及需要解決遇到的問題,所以比較耗費人力。
  • 考慮到數據模型是完全可以重用的,後續隻需再補充擴展就可以了。所以後期的維護成本並不會高,應該隻是接近或略大於Rails項目的維護成本。

最後更新:2017-04-20 11:31:14

  上一篇:go Jedis介紹及常見問題分析
  下一篇:go DockerCon 2017 Day2: 安全、生態、敏捷-Docker企業應用的全方位升級