• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    迪恩网络公众号

romapres2010/httpserver

原作者: [db:作者] 来自: 网络 收藏 邀请

开源软件名称:

romapres2010/httpserver

开源软件地址:

https://github.com/romapres2010/httpserver

开源编程语言:

Go 76.1%

开源软件介绍:

Шаблон backend сервера на Golang - часть 1 (HTTP сервер)

Представленный ниже шаблон сервера на Golang был подготовлен для передачи знаний внутри нашей команды. Основная цель шаблона, кроме обучения - это снизить время на прототипирование небольших серверных задач на Go.

Шаблон включает:

  • Передачу параметров для запуска HTTP сервера через командную строку github.com/urfave/cli
  • Настройка параметров сервера через конфигурационный файл github.com/sasbury/mini
  • Настройка параметров TLS HTTP сервера
  • Настройка роутера и регистрация HTTP и prof-обработчиков github.com/gorilla/mux
  • Настройка уровней логирования без остановки сервера github.com/hashicorp/logutils
  • Настройка логирования HTTP трафика без остановки сервера
  • Настройка логирования ошибок в HTTP response без остановки сервера
  • HTTP Basic аутентификация
  • MS AD аутентификация gopkg.in/korylprince/go-ad-auth.v2
  • JSON Web Token github.com/dgrijalva/jwt-go
  • Запуск сервера с ожиданием возврата в канал ошибок
  • Использование контекста для корректной остановки сервера и связанных сервисов
  • Настройка кастомной обработки ошибок github.com/pkg/errors
  • Настройка кастомного логирования
  • Сборка с внедрением версии, даты сборки и commit

Ссылка на репозиторий проекта.

В состав шаблона включено несколько HTTP обработчиков:

  • POST /echo - трансляция request HTTP и body в response
  • POST /signin - аутентификация и получение JSON Web Token в Cookie
  • POST /refresh - обновление времени жизни JSON Web Token в Cookie
  • POST /httplog - настройка логирования HTTP трафика
  • POST /httperrlog - настройка логирования ошибок в HTTP response
  • POST /loglevel - настройка уровней логирования DEBUG, INFO, ERROR

Подход к упрощению написания HTTP обработчиков для этого шаблона описан в статье Упрощаем написание HTTP обработчиков на Golang

Содержание статьи

  1. Предыстория
  2. Передача параметров серверу
    2.2. Командная строка
    2.3. Конфигурационный файл
  3. Создание, запуск и остановка сервера
    3.1. Создание daemon и сервисов
    3.2. Запуск daemon и сервисов
    3.3. Остановка daemon и сервисов
  4. Обработка ошибок
    4.1. Кастомная структура ошибки
    4.2. Форматирование печати ошибки
    4.3. Регистрация ошибок
    4.4. Логирование и обработка ошибок
  5. Логирование
    5.1. Куда логируем
    5.2. Формат логирования
    5.3. Как логируем
    5.4. Дополнительное логирование HTTP трафика
  6. Аутентификация
  7. Организация кода и сборка
    7.1. Использование go mod
    7.2. Сборка кода

1. Предыстория

В ходе внедрения 1С:ERP, появилась интересная задача - интеграция 1С с шиной IBM MQ. Ключевыми требованиями в части взаимодействия с IBM MQ были:

  • управление пулом подключений к IBM MQ (минимальное, максимальное, время простоя)
  • автоматическое переключение на резервный узел IBM MQ при сбое основного узла
  • использование транзакционного режима при работе с IBM MQ (SYNCPOINT)

Дополнительно были выдвинуты требования к обработке XML сообщений:

  • нормализация (канонизация) сообщений по стандарту RFC 3076
  • использование меток целостности для верификации сообщений по кастомному алгоритму HMAC с хэш-функцией ГОСТ-34.11.94
  • управление оперативным кэшем секретных ключей для вычисления HMAC

Стандартного адаптера в 1C к IBM MQ не было. Существующий REST API к IBM MQ не подходил под требования.

