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

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
云数据库 RDS MySQL Serverless,价值2615元额度,1个月
简介: 初创公司常会选择类似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
目录
相关文章
|
11天前
|
设计模式 存储 监控
《Go 简易速速上手小册》第4章:接口与抽象(2024 最新版)(上)
《Go 简易速速上手小册》第4章:接口与抽象(2024 最新版)
39 1
|
28天前
|
自然语言处理 Go 索引
Go语言学习8-接口类型
【4月更文挑战第1天】本篇 Huazie 向大家介绍 Go 语言中的接口类型
21 2
Go语言学习8-接口类型
|
2月前
|
Go
|
4月前
|
存储 设计模式 Cloud Native
云原生系列Go语言篇-类型、方法和接口 Part 1
通过前面章节的学习,我们知道Go是一种静态类型语言,包含有内置类型和用户定义类型。和大部分现代编程语言一样,Go允许我们对类型关联方法。它也具备类型抽象,可以编写没有显式实现的方法。
51 0
|
27天前
|
存储 安全 Go
掌握Go语言:Go语言类型转换,无缝处理数据类型、接口和自定义类型的转换细节解析(29)
掌握Go语言:Go语言类型转换,无缝处理数据类型、接口和自定义类型的转换细节解析(29)
|
23小时前
|
程序员 Go
|
1天前
|
存储 Go 开发者
【Go语言专栏】深入探索Go语言的接口与多态
【4月更文挑战第30天】本文探讨了Go语言中的接口和多态性。接口是方法集合的抽象类型,允许不同类型的值实现相同的方法,实现多态。接口定义不包含实现,仅包含方法签名。类型实现接口是隐式的,只要实现了接口所需的方法。接口用于编写与具体类型无关的通用代码。通过接口,不同类型的对象可以响应相同消息,展现多态性。此外,接口可以嵌入以继承其他接口,类型断言则用于访问接口内部的具体类型。空接口可存储任何类型值。理解并掌握接口有助于编写更灵活、模块化的Go代码。
|
5天前
|
安全 Go
Golang深入浅出之-接口(Interfaces)详解:抽象、实现与空接口
【4月更文挑战第22天】Go语言接口提供抽象能力,允许类型在不暴露实现细节的情况下遵循行为约定。接口定义了一组方法签名,类型实现这些方法即实现接口,无需显式声明。接口实现是隐式的,通过确保类型具有接口所需方法来实现。空接口`interface{}`接受所有类型,常用于处理任意类型值。然而,滥用空接口可能丧失类型安全性。理解接口、隐式实现和空接口的使用能帮助编写更健壮的代码。正确使用避免方法,如确保方法签名匹配、检查接口实现和谨慎处理空接口,是关键。
14 1
|
11天前
|
存储 设计模式 Go
《Go 简易速速上手小册》第4章:接口与抽象(2024 最新版)(下)
《Go 简易速速上手小册》第4章:接口与抽象(2024 最新版)
25 1
|
2月前
|
安全 Go
接口在Go语言中的实现与应用
【2月更文挑战第19天】接口是Go语言中一种重要的抽象机制,它定义了一组方法的集合,而不关注具体的实现。本文将深入探讨Go语言中接口的实现方式、接口的应用场景以及接口在Go语言设计中的优势,帮助读者更好地理解和应用接口。