Еще один http proxy на golang

Nikita
Golang Notes
Published in
4 min readJun 2, 2018

(История одного рефакторинга)

Одна из самых популярных тем на golang — http proxy. Однажды и нашей компании пришлось заняться данным вопросом. Пришли одни ребята и попросили написать прокси с “плюшечками” — часть запросов хотели проксировать напрямую, запросы же на один URI в зависимости от его содержимого хотели глушить, и отвечать фейком, не прокидывая его дальше.

Сдали задачу человеку из соседнего отдела, он ее за несколько рабочих дней накодил, проверил, что она работает, отдал в продакшн и убежал в отпуск.

Через 3 дня ко мне прибегают — прокся отбивает все подряд запросы 50х ошибками. Полез смотреть лог и вижу, что превышено количество сокетов на юзера. Прекрасно понимаю, что где-то потеряны таймауты на клиенте/сервере или не закрыт Body в http.req. Клонирую проект и начинаю смотреть. А там беда — весь код обмазан 30-ю фреймворками, куча глобальных переменных, шаблоны body фейковых ответов компилируются в рантайме на каждый запрос,мониторинга нет, под каждый URI сделан отдельный хендлер (хотя активно обрабатывать необходимо только один по условиям задачи).

Поставив текущую версию в рестарт по крону раз в 2 часа, я решил потратить день и отрефакторить (учитывая, что анонсировали увеличение нагрузки x100, а конструкция очень шаткая)

Что было:

  • serverMux от гориллы, с расписанными разрешенными методами и хендлерами на каждый возможный входной URI
  • огромная куча глобальных переменных — несколько http клиентов, счетчики для генерации ответов, коннекты к базе, логгеры и так далее. (Для меня это был какой то тихи ужас)
  • отдельный хендлер на каждый запрос с полной перепаковкой содержимого, для каждого хендлера объявлен отдельный глобальный http клиент. Пример:
func SyncUfjlServiceHandler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
sourceBody, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Errorln("syncUfjlServiceHandler get source body error:", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
}
request, err := http.NewRequest("POST", Configuration.Billing.getAddress()+
Configuration.Billing.Synk, bytes.NewBuffer(sourceBody))
if err != nil {
log.Println(err.Error())
}
for k, v := range r.Header {
for _, header := range v {
request.Header.Add(k, header)
}
}
log.Debugln("syncUfjlServiceHandler request:", request)
resp, err := fjlClient.Do(request)
if err != nil {
log.Errorln("syncUfjlServiceHandler response error:", err)
w.WriteHeader(http.StatusBadGateway)
w.Write([]byte(err.Error()))
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Errorln("syncUfjlServiceHandler read all error:", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
log.Debugln("syncUfjlServiceHandler body:", string(body))
for k, v := range resp.Header {
for _, header := range v {
w.Header().Add(k, header)
}
}
w.WriteHeader(http.StatusOK)
w.Write(body)
}
  • отдельный хендлер на “волшебный” URI
func magicUriServiceHandler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
sourceBody, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Errorln("get source body error:", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
}
soapRequest := SoapRequest{}
err = xml.Unmarshal(sourceBody, &soapRequest)
if err != nil {
log.Errorln("unmarshal error:", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
}
isShare := check(soapRequest.Body.Request.Username, Share) //часть проверки условия
_, isGC := GCs[soapRequest.Body.Request.Operation.ServiceCode] // лист, проверка наличия
if isGC && isSharePackages {
Callbacknumberstart.Inc()
tempCallBack := Callbacknumberstart.Load() //счетчик для псевдоответа
soapResponse := NewSoapResponse(tempCallBack) //тело глушилки основного запроса
go callback(tempCallBack)
w.WriteHeader(http.StatusOK)
w.Write(soapResponse)
} else {
request, err := http.NewRequest("POST", Configuration.Back.getAddress()+
Configuration.Back.Asynk, bytes.NewBuffer(sourceBody))
if err != nil {
log.Println(err.Error())
}
for k, v := range r.Header {
for _, header := range v {
request.Header.Add(k, header)
}
}
log.Debugln("magicUriServiceHandler request:", request)
resp, err := magicClient.Do(request)
if err != nil {
log.Errorln("magicUriServiceHandler response error:", err)
w.WriteHeader(http.StatusBadGateway)
w.Write([]byte(err.Error()))
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Errorln("magicUriServiceHandler read all error:", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
log.Debugln("magicUriServiceHandler body:", string(body))
for k, v := range resp.Header {
for _, header := range v {
w.Header().Add(k, header)
}
}
w.WriteHeader(http.StatusOK)
w.Write(body)
}
}

И где то в этих 2000+ строк потерялся незакрытый resp.Body, который приводил к утечке сокетов. Гораздо проще оказалось все переписать, оставив структуры для маршала/анмаршала ответов.

Что стало:

  • В итоге осталось 504 строки полезного кода
  • Все бывшие глобальные переменные схлопнул в структуру
type myServer struct {
Ctx context.Context
CouchBucket *gocb.Bucket //доступ к хранилищу
CallbackNumber atomic.Int64 //счетчик для генерации номеров ответов при перехвате
ReplayDuration time.Duration
ProxyAddress string
CallbackAddress string
GCs map[string]interface{}
SharePackages map[string]interface{}
Templates *template.Template // Скомпилированные шаблоны ответов}
  • Логгер ухеал в контекст, есть у меня одна библиотечка, которую пользую регулярно — https://github.com/lelvisl/logger
  • Остался один хендлер — proxy, который стал методом над myServer
//Proxy - общий метод для проксирования запроса
func (m *myServer) Proxy(w http.ResponseWriter, req *http.Request) {
log := logger.Logger(m.Ctx).WithFields(map[string]interface{}{
"actor": "proxy",
})
tempURL, _ := url.Parse(m.ProxyAddress)
req.URL.Host = tempURL.Host
req.URL.Scheme = tempURL.Scheme
req.Host = tempURL.Host
resp, err := http.DefaultTransport.RoundTrip(req)
if err != nil {
log.Errorf("proxy error: %s, request: %+v", err.Error(), req)
http.Error(w, err.Error(), http.StatusServiceUnavailable)
stopperCounter.WithLabelValues("proxy", req.RequestURI, err.Error()).Inc()
return
}
log.Infof("status code: %d", resp.StatusCode)
defer resp.Body.Close()
copyHeader(w.Header(), resp.Header)
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
stopperCounter.WithLabelValues("proxy", req.RequestURI, strconv.Itoa(resp.StatusCode)).Inc()
}
  • И middelware для перехватов (тоже метод над myServer)
//MiddlewareStop - обработка волшебного URI
func (m *myServer) MiddlewareStop(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := logger.Logger(m.Ctx).WithFields(map[string]interface{}{
"actor": "middlewareStop",
})
sourceBody, _ := ioutil.ReadAll(r.Body)
soapRequest := MagicUriServiceRequest{}
err := xml.Unmarshal(sourceBody, &soapRequest)
if err != nil {
log.Errorf("soap request unmarshal error:%s", err.Error())
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
Username := soapRequest.Body.Request.Username
gc := soapRequest.Body.Request.Operation.ServiceCode
_, isGC := m.GCs[gc]
if isGC && checkSubscriber(Username, m.SharePackages, m.CouchBucket) {
log.WithFields(map[string]interface{}{
"Username": Username,
"gc": gc,
}).Infof("stopping")
requestID := m.CallbackNumber.Inc()
soapResponse := NewMagicUriServiceResponse(requestID, m.Templates)
w.WriteHeader(http.StatusOK)
stopperCounter.WithLabelValues("stopped", r.RequestURI, strconv.Itoa(http.StatusOK)).Inc()
w.Write(soapResponse)
go func() {
time.Sleep(m.ReplayDuration)
err := callback(requestID, Username, m.CallbackAddress, m.Templates)
if err != nil {
log.WithFields(map[string]interface{}{
"Username": Username,
"gc": gc,
}).Errorf("callback error: %s", err.Error())
}
log.WithFields(map[string]interface{}{
"Username": Username,
"gc": gc,
}).Infof("callback sended")
}()
return
}
r.Body = ioutil.NopCloser(bytes.NewBuffer(sourceBody))
next(w, r)
})
}
  • И красивый mux сверху
serveMux := http.NewServeMux()
serveMux.HandleFunc("/", stopper.Proxy)
serveMux.HandleFunc("/magicURI", stopper.MiddlewareStop(stopper.Proxy))
serveMux.Handle("/metrics", promhttp.Handler())

К чему же это все было

  • не надо усложнять код
  • не надо плодить проверки там, где сервис уже сам занимается валидацией ответов
  • не выходите из стандартных библиотек без лишней необходимости
  • не плодите глобальные переменные — это неконтролируемое зло, которое обязательно сыграет с вами злую шутку

--

--