Пара недочётов в создании веб приложения на Go

    Если вам интересно, какой недочёт есть во всех вебприложениях на языке Go размещенных на хабре. И хотите знать, как сделать своё Go вебприложение на один шаг ближе к production ready, то добро пожаловать по кат.

    Важное отличие веб программирования на языке Go это то, что в результате вы обычно получаете программу, которая является вебсервером. Поэтому вы становитесь ответственными за то, что обычно отвечает вебсервер.

    Если сравнить как выглядят Hello World на PHP и Go, то мы увидим

    PHP

    <?php echo “Hello World”; ?>
    

    Go

    package main
    
    import (
        "fmt"
        "net/http"
    )
    
    func handler(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
    }
    
    func main() {
        http.HandleFunc("/", handler)
        http.ListenAndServe(":8080", nil)
    }
    

    Взято с вики сайта языка Go

    Аналогичный код используют, в следующих статьях на хабре: Веб-разработка на Go, Пишем веб-эмулятор терминала на Go, используя Websocket, Goblog: Самодельный статический движок для блога на Go, Написание своего Web-приложения на Go.

    Я и сам в статье Своё Certificate Authority — в 5 OpenSSL команд, привёл подобный пример на Go.

    Близко по теме: Go Language. Небольшое клиент-серверное приложение, TCP/IP proxy на Go.

    Важным отличием и преимуществом Go является то, как Go работает с сетью и может держать множество открытых соединений, например, для реализации long polling.

    В статье C10k (Проблема 10000 соединений) на разных языках/платформах Go показал второй результат после Erlang, открыв 9775 соодинений из 10000.

    Вторым преимуществом Go — является простота и читаемость кода, легкость в изучении, что делает его часто выбором №1 при разработке приложений, в которых нужно работать со множеством открытых соединений.

    Ошибка Недочёт всех этих примеров


    Недочёт в том, что Timeout (Deadline) для соединений по умолчанию 0 (т.е. timeout вовсе не установлен). Если клиент не отправит пакет о том, что соединение закрыто, то такое соединение повиснет навсегда. Это приведет к блокировке goroutine в режиме ожидания, до конца жизни вебсервера. Количество открытых соединение ресурс обычно ограниченный. По умолчанию в Линуксе приложение может открыть 1024 файла (TCP соединение приравнено к файлу).

    Это значит — сервер созданный на Go следующим образом

    http.ListenAndServe(":8080", nil)
    

    Будет постепенно накапливать не закрытые соединения и захлебнется ими через день, месяц или год.

    Поэтому важно устанавливать Timeout, которые в обычных вебсерверах установлены по умолчанию.

    Например

    	// log.Fatal(http.ListenAndServe(":8085", nil))
    	{
    		s := &http.Server{
    			Addr:           ":8085",
    			Handler:        nil,
    			ReadTimeout:    1000 * time.Second, 
    			WriteTimeout:   1000 * time.Second,
    			MaxHeaderBytes: 1 << 20,
    		}
    
    		log.Fatal(s.ListenAndServe())
    	} 
    

    Второй недочёт параметр, который в обычном вебсервере часто указывается это максимальное количество соединений, которое вебсервер готов открыть. В случаи достижения этого ограничения вебсервер начинает жертвовать keep-alive соединениями или выдавать ошибку и закрывать соединения.

    Go по умолчанию такого не делает, и вообще не очевидно как в Go ограничить количество соединений или посчитать количество соединений или тем более управлять этим процессом.

    Что бы разобраться с тем, как управлять соединениями нужно заглянуть в исходники func (*Server) ListenAndServe и func (*Server) ListenAndServeTLS, тогда мы увидим, что обе функции используют функцию func (*Server) Serve.

    Эта функция получает интерфейс net.Listener как аргумент.
    Вот его мы и можем реализовать для ограничения и контроля соединений.
    Пример обвертки вокруг интерфейса net.Listener является LimitListener

    Пройдя по статьям на хабре с тегом [go] я не нашел примеров в, которых устанавливались Timeout, DeadLine или контролировалось количество соединений.

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

    Похожие публикации

    Комментарии 28
      0
      Такие вещи должны быть в конфигурации по умолчанию. Было бы неплохо, если бы вы поговорили об этом с ответственными за http библиотеку.
        0
        Мой коллега писал о проблеме повисших коннектов больше года назад, но не так конечно — Роб Пайк ты сделал ошибку, исправь её. Разработчики Go прекрасно знают, что timeout=0 и этот 0 их устраивает. Интересно аналогичное значение в других похожих проектах.
        В торнадо на питоне вот такое нашел. У меня нет опыта использования торнадо, не аналогичное ли это значение?
        0
        промазал
          +1
          Огорчает еще одна вещь: отсутствие возможности переопределения ответов на ошибки, например, 400.
            0
            Я подумываю, что бы скопировать весь пакет http, переименовать в myhttp, тогда многое станет на много проще. Например, при достижении какого то количество соединений, можно будет отключать только мение приоритетные хендлеры. Например, хендлеры ответственные за живое обновление страницы (long polling), тогда под нагрузкой сайт продолжит открываться, но функционал живого обновления страницы будет отключен. Такое можно сделать и способом обвертки net.Listener, но каждый хендлер должен будет кое, что о себе сообщать Listener.

            Если скопировать пакет http, то можно и ошибки заодно переопределить.

            Еще чего не хватает в Go это OpenSSL врапера. SSL в Go реализован на Go. OpenSSL поддерживает аппаратное ускорение криптографии, встроенное в современные процессоры. Поэтому, классно было бы, если в Go было две реализации SSL.
              0
              Не защищаю Go, но разве в случае достижения высокой загрузки процессора не следует запускать несколько инстансов приложения на разных серверах и делать балансировку? IMHO, шифрование в целом будет занимать малую долю процессорного времени в сравнении со временем генерации веб страницы. А в случае балансировки нагрузки шифрование лучше производить на балансировщике нежели на каждом отдельном сервере.
                0
                Я как раз разрабатываю балансировщик(прокси) написанный на Go. Теоретически балансировщик может шифровать соединения с двух сторон.
                  0
                  Я понимаю возможную необходимость такого решения, но разве не нашлось существующих железных или других софтверных решений которые это могут сделать без разработки нового колеса, хоть и со стразами?
                    0
                    Еще есть нюанс, что постоянно устанавливать защищенное соединение с нижестоящими нодами очень дорого. Я бы посмотрел в сторону решения, когда каждая активная нода сама будет устанавливать некое VPN соединение с балансировщиком, а траффик бы ходил только по HTTP. В это случае у вас был бы автоматический пул присоедененных серверов, и конфигурация балансировщика была бы константной без необходимости расширять пул активных нод каким либо способом.
                      0
                      1-4 соединения между балансировщиком и каждым инстансом. Сам инстанс это lighttpd+fastcgi php на 8 процессов. 90% запросов обрабатывает один процесс. Каждый инстан одноядерный облачный сервер. Одноядерный сервер выгоднее по деньгам. Соединения устанавливаются редко. Большая часть запросов это идет через keep-alive соединение
                      0
                      Первый балансировщик был написан за 4 часа, когда сайту пришел кирдык. Ни кто не посказал, что можно наш трафик проксировать, чем то. Сейчас он прокси развился. Имеет АПИ, отчеты, фильтры и логи. Самое сложное это определить от какого пользователя пришел запрос. Запросы от каждого пользователя обрабатывает, конкретный сервер. Пользователя можно мигрировать с одного сервера на другой.
                      Пример
                      Пользователи от 1 до 100 обрабатывает сервер А
                      Пользователи от 101 до 200 обрабатывает сервер Б

                      Потом пользователь 51 перемещается на сервер В

                      Пользователи от 1 до 50 и от 52 до 100 обрабатывает сервер А
                      Пользователи от 101 до 200 обрабатывает сервер Б
                      Пользователя 51 обрабатывает сервер В

                      Самих балансировщиков тоже может быть несколько и они максимально stateless

                      Еще вот добавляем задержка запроса. Например, на сервере А мы перезапускаем апач или мускл и прокси запросы принимает, но не отсылает. Сервер перезапустился и прокси начинает отправлять задержанные запросы.
                        0
                        Если вы все запускаете на амазоне, там есть красивое решение для таких вещей.
                        На одном из моих проектов работал автоскейлинг. Если возрастает нагрузка, всегда можно спать спокойно.
                        Как минимум ранается 10 серверов, но если нагрузка растет запросто автоматом подниметься 1000 и 100000 дополнительных серверов. Хотя пока пики были только до 1000. Получается и дешево и сердито.
                          0
                          1000 серверов! Не знаю кто уснет платя амазону за 1000 серверов. Разве, что под прикрытием сибазона.
                            0
                            Если в производстве задействовано 1000 серверов, эти сервера должны приносить гораздо больше.
                            Кроме того стоимость выделения и возвращения этих серверов в пул стоит совсем мало в сравнении с содержанием и поддержкой своего датацентра, поддерживающего персонала, поддержка и оплата простаивающих мощностей на периоды простоя. А если задуматься, про энергетическую стабильность, про резервированые и ОЧЕНЬ широкие каналы, охрану, пропуска, оренду территории бла-бла-бла… то использование амазона обходится еще ой как дешево, точнее почти бесплатно.

                            зы. Мы сильно отклонились от изначальной темы.
                              0
                              Да, но все же. Это очень круто! www.quora.com/Amazon-EC2/What-are-the-largest-consumer-sites-that-run-on-EC2
                              Вот в Украине есть такой большой сайт ex.ua, но если бы он хостился на amazon он вряд ли мог свести концы.
                                0
                                Конечно, на пиратстве много не заработаешь, зато легко.
                                  0
                                  Когда он хостился в Украине то чуть почти не свел концы с концами. Neflix хостится на Amazon и как то сводит концы с концами, и у них то автоскейлинг работает на ура. И вообще самый главный плюс автоскейлинга — это «скейлдаун», так как именно он позволяет экономить деньги.
                +5
                Мне кажется, что Go в любом случае не будет смотреть напрямую в мир и перед ним будет стоять, например nginx, который и в SSL завернет и таймауты проверит.
                  +1
                  Конечно вам ни что не мешает поставить перед Go ngnix, но это не обязательно. У нас балансировщик написан на Go, а приложение на lighttpd+php, java и go.
                    +2
                    Было бы интересно почитать статью о построении такой архитектуры, с примерами кода на «связках».
                      +1
                      Я могу опубликовать, примерно, то что сварганили за 4 часа как первый прокси Go там всего 200 строк и повторить, что сказано в комментах. Не уверен, что достаточно для поста и так кто-то за этот пост минуснул. Чувствую уйду в минус вообще. За то стану видным тролем.
                        +3
                        Опубликовал балансировщика
                    +1
                    Крутое и полезное изыскание! Респект.

                    Но как очень верно заметил iamwizard, go-приложение стоит чаще всего за сервером (lighttpd или nginx), как минимум для того чтобы статику отдавать. И имея перед собой фронт-энд приложение не занимается всеми этими штуками: таймауты, соединения etc.
                      +2
                      Представим приложения аналог ВКонтакте на Go. Один пользователь размещает пост и у всех друзей обновляется стенка моментально. То есть если у нас одновременно 500 человек открыли страницу, то у нас 500 соединений. 500 гоурутин ожидают новостей. если мы поставим между Go и пользователями ngnix, то у нас будет те же 500 соединений между Go и ngnix и еще 500 соединений между ngnix и пользователями. В статье я как раз акцентировал внимание на серверах с long polling. 3 дня назад была статья про центрифугу, вряд ли есть смысл размещать приложение на Tornado как центрифуга или приложение на Go, за ngnix. Go, Tornado, Node.js претенденты на решение проблемы c10k как и ngnix.
                        0
                        dl.google.com — написан на Go. Раздавать статику это наверное одна из самых сильных сторон ngnix. Но в Google полагают, Go так же достойных кандидат.
                        golang.org/doc/faq#Is_Google_using_go_internally
                        0
                        Go и базовая библиотека net/http и не должны контролировать количество максимальное количество соединений. Это задача программиста.
                        На счет таймаутов, да, одобрям-с. Нелишняя ремарка.
                          +2
                          Я и описал в статье как программист может это контролировать. Разве нет? Спасибо за одобрям-с
                          +1
                          Так пример такого ограничения есть в документации:
                          s := &http.Server{
                          	Addr:           ":8080",
                          	Handler:        myHandler,
                          	ReadTimeout:    10 * time.Second,
                          	WriteTimeout:   10 * time.Second,
                          	MaxHeaderBytes: 1 << 20,
                          }
                          log.Fatal(s.ListenAndServe())
                          

                          Собственно ваш пример очень похож на приведенный.

                          Когда я писал статью про Web-приложение на Go, я руководствовался лишь мыслью о платформонезависимой GUI-библиотекой для Go, поэтому и не задумывался о подобном. Но все равно спасибо за статью, интересно было почитать.

                          А вообще, все эти ListenAndServe являются обертками, для простоты написания. Никто не запрещает писать так:
                          	l, err := net.Listen("tcp", ":2000")
                          	if err != nil {
                          		log.Fatal(err)
                          	}
                          	defer l.Close()
                          	for {
                          		conn, err := l.Accept()
                          		if err != nil {
                          			log.Fatal(err)
                          		}
                          
                          		go func(c net.Conn) {
                          			io.Copy(c, c)
                          			c.Close()
                          		}(conn)
                          	}
                          


                          Где можно легко контролировать соединения вручную.

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

                          Самое читаемое