Пользователь
0,0
рейтинг
18 ноября 2013 в 19:30

Разработка → Изящный вебсервер на Go (Graceful Restart) из песочницы

В этой статье я собираюсь описать Graceful Restart на Go. Graceful Restart важен для Go вебприложения. Go обладает одним недостатком. В Go нет возможности перезагружать код вовремя исполнения. Поэтому разработчики на Go встречаются с проблемой, которой нет в серверах написанных на Java, .NET или PHP. Если нужно обновить код сервера написанного на Go, то процесс сервера надо остановить и запустить новый процесс. Это понижает доступность сервера в момент обновления кода.

В предыдущей статье я описал Балансировщик на Go в 200 строк. На базе балансировщика можно обеспечить высокую доступность вовремя обновления приложения, но как тогда обновить сам балансировщик. Использование балансировщика часто может быть просто лишним. Если ваш сервер запущен на Mac OS X или Linux, то есть другой способ обновить код сервера и обработать все запросы поступившие в момент перезапуска сервера. Этим способ является Graceful Restart.

Суть Graceful Restart в том, что в unix/linux системах, открытые файлы и сокеты доступны порожденным процессам. Им достаточно знать значение файлового дескриптора (файловый дескриптор это целое число), что бы получить доступ к файлу или сокету открытому предком.

Вот перечень проблем которые нужно решить для реализации Graceful Restart на Go

  1. В Go автоматически закрываются все открытые файлы при окончании процесса (close-on-exec)
  2. Нужно, что то делать со старыми keep-alive соединениями открытыми в предке

Первая проблема решается двумя способами. С помощью fnctl можно снять флаг syscall.FD_CLOEXEC, или syscall.Dup создаст копию файлового дескриптора, без флага syscall.FD_CLOEXEC. Эти вызовы не доступны в Windows реализации Go, поэтому эта техника и работает Mac OS X и Linux. В данном примере я использую syscall.Dup. Это проще первого подхода.

Вторую проблему я решаю установкой Timeout для соединений в 10 секунд и выключением сервера через 11 секунд после Graceful Restart. Так же вторую проблему можно решить двумя другими способами: врапером net.Listner для того, что бы посчитать количество открытых соединений и предопределением func (c *conn) serve(), что достаточно сложно в Go. Может быть желательно и другое поведение. Например, что бы старый процесс после Graceful Restart сообщал об ошибке и закрывал соединения.

Важно понимать, что после Graceful Restart часть вебброузеров будут соеденены со старым сервером благодаря keep-alive. Новые соединения будут устанавливаться с новым сервером. Для наглядности какой сервер обработал какой запрос я в ответе с сервера указывать PID процесса.

grace1.go


package main

import (
	"flag"
	"fmt"
	"net"
	"net/http"
	"os"
	"os/exec"
	"syscall"
	"time"
	"log"
)

var FD *int = flag.Int("fd", 0, "Server socket FD")
var PID int = syscall.Getpid()
var listener1 net.Listener
var file1 *os.File = nil
var exit1 chan int = make(chan int)
var stop1 = false