Адаптер 1C к IBM MQ был успешно разработан на Go с использованием официальной библиотеки IBM MQ. Библиотека отличается неплохой стабильностью, что и не удивительно, так как она написана в виде "обертки" над стандартной C библиотекой. За полгода работы с ней было зафиксировано всего 2 бага с обработкой слайсов [].

Представленный в статье шаблон backend сервера, является обобщением полученного опыта.

Архитектура адаптера 1С к IBM MQ укрупнено показана на следующем рисунке.

REST_IBMMQ

2. Передача параметров серверу

2.1. Командная строка

Все чувствительные, с точки зрения безопасности, параметры сервера передаются через командную строку. Для этого используется библиотека github.com/urfave/cli. Список основных параметров:

   --httpconfig value, --httpcfg value    HTTP Config file name
   --listenstring value, -l value         Listen string in format <host>:<port>
   --httpuser value, --httpu value        User name for access to HTTP server
   --httppassword value, --httppwd value  User password for access to HTTP server
   --jwtkey value, --jwtk value           JSON web token secret key
   --debug value, -d value                Debug mode: DEBUG, INFO, ERROR
   --logfile value, --log value           Log file name

2.2. Конфигурационный файл

Для обработки конфигурационного файла используется библиотека github.com/sasbury/mini. Список типовых параметров, включенных в шаблон:

[HTTP_SERVER]
ReadTimeout = 6000          // HTTP read timeout duration in sec - default 60 sec
WriteTimeout = 6000         // HTTP write timeout duration in sec - default 60 sec
IdleTimeout = 6000          // HTTP idle timeout duration in sec - default 60 sec
MaxHeaderBytes = 262144     // HTTP max header bytes - default 1 MB
MaxBodyBytes = 1048576      // HTTP max body bytes - default 0 - unlimited
UseProfile = false          // use Go profiling
ShutdownTimeout = 30        // service shutdown timeout in sec - default 30 sec

[TLS]
UseTLS = false                  // use SSL
UseHSTS = false                 // use HTTP Strict Transport Security
TLSСertFile = certs/server.pem  // TLS Certificate file name
TLSKeyFile = certs/server.key   // TLS Private key file name
TLSMinVersion = VersionTLS10    // TLS min version VersionTLS13, VersionTLS12, VersionTLS11, VersionTLS10, VersionSSL30
TLSMaxVersion = VersionTLS12    // TLS max version VersionTLS13, VersionTLS12, VersionTLS11, VersionTLS10, VersionSSL30

[JWT]
UseJWT = false                  // use JSON web token (JWT)
JWTExpiresAt = 20000            // JWT expiry time in seconds - 0 without restriction

[AUTHENTIFICATION]
AuthType = INTERNAL             // Autehtification type NONE | INTERNAL | MSAD
MSADServer = company.com        // MS Active Directory server
MSADPort = 389                  // MS Active Directory Port
MSADBaseDN = OU=, DC=, DC=      // MS Active Directory BaseDN
MSADSecurity = SecurityNone     // MS Active Directory Security: SecurityNone, SecurityTLS, SecurityStartTLS

[LOG]
HTTPLog = false                         // Log HTTP traffic
HTTPLogType = INREQ                     // HTTP trafic log mode INREQ | OUTREQ | INRESP | OUTRESP | BODY
HTTPLogFileName = ./httplog/http%s.log  // HTTP log file
HTTPErrLog = HEADER | BODY              // Log error into HTTP response header and body

3. Создание, запуск и остановка сервера

На следующем рисунке показана упрощенная UML диаграмма последовательности запуска и остановки сервера.

http_server_run_stop

Для координации создания, запуска и остановки сервера используется daemon. В его задачи входит:

  • считывание конфигурационного файла
  • настройка конфигурации сервисов
  • создание контекста context.Context
  • создание каналов ошибок для обратной связи с сервисами
  • создание зависимых сервисов
  • корректный запуск сервисов
  • ожидание системных прерываний и/или ошибок от сервисов
  • корректная остановка сервисов

3.1. Создание daemon и сервисов

