предисловие
Недавно я хотел узнать о знаниях, связанных с шлюзом, я искал и увидел проект Wukong API Gateway. Документ насыщен картинками и текстами, и он на уровне предприятия.Решение есть.Адрес проекта:GOKU-API-Gateway
вопрос
Прежде чем смотреть на исходный код, вы должны сначала поставить цель, и легко заблудиться, когда вы смотрите на код вслепую. смотретьофициальная документацияПопробовав его с документацией, я поставил следующие цели.
- Как собирается информация мониторинга GOKU-API-Gateway? Как хранить?
- Как добиться эффективной переадресации?
- Лимит QPS, как это делается в распределенной ситуации, особенно лимит второго уровня?
- Как упростить добавление новых функций фильтрации?
- Есть чему поучиться?
- Есть ли что-то, что можно улучшить?
- Подумайте, какие функции должен обеспечивать шлюз?
- Какие проблемы стоят перед мыслящими шлюзами?
Ключевая структура ГОКУ
Прежде чем смотреть код, необходимо понять, как устроена абстракция данных в 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 оценивается как набор веб-фреймворков, вы можете искать ServeHTTP по всему миру. Среди них промежуточное ПО. Картирование — это функция обработки каждого 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
Когда я увидел деревья в коде, я подумал о джин-фреймворке, я щелкнул и обнаружил, что дерево маршрутизации в основном такое же, как и джин-фреймворк, но содержимое узлов немного отличается. Вместо интерфейса, соответствующего набору функций-обработчиков, всего один. Есть дополнительный указатель Context Объект Context в основном хранит в API адрес переадресации, текущую политику ограничения, статистическую информацию и т.д.Объект контекста — самый важный объект для понимания обработки всего шлюза, и нет ни одного.Это эквивалентно локальному кешу информации об интерфейсе.Когда найдена функция обработки маршрута, находится локальный кеш информации об интерфейсе, что уменьшает один кеш-запрос.Идея очень хорошая! ! !
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 сказано, что это функция обработки интерфейса.Весь процесс очень понятен.Кликнув по нему, можно посмотреть, как делается различная фильтрация. На самом деле можно обнаружить, что весь код не очень хорошо справляется с некоторыми мелкими деталями в high concurrency.Конкретно то, что можно улучшить, будет подробно описано.
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? Как хранить?
Во время процесса запроса информации о мониторинге мобильный телефон напрямую сохраняется в контексте, соответствующем интерфейсу. Вопрос в том, когда шлюз развертывает несколько узлов, как собирать информацию мониторинга каждого узла? С проблемой заходим в код и обнаруживаем, что кода на эту штуку нет. Предполагается, что кастрированная версия этой версии с открытым исходным кодом может быть развернута только на одном узле.
Лимит 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 также должен иметь буфер и печатать, когда буфер заполнен.
Старайтесь не заходить сюда, это не значит не заходить.Поскольку печать журнала на диск включает операции ввода-вывода, это влияет на производительность. Если можно допустить определенную потерю, журнал должен установить определенный буфер и распечатать на диск, когда буфер будет заполнен.
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 должна быть структура для его сохранения, он также должен участвовать в планировании, ставиться в очередь и так далее. В коде сбор статистической информации заключается в открытии горунтайма, чего стоит только добавить блокировку, добавить счетчик++, это совершенно не нужно. Здесь резидентная среда выполнения может использоваться для обработки статистической информации посредством канала.
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, она также должна требовать высокой доступности.
Суммировать
В общем, текущая опенсорсная часть оценивается только как код одной машины, и в ней нет того, что я хочу. Вам нужно посмотреть на другие коды шлюзов с открытым исходным кодом и продолжать учиться.