拆轮子系列:网关GoKu-API-Gateway

前言

最近想学习一下网关相关的知识,搜了一下,看到有个悟空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的入口,也必然要求高可用。

总结

总的来说,目前开源的部分估计仅仅是单机的代码,并没有我想要的东西。需要看其它开源的网关代码,继续学习。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容