В общем случае, сервисы создаются и настраиваются при создании daemon.
Если есть ошибки при создании отдельных сервисах, то daemon не создается.
Если сервис предназначен для работы в фоне, то в daemon для него создается отдельный канал ошибок:

httpserverErrCh: make(chan error, 1), // канал ошибок HTTP сервера

При создание сервиса, ему передаются параметры:

  • контекст daemon - используется для передачи в сервис информации о закрытии
  • канал ошибок - используется для возврата из сервиса в daemon информации о критичной ошибке
  • структуру с конфигурационными параметрами сервиса

Примеры задач при создании сервисов:

  • Для сервиса работы с IBM MQ:
    • проверяются входные параметры
    • создается контекст сервиса
    • делается тестовое подключение к кластеру IBM MQ, определяется какой из узлов кластера является рабочим, а какой находится в резерве
    • открывается минимальный пул подключений к IBM MQ
  • Для сервиса работы с PostgreSQL:
    • проверяются входные параметры
    • создается контекст сервиса
    • делается тестовое подключение
    • парсятся, предварительно определенные, SQL команды
  • Для сервиса кеширования JSON в BoltDB:
    • проверяются входные параметры
    • создается контекст сервиса
    • открывается файл BoltDB на запись
    • происходит считывание закешированных ключей и проверяются валидность кэша (данные могли поменяться в БД PostgreSQL). Эта операция может быть перенесена в отдельных фоновый процесс, чтобы сократить время старта сервера (в ходе теста на обработку BoltDB размером 150 Гбайт уходит примерно 2 минуты в 64 потока при условии, что BoltDB размещена на NVMe диске со средним временем отклика 0.03 ms).
  • Для HTTP сервера:
    • проверяются входные параметры
    • создается контекст сервиса
    • создается и настраивается http.server
    • создается TCP листенер
    • настраиваются параметры TLS
    • создается роутер
    • регистрируются HTTP обработчики
    • регистрируются pprof обработчики

3.2. Запуск daemon и сервисов

Запуск daemon заключается в скоординированном запуске сервисов.
Так как все сервисы уже были созданы ранее, то запуск сервиса - это, обычно, включение листенера или установление флага, разрешающего начать обработку.
Для запуска сервисов в фоне, используется анонимная функция с возвратом в канал ошибок. Пример, запуска HTTP сервера

go func() { httpserverErrCh <- d.httpserver.Run() }()

После запуска сервисов, daemon подписывается на основные системные прерывания и переходит в режим ожидания сигналов или возврата в каналы ошибок от сервисов.
Получении daemon ошибки от сервиса, означает, что какой-то из сервисов не может продолжить работу. Здесь логика обработки может существенно отличаться, от полной остановки (как в примере ниже) до перезапуска сбойного сервиса.
Например, если основной сервер IBM MQ становится недоступен, то daemon пробует пересоздать сервис на резервном сервере IBM MQ и продолжить обработку.

syscalCh := make(chan os.Signal, 1) // канал системных прерываний
signal.Notify(syscalCh, syscall.SIGINT, syscall.SIGTERM)

// ожидаем прерывания или возврат в канал ошибок
select {
case s := <-syscalCh: // системное прерывание
    mylog.PrintfInfoMsg("Exiting, got signal", s)
    d.Shutdown() // останавливаем daemon
    return nil
case err := <-d.httpserverErrCh: // возврат от HTTP сервера в канал ошибок
    mylog.PrintfErrorInfo(err) // логируем ошибку
    d.Shutdown() // останавливаем daemon
    return err
}

В запуск сервисов, работающих в фоне, добавляется анонимная функция восстановления после паники (пример ниже). При обработке паники, ошибка возвращается в канал ошибок для уведомления daemon.

func (s *Server) Run() error {
    defer func() {
        var myerr error
        r := recover()
        if r != nil {
            msg := "Recover from panic"
            switch t := r.(type) {
            case string:
                myerr = myerror.New("8888", msg, t)
            case error:
                myerr = myerror.WithCause("8888", msg, t)
            default:
                myerr = myerror.New("8888", msg)
            }
            mylog.PrintfErrorInfo(myerr) // логируем ошибку
            s.errCh <- myerr             // передаем ошибку в канал для уведомления daemon
        }
    }()

    // Запуск сервера
}

