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

от автора

Я упомянул, что разработал балансировщик на 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. Инстансов должно быть на порядок больше, чем балансировшиков.

ссылка на оригинал статьи http://habrahabr.ru/post/197570/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *