Балансировщик на Go в 200 строк

в 7:51, , рубрики: балансировка нагрузки, балансировка трафика, Веб-разработка, высокая производительность, высоконагруженные проекты, метки: , ,

Я упомянул, что разработал балансировщик на Go, хотя есть мнение, что фронтендом должен быть ngnix.

У меня есть такое чувство, что в комментах люди бывает фантазируют, о чем угодно. Возможно кто-то думает, что и я брешу и нет балансировщика на Go. Поэтому, я решил выложить код балансировщика сразу. Этот код был написан в “особой ситуации” за 4 часа, и потом работал примерно в такой форме 2 недели без перегрузки так, как “все” были в Греции. Код не красив и даже содержит ошибки, но так как он работал и балансировал, то уже чего то стоит.

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

package main
// (c) http://habrahabr.ru/users/pyra/ BSD license

import (
	//	"encoding/json"
	"fmt"
	//	"io"
	"io/ioutil"
	"log"
	"time"
	"net/http"
	"net/url"
	//	"os"
	//	"sort"
	"strconv"
	"strings"
	//	"time"
	"errors"
)

func main() {

	//http.HandleFunc("/r", handle_redir)
	//http.Handle("/extrahtml/", http.FileServer(http.Dir("./extrahtml/")))
	http.HandleFunc("/googleXXXXXXXXXXXX.html", handle_google)
	http.HandleFunc("/", handle_def)

	https1 := &http.Server{
		Addr:           ":8443",
		Handler:        nil,
		ReadTimeout:    20 * time.Second,
		WriteTimeout:   20 * time.Second,
		MaxHeaderBytes: 1 << 15,
	}

	go func() {
		log.Fatal(https1.ListenAndServeTLS("device.crt", "device.key"))
	}()

	http1 := &http.Server{
		Addr:           ":8080",
		Handler:        nil,
		ReadTimeout:    20 * time.Second,
		WriteTimeout:   20 * time.Second,
		MaxHeaderBytes: 1 << 15,
	}

	http1.ListenAndServe()
}

var reqcntr int
var opencntr int

func redirectPolicyFunc(req *http.Request, via []*http.Request) error {
	e := errors.New("redirect")
	return e
}
func handle_google(w http.ResponseWriter, r *http.Request) {
	fmt.Println("google")
	b, _ := ioutil.ReadFile("googleXXXXXXXXXXXXXXXX.html")
	fmt.Println(len(b))
	w.Write(b)
}

func handle_def(w http.ResponseWriter, r *http.Request) {
	// считаем количество открытых одновременно соединений
	opencntr++
	defer func() {
		opencntr--
	}()
	client := &http.Client{
		CheckRedirect: redirectPolicyFunc,
	}
	ip := strings.Split(r.RemoteAddr, ":")[0]

	// считаем примерно запросы (без мютексов)
	reqcntr++
	q := r.URL.RawQuery
	//fmt.Println("def ", r.Method, reqcntr, q)
	//r.Form, _ = url.ParseQuery(r.URL.RawQuery)
	//io.WriteString(w, r.URL.Path)
	path := r.URL.Path
	
	// ID пользователя будем хранить тут
	var cid int64	
	
	// пометим если запрос к ПХП странице
	breporting := false
	
	// ПХП будем обрабатывать особым образом
	if strings.HasSuffix(path, ".php") {
//		fmt.Println("breporting = true")
		breporting = true
	}
	
	// через прокси будем запрашивать другую ПХП страницу
	if path == "/ajax/main.php" {
		path = "/ajax/main_hide1777.php"
	}

	cid = -2
	if strings.HasPrefix(path, "/ajax/") || strings.HasPrefix(path, "/im/") {
		// файлы в след папках содержат ID пользователя
		
		m, err := url.ParseQuery(q)
		if err == nil {
			id := m.Get("xid")
			if id == "" {
				id = m.Get("aid")
				if id == "" {
					id = m.Get("cid")
					if id == "" {
						id = m.Get("bid")
					}
				}
			}
			cid, err = strconv.ParseInt(id, 10, 64)
			if err != nil {
				cid = -1
			}
		}
	} else if strings.HasPrefix(path, "/avatar/") {
		// аватарка тоже содержит ИД /avatar/1234.gif 
		// ID - 1234
		
		cid = -1
		pos1 := strings.Index(path[8:], ".")
		if pos1 != -1 {
			id := path[6 : pos1+6]
			var err error
			cid, err = strconv.ParseInt(id, 10, 64)
			if err != nil {
				cid = -1
			}
		}
	}
	// тут был ещё один else где хитро декриптились куки для определения целевого сервера

	// переводим ID пользователя в домен сервера обрабатающего его запросы
	//host := "test000.cloud"
	host := "login.yahoo.com" // для интереса можно посматреть чем заголовок балансировшика отличается от оригинала
	if cid > 1000 && cid < 5000 {
		host = "prod002.cloud"
	} else if cid >= 5000 && cid < 7000 {
		host = "prod003.cloud"
	} else if cid >= 7000 && cid < 15000 {
		host = "prod005.cloud"
	} else if cid >= 15000 && cid < 16000 {
		host = "prod006.cloud"
	} else if cid >= 25000 && cid < 34000 {
		host = "prod011.cloud"
	}

	url := ""
	// передаем реальный IP в урле так как PHP 5.3 FastCGI не умеет хедеры читать
	if breporting {
		url = "https://" + host + path + "?" + q + "&HEHE_IP="+ip+"&HEHE_SECRET=B87BVf5"
	}else{
		url = "https://" + host + path + "?" + q
	}
	fmt.Println(url)
	
	// проксируем только GET запросы
	if r.Method == "GET" {
		req1, err := http.NewRequest("GET", url, nil)
		if err != nil {
			fmt.Println("Error1: ", err)
			// тут чего то не хватает
		}
		req1.Header = r.Header
		req1.Header.Add("XHEHE_REMOTE_IP", ip)
		resp, _ := client.Do(req1)
		StatusCode := resp.StatusCode
		defer resp.Body.Close()
		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			fmt.Println("Error2: ", err)
			// и тут ))))
			// это обычно выстреливает когда редирект
		}
		fmt.Println("def ", r.Method, reqcntr, opencntr, url, len(body), StatusCode)
		for k1, v1 := range resp.Header {
			for _, v2 := range v1 {
				w.Header().Add(k1, v2)
			}
		}
		w.WriteHeader(StatusCode)
		w.Write(body)
	}
} // 190 строк с комментами

После этот код был переписан. В нем были реализованы идеи из моей предыдущей стаьи. Отдельный порт для админки, который работает всегда, благодаря контролю того сколько к серверу соединений, статистику и отчеты, API для обновления таблиц маршрутизации (это позволяет мигрировать пользователей). И вот в разрабатываем задержку запросов на время обновления инстансов или миграции пользователей, так же в бете есть кеширование статики, кеширование редиректов.

Что такое миграция пользователя. Основной профайл пользователя без фоток это ~10КБ. Сервер А сериализирует пользователя в файл, файл сжимается, файл копируется на сервер Б, балансировщику или балансировшикам говорится, что теперь запросы от этого пользователя следует направлять серверу Б.

Мне было бы интересно сравнить Go и ngnix. Понятно ngnix быстрей, но если ngnix на одном ядре балансирует 300 или 500 мбит, а Go только 50 мбит, то учитывая скорость типичного виртуального порта у облачного сервера за 5 баксов у того же DigitalOcean, интересно как это выражается в деньгах? А вдруг Go может балансировать только 1мбит? Времени нет сравнить и написать.

Если кто-то решит сделать бенчмарк, то важно учитывать не только, то что размер ответа разный, но и то что запрос может обрабатываться с задержкой. Поэтому, для симуляции инстансов отлично подойдет Go. Вместо http.NewRequest поставить time.Sleep. Инстансов должно быть на порядок больше, чем балансировшиков.

Автор: pyra

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js