3.3. Остановка daemon и сервисов

Остановка daemon заключается в скоординированной остановке сервисов и последующем закрытии корневого контекста.

Остановка сервисов, работающих в фоне, осуществляется по следующему сценарию:

  • устанавливается таймер ожидания успешной остановки (параметр ShutdownTimeout в конфигурационном файле)
  • закрывается контекст сервиса
  • в задачу всех сервисов входит корректная остановка активной работы при закрытии их контекста. На примере сервиса IBM MQ это:
    • ожидание обработки текущих сообщений
    • завершение открытых транзакций
    • возвращение активных подключений в пул
    • закрытие открытых очередей
    • закрытие пула активных подключений к IBM MQ
  • после успешной остановки сервис отправляет подтверждение в канал stopCh

Для корректной остановки сервисов при закрытии контекста, используется такой подход:

  • в корневых циклах добавляется проверка состояния контекста. Если контекст закрыт, то очередную итерацию не начинать и освободить ресурсы
  • в обработчиках, в безопасных местах, добавляется проверка на состояние контекста, если контекст закрыт, то не начинать обработку
for {
    select {
    case <-ctx.Done(): // получен сигнал закрытия контекста

        // Освободить ресурсы

        s.stopCh <- struct{}{} // отправить подтверждение об успешном закрытии
        return
    default:
        // Обработка очередной итерации
    }
}

Для остановки HTTP сервера использовался несколько другой подход:

// создаем новый контекст с отменой и отсрочкой ShutdownTimeout
cancelCtx, cancel := context.WithTimeout(s.ctx, time.Duration(s.cfg.ShutdownTimeout*int(time.Second)))
defer cancel()

// ожидаем закрытия активных подключений в течении ShutdownTimeout
if err := s.httpServer.Shutdown(cancelCtx); err != nil {
    return err
}

s.httpService.Shutdown() // Останавливаем служебные сервисы

// подтверждение об успешном закрытии HTTP сервера
s.stopCh <- struct{}{}

4. Обработка ошибок

4.1. Кастомная структура ошибки

Один из наиболее удачных пакетов для обработки ошибок github.com/pkg/errors.
Первоначально использовал его, но со временем стало не хватать структурности ошибки, поэтому перешел на простой кастомный пакет.

Структура для хранения ошибки:

type Error struct {
    ID       uint64 // уникальный номер ошибки
    Code     string // код ошибки
    Msg      string // текст ошибки
    Caller   string // файл, строка и наименование метода в месте регистрации ошибки
    Args     string // строка аргументов
    CauseErr error  // ошибка - причина
    CauseMsg string // текст ошибки - причины
    Trace    string // стек вызова в месте регистрации ошибки
}

Мне удобно работать с типизированными ошибками, поэтому код ошибки выделен отдельным атрибутом. Например, в адаптере 1С к IBM MQ, использовался простой 4 символьный числовой код. Например, ошибки начинающиеся с "8ххх" относились к HTTP, с "7ххх" - к IBM MQ.

Caller - файл, строка и наименование метода в месте регистрации ошибки. Удобно использовать, если нет необходимости выводить полный стек. Пример вывода:

httpserver.go:[209] - (*Server).Run()

Caller вычисляется функцией

func caller(depth int) string {
    pc := make([]uintptr, 15)
    n := runtime.Callers(depth+1, pc)
    frame, _ := runtime.CallersFrames(pc[:n]).Next()
    idxFile := strings.LastIndexByte(frame.File, '/')
    idx := strings.LastIndexByte(frame.Function, '/')
    idxName := strings.IndexByte(frame.Function[idx+1:], '.') + idx + 1

    return frame.File[idxFile+1:] + ":[" + strconv.Itoa(frame.Line) + "] - " + frame.Function[idxName+1:] + "()"
}

Args - отдельная строка аргументов, которые можно добавить к сообщению при регистрации ошибки. Используется для целей отладки.

