前言
最近想学习一下网关相关的知识,搜了一下,看到有个悟空API网关的项目。文档图文并茂,又是企业级别的,就决定第一个网关代码就是它了,项目地址:GOKU-API-Gateway
问题
看在源码之前,得先定一下目标,盲目地看代码容易迷失。在看了官方的文档和跟着文档搭起来试用了一下之后,定下了下面这些目标。
GOKU-API-Gateway监控信息如何收集?如何存储?
如何做到高效的转发?
QPS限制,在分布式的情况下是怎么做的,尤其是秒级的限制?
如何做到方便添加新的过滤功能?
有没有什么可以学习的?
有没有可以改进的地方?
思考网关应该提供一些什么功能?
思考网关所面临着的挑战有哪些?
GOKU关键的结构体
看代码之前,有必要理解一下GOKU-API-Gateway中数据的抽象是怎样的。这个打开管理后台,把用起需要设置的东西都设置一遍,这一块基本也就可以了。对应的结构体在这里:server/conf。
关键的
API: 定义了一个接口转发,里面主要包含了,请求的URL,转发的URL,方法,流量策略等等信息
策略: 定义了流量限制的策略,主要有:鉴权方式,IP的黑白名单,流量控制等等信息
一次请求处理的大体流程
入口
在工程的最外层有两个文件:goku-ce.go,goku-ce-admin.go。点进去瞄一眼,大体就知道goku-ce-admin.go是后台管理的接口,goku-ce.go是真正的网关服务。
goku-ce.go
看到有ListenAndServe估计就是web框架那一套东西,可以全局搜一下ServeHTTP。其中middleware.Mapping是每一个API的处理函数。
func main() { server := goku.New() // 注册路由的处理函数 server.RegisterRouter(server.ServiceConfig,middleware.Mapping,middleware.GetVisitCount) fmt.Println("Listen",server.ServiceConfig.Port) // 启动服务 err := goku.ListenAndServe(":" + server.ServiceConfig.Port,server) if err != nil { log.Println(err) } log.Println("Server on " + server.ServiceConfig.Port + " stopped") os.Exit(0) }
ServeHTTP
看到代码中的trees就想到了gin这个框架,点进去发现路由树这一块基本上和gin框架的差不多,但是节点中的内容有点不一样。不再是一个接口对应一组处理函数,而是只有一个。多了个Context的指针,Context对象里面主要是保存了API的中的转发地址,限流策略,统计信息等等,context对象是理解整个网关的处理最重要的对象,没有之一。相当于接口信息的本地缓存,当找到路由的处理函数时,就找到了接口信息的本地缓存,减少了一次缓存查询,这个思路非常棒!!!
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { // 省略N多代码 // 看到这个trees就想到了之前看的gin框架, if root := r.trees[req.Method]; root != nil { // context是个关键点, handle, ps, context,tsr := root.getValue(path); if handle != nil { handle(w, req, ps,context) return } else{ // 省略N多代码 } // 省略N多代码}// type node struct { path string wildChild bool nType nodeType maxParams uint8 indices string children []*node // 只有一个处理函数 handle Handle priority uint32 // API的中的转发地址,限流策略,统计信息都这context里面 context *Context }
middleware.Mapping
在goku-ce.go中就说了这个是接口的处理函数,整个流程很清晰,各种过滤是怎么做的顺着点进去就可以看到了。其实可以发现,整个代码对应处理高并发中的一些小细节做不是很好,具体的在有什么可以改进的地方会重点描述。
func Mapping(res http.ResponseWriter, req *http.Request,param goku.Params,context *goku.Context) { // 更新实时访问次数 go context.VisitCount.CurrentCount.UpdateDayCount() // 验证IP是否合法 f,s := IPLimit(context,res,req) if !f { res.WriteHeader(403) res.Write([]byte(s)) // 统计信息的收集 go context.VisitCount.FailureCount.UpdateDayCount() go context.VisitCount.TotalCount.UpdateDayCount() return } // 权限验证 f,s = Auth(context,res,req) if !f { res.WriteHeader(403) res.Write([]byte(s)) go context.VisitCount.FailureCount.UpdateDayCount() go context.VisitCount.TotalCount.UpdateDayCount() return } // 速率限制 f,s = RateLimit(context) if !f { res.WriteHeader(403) res.Write([]byte(s)) go context.VisitCount.FailureCount.UpdateDayCount() go context.VisitCount.TotalCount.UpdateDayCount() return } //接口转发 statusCode,body,headers := CreateRequest(context,req,res,param) for key,values := range headers { for _,value := range values { res.Header().Set(key,value) } } res.WriteHeader(statusCode) res.Write(body) if statusCode != 200 { go context.VisitCount.FailureCount.UpdateDayCount() go context.VisitCount.TotalCount.UpdateDayCount() } else { go context.VisitCount.SuccessCount.UpdateDayCount() go context.VisitCount.TotalCount.UpdateDayCount() } return}
问题的答案
GOKU-API-Gateway监控信息如何收集?如何存储?
监控信息请求过程中进行手机,直接存储在接口对应的Context里面。问题来了,当网关部署多个节点时,怎么将各个节点的监控信息收集起来?带着问题,去找代码,发现没有这一块的代码。估计这个开源的版本的阉割版吧,只能单节点部署。
QPS限制,在分布式的情况下是怎么做的,尤其是秒级的限制?
代码当中木有考虑到这一块
如何做到方便添加新的过滤功能?
有新的过滤功能需要,在middleware.Mapping函数里面添加。我觉得这里可以借鉴gin框架那一套,一个URI对应多个处理函数,每个处理函数就是一个过滤功能。这样的话,甚至可以实现热拔插功能,只要每个进程提供对应的接口修改,URI的处理函数列表。
有没有什么可以学习的?
接口信息放在路由树中
这个在上面已经说了,就不再做说明,很棒的思路。
有没有可以改进的地方?
在超高并发的场合,对代码要求会很高,没有必要的开销能省就省,考虑到一般用上了网关这东西,并发量肯定比较高的了,所以才有了下面的那些改进点。
时间如果不需要绝对的精确,没有必要每次都调用time.now()获取
代码里面有很多关于时间判断,其实都不要求绝对的精准,可以直接从缓存里面获取时间。因为每次调用time.now()都会进行系统调用,开销虽然很小。缓存也很简单,弄个定时器每秒更新一次就好。代码中的可以改进的例子。
func (l *LimitRate) UpdateDayCount() { // TODO 改进 l.lock.Lock() now := time.Now() // 这里损失1以内秒的统计不会造成太大的影响,当前时间也应该从缓存里面拿,避免系统调用 if now.Day() != l.begin.Day(){ l.begin = now l.count = 0 } l.count++ l.lock.Unlock() }
能缓存的就缓存起来,不需要每次都计算
func (l *LimitRate) UpdateDayCount() { // TODO 改进 l.lock.Lock() now := time.Now() // 应为begin的时间是不变的日期应该在初始化的时候就计算好,这样就不用每次都调用l.begin.Day() if now.Day() != l.begin.Day(){ l.begin = now l.count = 0 } l.count++ l.lock.Unlock() }
高并发场景尽量不要打LOG,而且LOG也要有缓冲区的,缓冲区满了再打印
这里的尽量不要打log,并不是说不要不打log。 因为把log打印到磁盘是涉及到IO的,对性能是有所影响的。如果可以忍受一定的丢失,log应该设置一定的缓冲区,等缓冲区满了才打印到磁盘。
func (l *LimitRate) DayLimit() bool { result := true l.lock.Lock() now := time.Now() // 清除,重新计数 if now.Day() != l.begin.Day(){ l.begin = now l.count = 0 } if l.rate != 0 { t := now.Hour() bh := l.begin.Hour() // TODO 改进 求加括号,用意很不明确 if bh <= t && t < l.end || (bh > l.end && (t < bh && t < l.end)){ // TODO 改进 万一有错超过了rate那就GG了,应用用>= if l.count == l.rate { result = false } else { l.count++ } } } // TODO 改进 这种高并发场景不要打印 fmt.Println("Day count:") fmt.Println(l.count) l.lock.Unlock() return result }
开启goruntime是有成本的,简单的操作不应该开新的goruntime
goruntimes的声誉非常非常之好,既轻量,又廉价,开成千上万不成问题,但是这并不意味着没有开销。goruntime也是要有结构体来保存,也是要参与调度,也是要排队的等等。在代码当中,统计信息的收集都是开启一个goruntime,里面仅仅是加个锁,将计数器++,这个完全是没有必要的。这里可以通过channle的方式,弄常驻的goruntime专门来处理统计信息。
func Mapping(res http.ResponseWriter, req *http.Request,param goku.Params,context *goku.Context) { // 更新实时访问次数 go context.VisitCount.CurrentCount.UpdateDayCount() // 验证IP是否合法 f,s := IPLimit(context,res,req) if !f { res.WriteHeader(403) res.Write([]byte(s)) go context.VisitCount.FailureCount.UpdateDayCount() go context.VisitCount.TotalCount.UpdateDayCount() return } }
思考网关应该提供一些什么功能?
这个需要再看看其它的网关代码,才能总结出来。
思考网关所面临着的挑战有哪些?
网关作为所有API的入口,几乎可以说必然会有高并发的挑战。由于是所有API的入口,也必然要求高可用。
总结
总的来说,目前开源的部分估计仅仅是单机的代码,并没有我想要的东西。需要看其它开源的网关代码,继续学习。
作者:Ljian1992
链接:https://www.jianshu.com/p/a78849cadcb4
共同學習,寫下你的評論
評論加載中...
作者其他優質文章