preface
Recently, I wanted to learn some knowledge about gateway. I searched and found a project about Wukong API gateway. Well documented and enterprise-level, the decision was made: goku-api-Gateway
The problem
Before you look at the source code, you have to set a target. It’s easy to get lost by looking at the code blindly. After reading the official documentation and experimenting with it, I decided on the following goals.
- Goku-api-gateway How to collect monitoring information? How to store it?
- How to make efficient forwarding?
- How does QPS limit work in distributed situations, especially the second limit?
- How to add new filtering function conveniently?
- Is there anything to learn?
- Is there anything that can be improved?
- What should the Thinking gateway provide?
- What are the challenges facing the Thinking gateway?
Key structures of GOKU
Before looking at the code, it’s important to understand how data is abstracted in Goku-api-Gateway. This open the management background, the use of the need to set up the things set again, this piece of basic can also be. The corresponding structure is here: server/conf.
The key of
API: defines an interface forwarding, which mainly contains, request URL, forwarding URL, method, traffic policy and other information
Policy: Defines traffic restriction policies, including authentication mode, IP address blacklist and whitelist, and traffic control
The general process of processing a request
The entrance
In the outermost layer of the project there are two files: goku-ce. Go, goku-ce-admin.go. Goku-ce-admin. go is the back-end management interface and goku-ce-go is the real gateway service.
goku-ce.go
See ListenAndServe is estimated to be the Web framework that set of things, you can global search ServeHTTP. Middleware.Mapping is the handler for each API.
func main(a) {
server := goku.New()
/ / register routing handlers for server RegisterRouter (middleware server. ServiceConfig, Mapping, middleware. GetVisitCount)
fmt.Println("Listen",server.ServiceConfig.Port)
// Start the service
err := goku.ListenAndServe(":" + server.ServiceConfig.Port,server)
iferr ! =nil {
log.Println(err)
}
log.Println("Server on " + server.ServiceConfig.Port + " stopped")
os.Exit(0)}Copy the code
ServeHTTP
Gin is a framework that comes to mind when you see the trees in the code. When you click on it, you find that the routing tree is basically the same as gin, but the content in the nodes is a little different. Instead of one interface for a set of handlers, there is only one. Add a pointer to the Context object, which stores the forwarding address, traffic limiting policy, statistics and so on in the API. Context object is the most important object to understand the entire gateway processing. It is equivalent to the local cache of the interface information, when the route handler function is found, the local cache of the interface information is found, reducing a cache query, this idea is very good!!
Func (r *Router) ServeHTTP(w http.responsewriter, req * http.request) {//ifroot := r.trees[req.Method]; root ! // handle, ps, context, TSR := root.getValue(path);ifhandle ! = nil { handle(w, req, ps,context)return
} else{// omit N code} // omit N code} //typeNode struct {path String wildChild bool nType nodeType maxParams Uint8 indices String children []*node Handle HANDLE PRIORITY uint32 // API forwarding address, traffic limiting policy, and statisticsCopy the code
middleware.Mapping
In Goku-ce. go it says that this is the interface handler, and the whole process is very clear, and you can see how the various filters are done by following the dots. In fact, it can be found that the whole code does not deal with the small details of high concurrency very well, and specific areas where there can be improvements will be highlighted.
func Mapping(res http.ResponseWriter, Req *http.Request,param goku.params,context * goku.context) {// Update the number of live access times go Context. VisitCount. CurrentCount. UpdateDayCount () / / verification IP whether f, s: = IPLimit (context, res, the req)if! F {res. WriteHeader (403) res. Write (byte [] (s)) / / statistical information collection 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} // RateLimit 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)
ifstatusCode ! = 200 { go context.VisitCount.FailureCount.UpdateDayCount() go context.VisitCount.TotalCount.UpdateDayCount() }else {
go context.VisitCount.SuccessCount.UpdateDayCount()
go context.VisitCount.TotalCount.UpdateDayCount()
}
return
}
Copy the code
The answer to the question
Goku-api-gateway How to collect monitoring information? How to store it?
The monitoring information is stored directly in the Context of the interface. The problem arises when multiple nodes are deployed on the gateway, how to collect the monitoring information of each node? With the problem, go to the code, found that there is no code. This is probably a castrated version of the open source version, which can only be deployed on a single node.
How does QPS limit work in distributed situations, especially the second limit?
The code doesn’t take that into account
How to add new filtering function conveniently?
New filtering is needed, add it to the middleware.mapping function. I think we can take a page from the GIN framework, where a URI corresponds to multiple handlers, each of which is a filtering function. In this way, it is even possible to hot-plug, as long as each process provides a list of URI handlers for corresponding interface modifications.
Is there anything to learn?
The interface information is stored in the routing tree
I’ve already said that up there, so I’m not going to do that. Great idea.
Is there anything that can be improved?
In the case of high concurrency, the code requirements will be very high, there is no necessary overhead can be saved, considering the general use of the gateway this thing, the concurrency must be relatively high, so there are the following improvements.
If absolute accuracy is not required, there is no need to call time.now() every time to get the time
There are a lot of code about time judgment, in fact, do not require absolute accuracy, can be directly from the cache inside the time. Because a system call is made every time time.now() is called, the overhead is minimal. Caching is also very simple. Just set a timer that updates every second. Examples of code that could be improved.
func (l *LimitRate) UpdateDayCount() {// TODO improves l.lock.Lock() now := time.now (); // TODO improves l.lock.Lock() now := time.now ()ifnow.Day() ! = l.begin.Day(){
l.begin = now
l.count = 0
}
l.count++
l.lock.Unlock()
}
Copy the code
If you can cache it, cache it. You don’t have to compute it every time
func (l *LimitRate) UpdateDayCount() {// TODO improved l.lock.Lock() now := time.now () // Begin should be fixed. The date should be calculated at initialization so that l.bigen.day () is not called every time.ifnow.Day() ! = l.begin.Day(){
l.begin = now
l.count = 0
}
l.count++
l.lock.Unlock()
}
Copy the code
In high concurrency scenarios, try not to type LOG, and the LOG must have a buffer, and the buffer is full before printing
Try not to log. I don’t mean don’t log. Because printing logs to disk involves IO, there is a performance impact. If a certain amount of loss can be tolerated, the log should set a buffer and wait until the buffer is full before printing to disk.
func (l *LimitRate) DayLimit() bool {
result := trueL.lock. Lock() now := time.now (ifnow.Day() ! = l.begin.Day(){
l.begin = now
l.count = 0
}
ifl.rate ! = 0 {t := now.hour () bh := now.hour (ifBh && t < = t < l.e nd | | (bh > l.e nd && (t < bh && t < l.e nd)) {/ / TODO one thousand has exceeded the rate a mistake that GG, application use > =if l.count == l.rate {
result = false
} else{l.count++}}} // TODO improves this high concurrency scenario by not printing fmt.println ("Day count:")
fmt.Println(l.count)
l.lock.Unlock()
return result
}
Copy the code
There is a cost to enabling Goruntime and a simple operation should not open a new Goruntime
Goruntimes has a very, very good reputation for being lightweight and cheap, and it’s not a problem to drive thousands, but that doesn’t mean there’s no cost. Goruntime also has to have a structure to hold, also has to participate in scheduling, also has to queue and so on. In the code, statistics are collected by opening a Goruntime, which simply adds a lock to the counter ++, which is completely unnecessary. Here you can use channle’s approach to create a resident Goruntime dedicated to handling statistics.
func Mapping(res http.ResponseWriter, Req *http.Request,param goku.params,context * goku.context) {// Update the number of live access times go Context. VisitCount. CurrentCount. UpdateDayCount () / / verification IP whether f, s: = IPLimit (context, res, the req)if! f { res.WriteHeader(403) res.Write([]byte(s)) go context.VisitCount.FailureCount.UpdateDayCount() go context.VisitCount.TotalCount.UpdateDayCount()return}}Copy the code
What should the Thinking gateway provide?
This needs to be summarized by looking at the other gateway code.
What are the challenges facing the Thinking gateway?
Gateways, as the entry point to all apis, almost certainly present high concurrency challenges. Being the gateway to all apis also requires high availability.
conclusion
In general, the open source part of the current estimation is just stand-alone code, not what I want. Need to look at other open source gateway code and keep learning.