CauseErr и CauseMsg - исходная ошибка и сообщение. Используется, если оборачиваем чужую ошибку в свою структуру.

Trace - стандартный трейс стека. Для его получения использовал несколько своеобразный подход. При регистрации ошибки создавал дополнительно пустую ошибку из пакета github.com/pkg/errors и печатал ее с ключом '%+v'. В этом режиме она выводит стек.

fmt.Sprintf("'%+v'", pkgerr.New(""))

4.2. Форматирование печати ошибки

Для соответствия интерфейсу Error, используется вывод в сокращенном формате.

func (e *Error) Error() string {
    mes := fmt.Sprintf("ID=[%v], code=[%s], mes=[%s]", e.ID, e.Code, e.Msg)
    if e.Args != "" {
        mes = fmt.Sprintf("%s, args=[%s]", mes, e.Args)
    }
    if e.CauseMsg != "" {
        mes = fmt.Sprintf("%s, causemes=[%s]", mes, e.CauseMsg)
    }
    return mes
}

Пример вывода в сокращенном формате

ID=[1], code=[8004], mes=[Error message], args=['arg1', 'arg2', 'arg3']

Для расширенного форматированного вывода используются ключи

// %s    print the error code, message, arguments, and cause message.
// %v    in addition to %s, print caller
// %+v   extended format. Each Frame of the error's StackTrace will be printed in detail.
func (e *Error) Format(s fmt.State, verb rune) {
    switch verb {
    case 'v':
        fmt.Fprint(s, e.Error())
        fmt.Fprintf(s, ", caller=[%s]", e.Caller)
        if s.Flag('+') {
            fmt.Fprintf(s, ", trace=%s", e.Trace)
            return
        }
    case 's':
        fmt.Fprint(s, e.Error())
    case 'q':
        fmt.Fprint(s, e.Error())
    }
}

Пример вывода с ключом '%+v'

ID=[1], code=[8004], mes=[Error message], args=['arg1', 'arg2', 'arg3'], caller=[handler_echo.go:[31] - (*Service).EchoHandler.func1()], trace='
github.com/romapres2010/httpserver/error.New
        D:/golang/src/github.com/romapres2010/httpserver/error/error.go:72
github.com/romapres2010/httpserver/httpserver/httpservice.(*Service).EchoHandler.func1
        D:/golang/src/github.com/romapres2010/httpserver/httpserver/httpservice/handler_echo.go:31
        ...

Предложенный формат вывода не полностью соответствует подходу структурированного логирования. Это сделано специально для более удобного чтения лога в ходе отладки. Если нужен боле строгий формат - достаточно поправить в одном месте метод Format.

4.3. Регистрация ошибок

Используются два метода для регистрации ошибок:

  • Создание новой ошибки
New(code string, msg string, args ...interface{}) error
  • Оборачивание существующей ошибки
WithCause(code string, msg string, causeErr error, args ...interface{}) error

Дополнительные аргументы можно либо встроить в сообщение ошибки, либо передать дополнительными параметрами в args ...interface{}.

Все ошибки от сторонних и стандартных пакетов оборачиваются в кастомную ошибку в месте возникновения. Исходная ошибка вкладывается внутрь кастомной, например:

myerr = myerror.WithCause("8001", "Failed to read HTTP body: reqID", err, reqID)

4.4. Логирование и обработка ошибок

Использовался следующий подход:

  • в точке возникновения, ошибка логируется на уровне INFO без trace. В этот момент, обычно, не известно, является ли это ошибкой, или она будет успешно обработана на уровне выше
  • при передаче ошибок на уровень вверх она повторно не оборачивается в WithCause() и не логируется
  • в точке обработки ошибки логируется результат обработки на уровне INFO. Ошибка перестает быть ошибкой.
  • если ошибка дошла необработанной до самого верхнего уровня, значит это действительно ошибка и она логируется на уровне ERROR с максимальной детальностью, включая trace.

Пример логирования при ошибке чтения тела HTTP запроса:


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap