古老的榕树

用 Go 开发终端接口服务--保证高性能项目的法宝

潘军杰 发表于 2019-05-14 18:54 阅读(3610) 评论(1) 赞(0)
我们在《准备项目所需的 Go 类包》章节里,选择所需的类包,其实是为保证项目的高性能做好了准备。整个项目自上而下,我们都尽量避免产生性能损耗发生的情况。

最顶层路由器部分,是终端每次请求服务必经的模块,我们直接放弃了第三方的路由器,采用 Go 原生内置的 ServeMux 路由器, 并全面禁止在 URL 上携带任何参数,每个 URL 在部署之前都是已固定了。采用此方案后,我们的 chapter01 项目和现在比较流行的 RESTfull 架构,有了较大的区别。RESTfull  在路由器上使用了一定的约定,把 URL 动态参数和请求方式结合起来使用,产生灵活的路由用法。我们的方案却类似相反的,我们禁止了 URL 任何形式的参数,避免路由器复杂的字符串匹配和 Go 的反射功能,请求方式也采用了单一的 POST 方式,忽略了 PUT、DELETE、OPTIONS 一系列的请求方式,保持路由的简单性。而参数都是通过 JSON 直接传递给控制器。 正是因为我们的路由器足够简单单一,所以我们使用 ServeMux 已足够,如果采用 RESTfull 架构,恐怕需要依赖一些第三方类包,比如 :
- gorilla/mux:github.com/gorilla/mux
- httprouter:github.com/julienschmidt/httprouter

gorilla/mux 追求可扩展性,用法倾向于 Go 理念;httprouter 追求更高性能,独创性的嵌入式使用方式。这里我们展示这三者的简单使用方式:

import (
    "net/http"
    
    "github.com/gorilla/mux"
    "github.com/julienschmidt/httprouter"
)
// Go 原生内置 ServeMux 的使用
router := http.NewServeMux()
router.HandleFunc("/api/v1/products", controller.ProductList)
router.HandleFunc("/api/v1/products/detail", controller.ProductDetail)

// gorilla/mux 的使用
router := mux.NewRouter()
router.HandleFunc("/api/v1/products", controller.ProductList)
router.HandleFunc("/api/v1/products/{id:[0-9]+}", controller.ProductDetail)

// httprouter 的使用
router := httprouter.New()
router.HandleFunc("/api/v1/products", controller.ProductList)
router.HandleFunc("/api/v1/products/:id", controller.ProductDetail)
路由器采用 Go 原生内置的 ServeMux,并禁止 URL 携带参数,直接带来的效应是:超高性能,极低内存损耗,更加符合 Go 的使用习惯。截止目前为止还没有哪个路由器的高性能和低消耗可以超过 Go 原生的 ServeMux。

有人说 Go 语言的反射是一把双刃剑,它既可以保证灵活性,也能保证一定的性能。但 Go 还算是一门比较年轻的语言,她的反射机制还是有很大的提升空间的,她的反射尚未能做到性能上的极致,所以我们暂时尽量避免使用它,类包的选择上,我们的原则也是一样的,比如 http 中间件 martini 和 negroni,我们选了 negroni。虽然两者都出自同一个作者,但 martini 使用了大量的反射机制,整个底层架构都是基于反射构造出来的,以至于 martini 在实际项目中,很多人反馈它存在致命的性能缺陷,这点也得到了作者的共识和反思,所以才会有后来的negroni 诞生。为了吸取教训,我们项目选择了 negroni,开发过程中很明显得到了很好的体验,第一是它非常简单实用,第二是它非常接近原生的 net/http 的使用方式,可相互切换,第三是它的性能和损耗,跟 net/http 是几乎相同的,negroni 只是简要封装了 net/http,并提供更多便捷性的东西,比如中间件的引用等

通常一个项目,访问压力都集中在数据的存取上,这和数据库软硬件配置、数据层编码直接有关,除了数据库外部设施需提供足够的抗压能力外,我们开发人员也需要重视 dao 数据层的性能,尽可能避免性能的损耗,所以我们选择了 sqlx 作为数据的存取方案。sqlx 与原生 database/sql 高度兼容,另外还做了许多扩展,相对于 database/sql 性能损耗 2%-3% 左右,性能非常优秀。

*代码清单 - sqlx 和原生 database/sql 保持兼容的部分函数*
Exec(...) (sql.Result, error) // 和 database/sql 的一样
Query(...) (*sql.Rows, error) // 和 database/sql 的一样
QueryRow(...) *sql.Row // 和 database/sql 的一样


*代码清单 - sqlx 扩展的 Get、Select函数*

p := Place{}
// this will pull the first place directly into p
err = db.Get(&p, "SELECT * FROM place LIMIT 1")

pp := []Place{}
// this will pull places with telcode > 50 into the slice pp
err = db.Select(&pp, "SELECT * FROM place WHERE telcode > ?", 50)


sqlx 保证高性能的同时仍然保持原生 database/sql 的使用方式,是非常好的选择,在此方案基础上再增加它灵活性,我们可以使用 sqrl 来做辅助生成 sql 语句和参数,使用起来和 orm 一样灵活和便利,并可以做到非嵌入的结构体,又不损耗性能。

*代码清单 - sqrl 使用的部分例子*

import sq "github.com/elgris/sqrl"
users := sq.Select("*").From("users").Where(sq.Eq{"status": 1})
sql, args, err := users.ToSql()
// 生成的 sql 语句
sql == "SELECT * FROM users WHERE status=?"
// 生成的参数
args == [1]

sql, args, err := sq.
    Insert("users").Columns("name", "age").
    Values("moe", 13).Values("larry", sq.Expr("? + 5", 12)).
    ToSql()
// 生成的 sql 语句
sql == "INSERT INTO users (name,age) VALUES (?,?),(?,? + 5)"


数据层采用的方案,是目前我觉得最满意的,它既保证了高性能的底层数据存取,又不缺乏操作上的灵活性,实体结构体又能做的很漂亮。

顶层、中间层和底层,我们都做了严格的技术考量和实施,减少项目性能损耗,保证稳定的高性能。实际上我们项目部署完成后,在服务器极端的环境下,一直长期保持服务高性能、低损耗的运行着,而且在业务交易不断增长的过程中没有出现过任何中断的问题。


《用 Go 开发终端接口服务》 目录



1 条网友评论

1 楼: 勒克斯 发表于 2020-09-18 11:56:44   回复 TA

mark
称呼*
邮箱*
内容*
验证码*
验证码 看不清换张