记一次从Rails至Golang的接口迁移

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 Tair(兼容Redis),内存型 2GB
简介: 初创公司常会选择类似Rails这样的框架进行业务的快速开发,但Rails存在并发性低的弱点,随着业务量的提升,有可能出现性能上的问题。这时,往往需要将一部分高频调用的接口使用一种并发性更好的技术(如openresty, golang, erlang, scala...)加以改造,本文总结了将一个线上高频访问的接口服务从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,数组类型需要先排序再比较

比较直观的线上工具可以使用http://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项目的维护成本。
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
15天前
|
存储 Rust Go
Go nil 空结构体 空接口有什么区别?
本文介绍了Go语言中的`nil`、空结构体和空接口的区别。`nil`是预定义的零值变量,适用于指针、管道等类型;空结构体大小为0,多个空结构体实例指向同一地址;空接口由`_type`和`data`字段组成,仅当两者均为`nil`时,空接口才为`nil`。
Go nil 空结构体 空接口有什么区别?
|
5月前
|
Go 数据安全/隐私保护
go 基于gin编写encode、decode、base64加密接口
go 基于gin编写encode、decode、base64加密接口
48 2
|
2月前
|
存储 Go
Go to Learn Go之接口
Go to Learn Go之接口
31 7
|
2月前
|
Go
Golang语言基础之接口(interface)及类型断言
这篇文章是关于Go语言中接口(interface)及类型断言的详细教程,涵盖了接口的概念、定义、实现、使用注意事项以及类型断言的多种场景和方法。
38 4
|
6月前
|
程序员 Go
|
3月前
|
存储 缓存 NoSQL
在 Go 中使用接口进行灵活缓存
在 Go 中使用接口进行灵活缓存
|
3月前
|
XML 存储 JSON
在Go中使用接口:实用性与脆弱性的平衡
在Go中使用接口:实用性与脆弱性的平衡
|
3月前
|
SQL 安全 测试技术
[go 面试] 接口测试的方法与技巧
[go 面试] 接口测试的方法与技巧
|
3月前
|
存储 安全 程序员
|
3月前
|
存储 设计模式 Go
深入理解Go语言的接口
【8月更文挑战第31天】
16 0