func main() {
	fo1, err := os.Create(fmt.Sprintf("pid-%d.log", PID))
	if err != nil { panic(err) }
    	log.SetOutput(fo1)
	log.Println("Grace1 ", PID)

	flag.Parse()

	s := &http.Server{Addr: ":8080",
		ReadTimeout:  10 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	http.HandleFunc("/", DefHandler)
	http.HandleFunc("/stop", StopHandler)
	http.HandleFunc("/restart", RestartHandler)
	http.HandleFunc("/grace", GraceHandler)
	http.HandleFunc("/think", ThinkHandler)


	if *FD != 0 {
		log.Println("Starting with FD ", *FD)
		file1 = os.NewFile(uintptr(*FD), "parent socket")
		listener1, err = net.FileListener(file1)
		if err != nil {
			log.Fatalln("fd listener failed: ", err)
		}
	} else {
		log.Println("Virgin Start")
		listener1, err = net.Listen("tcp", s.Addr)
		if err != nil {
			log.Fatalln("listener failed: ", err)
		}
	}

	err = s.Serve(listener1)
	log.Println("EXITING", PID)
	<-exit1
	log.Println("EXIT", PID)
	
}

func DefHandler(w http.ResponseWriter, req *http.Request) {
	fmt.Fprintf(w, "def handler %d %s", PID, time.Now().String())
}

func ThinkHandler(w http.ResponseWriter, req *http.Request) {
	time.Sleep(5 * time.Second)
	fmt.Fprintf(w, "think handler %d %s", PID, time.Now().String())
}

func StopHandler(w http.ResponseWriter, req *http.Request) {
	log.Println("StopHandler", req.Method)
	if(stop1){
		fmt.Fprintf(w, "stopped %d %s", PID, time.Now().String())
	}
	stop1 = true
	fmt.Fprintf(w, "stop %d %s", PID, time.Now().String())
	go func() {
		listener1.Close()
		if file1 != nil {
			file1.Close()
		}

		exit1<-1
	}()
}

func RestartHandler(w http.ResponseWriter, req *http.Request) {
	log.Println("RestartHandler", req.Method)
	if(stop1){
		fmt.Fprintf(w, "stopped %d %s", PID, time.Now().String())
	}
	stop1 = true
	fmt.Fprintf(w, "restart %d %s", PID, time.Now().String())

	go func() {
		listener1.Close()
		if file1 != nil {
			file1.Close()
		}

		cmd := exec.Command("./grace1")
		err := cmd.Start()
		if err != nil {
			log.Fatalln("starting error:", err)
		}
		exit1<-1
	}()
}
func GraceHandler(w http.ResponseWriter, req *http.Request) {
	log.Println("GraceHandler", req.Method)
	if(stop1){
		fmt.Fprintf(w, "stopped %d %s", PID, time.Now().String())
	}
	stop1 = true
	fmt.Fprintf(w, "grace %d %s", PID, time.Now().String())

	go func() {
		defer func() { log.Println("GoodBye") }()
		listener2 := listener1.(*net.TCPListener)
		file2, err := listener2.File()
		if err != nil {
			log.Fatalln(err)
		}
		fd1 := int(file2.Fd())

		fd2, err := syscall.Dup(fd1)
		if err != nil {
			log.Fatalln("Dup error:", err)
		}

		listener1.Close()
		if file1 != nil {
			file1.Close()
		}

		cmd := exec.Command("./grace1", fmt.Sprint("-fd=", fd2))
		err = cmd.Start()
		if err != nil {
			log.Fatalln("grace starting error:", err)
		}

		log.Println("sleep11", PID)
		time.Sleep(10 * time.Second)
		log.Println("exit after sleep", PID)
		exit1<-1
	}()
}


Запускать эту программу следует без go run.

go build grace1.go
./grace1


Теперь когда сервер запущен у нас есть следующие обрабочики (handlers)

http://127.0.0.1:8080/ — обработчик по умлчанию
http://127.0.0.1:8080/restart — обычный перезапуск сервера
http://127.0.0.1:8080/grace — Graceful перезапуск сервера
http://127.0.0.1:8080/think — обработчик с задержкой

Для того, что бы проверить как это все работает, я написал другую программу на Go. Она делает последовательно запросы к серверу, если нет ошибки то на экран выводится буква g, если ошибка то E. После каждого запроса программа засыпает на 10ms.

bench1.go


package main

import (
	"net/http"
	"time"
)

func main() {
	nerr := 0
	ngood := 0
	for i := 0; i < 10000; i++ {
		resp, err := http.Get("http://127.0.0.1:8080/")
		if err != nil {
			// error
			print("E")
			nerr++
		}else{
			print("g")
			ngood++
			resp.Body.Close()
		}
		time.Sleep(10 * time.Millisecond)
	}
	println()
	println("Good:", ngood, "Error", nerr)
}


Если перезапускать сервер под нагрузкой то bench1.go выдаёт следующую картину.

gggggggggggggggggggggggggggggggggggggggggggggggggggggggEEEEEgggggggggggggggggggg
gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
gggggggggggggggggggggggggggggggggEEggggggggggggggggggggggggggggggggggggggggggggg
ggggggggggggggggggggggggggggggggggggggggggggggggEEgggggggggggggggggggggggggggggg
ggggggggggggggggggggggggEggggggggggggggggggggggggggggggggggggggggggggggggggggggg
gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg
gggEEggggggggggggggggggEgggggggggggggggggggEggggggggggggggggEEgggggggggggggggggE
gggggggggggggggggggggEEgggggggggggggggggEggggggggggggggggggggEggggggggggggggggEE
gggggggggggggggggEEgggggggggggggggggEEggggggggggggggggggEgggggggggggggggEEgggggg


Одна или несколько букв E символизирует об ошибке и недоступности сервева вовремя перезапуска. (Я многократно перегрузил сервер, поэтому буквы E встречаются часто)

Если же использовать Graceful Restart то ошибок я не наблюдал вообще.
@pyra
карма
22,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (37)

  • 0
    Хорошее решение. Я делал вариант с балансировщиком. Во время обновления новая версия сервера поднималась на другом сокете (или физическом сервере, будь их больше одного=), запросы перенаправлялись на неё. Старый сервер выключался после обработки всех соединений/запросов. Но всё вручную, довольно гемморойно.
    • 0
      А балансировщик на Go или начем?
      • +2
        Балансировщик — громкое слово. Скрипт на РНР по таймеру проверяет количество людей на сервере, и отправляет новых на наименее загруженный.
  • +5
    Лучше расскажите как нормально работать с импортом. Как написать приложение с большим количеством файлов, со структурой более чем одна папка/5 файлов. Потому как импорт в GO работает не совсем так же как в других языках, и ставит в ступор с первого взгляда.
    • 0
      Яростно плюсую. Да, я читал документацию. Но русским языком, да туториал — лучше всего.
      • 0
        Точно помню была мутная статья на хабре про про пакеты. Хотел ссылку кинуть, а теперь ее нет. Кратко: Пакеты в Go совсем не такие как в Java.
        1. Программа сама по себе это один пакет и все файлы в программе добавляются так
        go build file1.go file2.go ... fileN.go
        

        2. Можно отдельно разработать пакет, установить в Go, после чего использовать этот пакет в других пакетах
        3. go get — позволяет устанавливать пакет с сайта
        • 0
          и название пакета это последнее слово после слеша не `os.exec`, а `exec`
          • 0
            Получается что если у меня программа состоит из многих файлов с развернутой древовидной структурой мне нужно билдить все файлы вместе? И не нужно ничего импортить?
            • +3
              Да. Но вы можете создать пакет. Наверное, нужна статья, которая раскажет как создать один пакет и установить его в Go без лишней информации.
              • 0
                Лучше статья про то, как создать 2 пакета для одной программы. Вот у меня есть, например, фронт и бэк. Пусть я хочу, чтоб оно всё жило единым процессом, а значит каждый из них будет максимум отдельным пакетом. Ну и интеграционная какая штука, для запуска всего этого счастья. Плюс обвязка всякая сферическая — файлы с данными. Как это хозяйство организовать и вместе заиспользовать — вот чего не понимаю. На тему конфигурационного и сборочного менеджмента у Го как-то сообщество еще не определилось. Каждый лепит свой инструмент.
                • –1
                  Я думаю это проблема отсутствия нормальной IDE. Сейчас, что бы работать можно использовать Sublime Text с плагином для Go и консоль. Открыл консоль. Запустил bat или sh файл и он настроил GOPATH для проекта. После можно добавлять пакеты связанные с проектом. IDE может автоматом следить за GOPATH и визард создания пакета мог бы это все спрятать и снять много вопросов.

                  Или просто пара скриптов для создания проектов, добавления пакетов, компиляции и тд

                  goide createproject proj01
                  doide addpacket connectorsql
                  goide buildall
                  


                  но как очень правильно сказал antarx
                  golang.org/doc/code.html — это все. все ясно. не больше и не меньше
                • +1
                  Я делаю так (Linux):
                  0.a. Допустим, имеется некоторый глобальный адрес для наших репозиториев (в моём случае git, но с остальными аналогично):
                  REPO_ADDR=mysite.ru/git-repos
                  

                  1.
                  export GOPATH=$HOME/go
                  mkdir -p $GOPATH/src
                  

                  2.
                  ln -s $HOME/projects $GOPATH/src/$REPO_ADDR
                  

                  3. Создаю в $HOME/projects корневой каталог для проекта и его основных частей:
                  mkdir -p $HOME/projects/myproject.git/{backend,frontend,main}
                  

                  Имя каталога проекта должно (очень желательно) быть таким, чтобы путь от $GOPATH/go/src/ совпадал с URL репозитория, в данном случае получится (с учетом симлинков): $GOPATH/go/src/mysite.ru/git-repos/myproject.git

                  В импортах пакеты ссылаются друг на друга по полным путям: mysite.ru/git-repos/myproject.git/backend, mysite.ru/git-repos/myproject.git/frontend, mysite.ru/git-repos/myproject.git/main.

                  В принципе, ничто, кроме лени, не мешает иметь несколько «сред» — нужно всего лишь правильно выставлять GOPATH в зависимости от ситуации.
    • 0
      Пакет == директория. Если не заморачиваться с установкой пакетов, можно просто создавать подкаталоги внутри вашего проекта. Например, подкаталоги pac1/, pac2/, pac3/. Файлы внутри пакета(каталога) могут называться как угодно, но должны начинаться с package имя_пакета(каталога), т.е. внутри pac1/ файлы должны начинаться с package pac1. Тогда из основного пакета их можно импортировать так import ("./pac1", "./pac2", "./pac3")
      • 0
        а как мне импортить из pac2 в pac1?
        • 0
          import "../pac2"
          • 0
            … — нет вообще
            есть переменная окружения $GOPATH вот в той папке установлены все пакеты. в $GOPATH может хранить несколько путей. Если xx/yy то он там
            • 0
              … — нет вообще
              ../../ :)
              Я понимаю, что было задумано всё ставить в $GOPATH/src/, но иногда действительно проще запихнуть всё в один проект и передать в таком виде, чем заставлять человека у себя заморачиваться с $GOPATH и прочими вещами для сборки приложения из исходников.
              • 0
                Можно иметь отдельный путь в $GOPATH для каждого проекта. Пакеты в $GOPATH это реальность. В проекте можно быть sh/bat файл для каждого проекта,
                • 0
                  Дело в том что при деплое я не всегда могу работать с пакетами.
                  + Го не работает с закрытыми репозиториями, поэтому у меня переодически завязаны руки, если я хочу разбить проект на пакеты и работать через $GOPATH но не хочу показывать исходники наружу.
                  + В инете столько мусора по поводу GO очень многие мнят себя супер программистами и дают ужасные своеты =) Я перечитал все статьи на golang.org и там явно дают понять что стоит использовать $GOPATH, и я понимаю как с ним работать, как создать пакет и добавить его в проект, но иногда нужно сделать проект без нормальных пакетов, но структура проекта широка и поэтому приходится импортить файлы, ибо билдить все файлы не удобно =) И способ Stronix в принципе работает хорошо. Я даже так начинал делать, но послушал «умных людей» и все в голове перемешалось в ужасную кучу.
                  • 0
                    Не очень уловил суть затруднений но, чтобы подбросить в ужасную кучу. Вы ведь наверняка в курсе $GOPATH, как и $PATH может иметь сруктуру PATH1:PATH2:PATH3…
                    • 0
                      o.O Век живи, век учись. Спасибо, не знал этого. Это сильно упростит жизнь.
                      Дело в том что такие куски кода:
                      $ mkdir $GOPATH/src/github.com/user/hello
                      в официальных мануалах сбивают тоже с толку =)
                    • 0
                      =) Всетаки два пути GOROOT и GOPATH путают, назвали бы уже один GOROOT а другой GOLIB
                      • 0
                        Если вы можете обойтись без GOROOT и вы почти всегда можете, просто забудьте о нем. Так рекомендуют разработчики языка.
                        • 0
                          Как же без GOROOT обойтись, если он туда ставит либы внешние?
                          • 0
                            Если задан GOPATH, go install будет ставить либы в него, а не в GOROOT.
                            • 0
                              А если в GOPATH будут 6 путей, в какой из них поставится либа?
                              • 0
                                Если я не ошибаюсь, то в первый. Надо посмотреть на сайте Go, там это есть.
                              • 0
                                Вот здесь написано:
                                Since the $GOPATH variable can be a list, the rest of this document will use $GOPATH to mean the first element unless otherwise specified.


                                И там же:
                                It is useful to have two GOPATH entries. One for a location for 3rd party goinstalled packages, and the second for your own projects. List the 3rd party GOPATH first, so that goinstall will use it as a default destination.


                                Так что, я думаю, действительно будет использоваться первый элемент.

                                Ну и, в конце концов, всегда можно провести эксперимент.
    • 0
      golang.org/doc/code.html — начните с этого.
      На моём опыте оказалось удобно иметь GOPATH на директорию проекта, и внутри него держать все зависимости (например, git submodule'ями), и собирать встроенным go build (например, через Makefile).
      Пути вида "../exec" имхо зло.
  • 0
    Это же перевод? Я эту статью (или очень похожую) читал на английском.
    • 0
      Похоже здесь blog.nella.org/?p=879 читал. Да, просто похожая статья.
      • 0
        Я знаю эту статью и еще go-nuts есть обсуждения и SO
    • 0
      нет. этот сервер я написал перед тем как добавить его в свой балансировщик, об это я уже писал в статье Балансировщик на Go в 200 строк Сервер послужил основой для статьи. Статью написал сам. А песочница это потому, что моя предыдущая статья нарушила правила и меня удалили.
  • 0
    В Go нет возможности перезагружать код вовремя исполнения.


    А как работает Revel?
    • 0
      Вогнали в ступор. Я спать собирался )))
      github.com/robfig/revel/blob/master/harness/app.go
      Revel это балансировшик и приложения это отдельные процессы. Hot Reload это в Revel это

      go build app.go
      kill $OLD_PID
      ./app.go -port=…
  • 0
    Спасибо что поделились рецептом. По ходу чтения кода появился вопрос.
    Что происходит в следующей строчке кода?
    listener2 := listener1.(*net.TCPListener)
    Не могу понять что мы имеем в listener1 на этот момент.
    Спасибо.
  • 0
    var listener1 net.Listener
    

    golang.org/pkg/net/#Listener — это интерфейс
    а TCPListener это структура
    golang.org/pkg/net/#TCPListener

    в C++ есть. и -> в Go оба заменены на.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.