前言
最近想学习一下网关相关的知识,搜了一下,看到有个悟空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的入口,也必然要求高可用。
总结
总的来说,目前开源的部分估计仅仅是单机的代码,并没有我想要的东西。需要看其它开源的网关代码,继续学习。