Низкая производительность html/template в Go lang, есть обходной путь?

Я тестирую (с loader.io) этот тип кода в Go, чтобы создать массив из 100 элементов вместе с некоторыми другими базовыми переменными и проанализировать их все в шаблоне:

package main

import (
    "html/template"
    "net/http"
)

var templates map[string]*template.Template

// Load templates on program initialisation
func init() {
    if templates == nil {
        templates = make(map[string]*template.Template)
    }

    templates["index.html"] = template.Must(template.ParseFiles("index.html"))
}

func handler(w http.ResponseWriter, r *http.Request) {
    type Post struct {
        Id int
        Title, Content string
    }

    var Posts [100]Post

    // Fill posts
    for i := 0; i < 100; i++ {
        Posts[i] = Post{i, "Sample Title", "Lorem Ipsum Dolor Sit Amet"}
    }

    type Page struct {
        Title, Subtitle string
        Posts [100]Post
    }

    var p Page

    p.Title = "Index Page of My Super Blog"
    p.Subtitle = "A blog about everything"
    p.Posts = Posts

    tmpl := templates["index.html"]

    tmpl.ExecuteTemplate(w, "index.html", p)
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8888", nil)
}

В моем тесте с Loader используется 5 тыс. одновременных подключений/с в течение 1 минуты. Проблема в том, что буквально через несколько секунд после запуска теста я получаю высокую среднюю задержку (почти 10 с) и в результате 5к успешных ответов и тест останавливается, потому что достигает 50% Error Rate (тайм-ауты).

На той же машине PHP дает 50к+.

Я понимаю, что это не проблема производительности Go, а, вероятно, что-то связанное с html/шаблоном. Конечно, Go может легко справляться с достаточно сложными вычислениями намного быстрее, чем что-либо вроде PHP, но когда дело доходит до синтаксического анализа данных в шаблоне, почему это так ужасно?

Любые обходные пути, или, возможно, я просто делаю это неправильно (я новичок в Go)?

P.S. На самом деле даже с 1 пунктом это точно так же... 5-6k и остановка после огромного количества тайм-аутов. Но это, вероятно, потому, что массив с сообщениями остается той же длины.

Код моего шаблона (index.html):

{{ .Title }}
{{ .Subtitle }}

{{ range .Posts }}
        {{ .Title }}
        {{ .Content }}
{{ end }}

Вот результат профилирования github.com/pkg/profile:

root@Test:~# go tool pprof app /tmp/profile311243501/cpu.pprof
Possible precedence issue with control flow operator at /usr/lib/go/pkg/tool/linux_amd64/pprof line 3008.
Welcome to pprof!  For help, type 'help'.
(pprof) top10
Total: 2054 samples
      97   4.7%   4.7%      726  35.3% reflect.Value.call
      89   4.3%   9.1%      278  13.5% runtime.mallocgc
      85   4.1%  13.2%       86   4.2% syscall.Syscall
      66   3.2%  16.4%       75   3.7% runtime.MSpan_Sweep
      58   2.8%  19.2%     1842  89.7% text/template.(*state).walk
      54   2.6%  21.9%      928  45.2% text/template.(*state).evalCall
      51   2.5%  24.3%       53   2.6% settype
      47   2.3%  26.6%       47   2.3% runtime.stringiter2
      44   2.1%  28.8%      149   7.3% runtime.makeslice
      40   1.9%  30.7%      223  10.9% text/template.(*state).evalField

Это результаты профилирования после уточнения кода (как предложено в ответе icza):

root@Test:~# go tool pprof app /tmp/profile501566907/cpu.pprof
Possible precedence issue with control flow operator at /usr/lib/go/pkg/tool/linux_amd64/pprof line 3008.
Welcome to pprof!  For help, type 'help'.
(pprof) top10
Total: 2811 samples
     137   4.9%   4.9%      442  15.7% runtime.mallocgc
     126   4.5%   9.4%      999  35.5% reflect.Value.call
     113   4.0%  13.4%      115   4.1% syscall.Syscall
     110   3.9%  17.3%      122   4.3% runtime.MSpan_Sweep
     102   3.6%  20.9%     2561  91.1% text/template.(*state).walk
      74   2.6%  23.6%      337  12.0% text/template.(*state).evalField
      68   2.4%  26.0%       72   2.6% settype
      66   2.3%  28.3%     1279  45.5% text/template.(*state).evalCall
      65   2.3%  30.6%      226   8.0% runtime.makeslice
      57   2.0%  32.7%       57   2.0% runtime.stringiter2
(pprof)

person uiwe83    schedule 11.07.2015    source источник
comment
Можете ли вы проверить, является ли Go 1.5 beta 1 (golang.org/dl/#go1.5beta1 ) ничего не меняет? (в случае, если здесь проблема со скоростью сборщика мусора)   -  person VonC    schedule 11.07.2015
comment
Да. Результаты такие же.   -  person uiwe83    schedule 12.07.2015
comment
Посмотрите на профилирование с помощью github.com/pkg/profile и публикацию топ-10 из профиля ЦП. Также было бы полезно узнать, работает ли ваш шаблон с вводом? Я предполагаю, что он просто перебирает входной массив.   -  person elithrar    schedule 12.07.2015
comment
Передача указателя на .Execute должна немного ускорить его.   -  person OneOfOne    schedule 12.07.2015
comment
@elithrar, как мне прочитать файл? В конце концов я получил /tmp/profile*****/cpu.pprof, но он не читается...   -  person uiwe83    schedule 12.07.2015
comment
@uiwe83 uiwe83 go tool pprof $YOURBINARY cpu.pprof, а затем введите top10 в командной строке и опубликуйте результаты в своем вопросе. Я подозреваю много вызовов makeSlice.   -  person elithrar    schedule 12.07.2015
comment
Как вы запускаете свое PHP-приложение?   -  person kostya    schedule 14.07.2015
comment
Также, учитывая, что профилировщик показывает 35,5% времени, затраченного на reflect.Value.call, было бы интересно посмотреть, как изменится производительность, когда структуры Post и Page заменяются картами (как в вашем примере PHP).   -  person kostya    schedule 14.07.2015
comment
@kostya Я полагаю, вы уже видели мой код из поста на Reddit, так что ... ;) Да, я обязательно попробую заменить на карты, причина, по которой я выбрал структуры, заключается в том, что я впервые заметил их и с самого начала любил строить их.   -  person uiwe83    schedule 14.07.2015
comment
@ uiwe83 uiwe83, да, я видел ваш пост на Reddit, хотя у меня нет там учетной записи;) Я бы тоже предпочел использовать структуры, поскольку код чище и безопаснее. Использование структур также намного быстрее, если все свойства разрешены во время компиляции. Мне просто было любопытно, насколько сильно карты повлияют на производительность.   -  person kostya    schedule 14.07.2015
comment
@kostya Я понимаю, что код PHP довольно прост, пытался сделать его как можно более похожим на Go и в итоге получил этот код с использованием карт (хотя в PHP я использую массив массивов, и они даже не ассоциативны, но я не удалось воссоздать его в Go): pastebin.com/1ckqEDYZ — бенчмаркинг дает около 5 000 успешных результатов, всего как в моей первой попытке - даже с текстом/шаблоном.   -  person uiwe83    schedule 15.07.2015


Ответы (5)


Есть две основные причины, по которым эквивалентное приложение, использующее html/template, работает медленнее, чем вариант PHP.

Во-первых, html/template предоставляет больше возможностей, чем PHP. Основное отличие состоит в том, что html/template будет автоматически экранировать переменные, используя правильные правила экранирования (HTML, JS, CSS и т. д.) в зависимости от их местоположения в результирующем выводе HTML (что, я думаю, довольно круто!).

Во-вторых, код рендеринга html/template активно использует рефлексию и методы с переменным числом аргументов, и они просто не так быстры, как статически скомпилированный код.

Под капотом следующий шаблон

{{ .Title }}
{{ .Subtitle }}

{{ range .Posts }}
    {{ .Title }}
    {{ .Content }}
{{ end }}

преобразуется во что-то вроде

{{ .Title | html_template_htmlescaper }}
{{ .Subtitle | html_template_htmlescaper }}

{{ range .Posts }}
    {{ .Title | html_template_htmlescaper }}
    {{ .Content | html_template_htmlescaper }}
{{ end }}

Вызов html_template_htmlescaper с использованием отражения в цикле убивает производительность.

Сказав все это, этот микротест html/template не должен использоваться, чтобы решить, использовать ли Go или нет. Как только вы добавите код для работы с базой данных в обработчик запросов, я подозреваю, что время отрисовки шаблона будет практически не заметно.

Также я уверен, что со временем и отражение Go, и пакет html/template станут быстрее.

Если в реальном приложении вы обнаружите, что html/template является узким местом, все еще можно переключиться на text/template и предоставить ему уже экранированные данные.

person kostya    schedule 14.07.2015
comment
Спасибо, я думаю, что и ваш ответ, и ответ @icza великолепны, хотя ваш ответ более четко отвечает на вопрос. Одна вещь, о которой я подумал, - уместно ли пытаться использовать горутины/каналы и поможет ли это, если все еще использовать html/шаблон? - person uiwe83; 14.07.2015
comment
http.ListenAndServer создает одну горутину для каждого запроса, поэтому рендеринг одного и того же шаблона для разных подключений происходит параллельно. - person kostya; 14.07.2015
comment
Я имею в виду, будет ли это полезно, если я буду использовать их для внутренних циклов (где я их создаю, заполняю данными и т. д.)? И если да, не могли бы вы привести пример на основе моего кода? Извините за вопрос не по теме и слишком много прошу! Сейчас я борюсь с этим, но не хочу создавать новый вопрос по этому поводу. Пока это мой код: pastebin.com/9NBQ9xY8 - person uiwe83; 14.07.2015
comment
Я не думаю, что это можно ускорить, используя больше горутин. Подумайте об этом таким образом. Горутины могут сделать некоторый код быстрее, только если код можно распараллелить и выполнить на нескольких ядрах одновременно. В этом случае алгоритм рендеринга шаблонов не может быть распараллелен, но, что более важно, все ядра процессора уже заняты рендерингом шаблонов для других подключений и не имеют возможности делать что-либо еще. Как правило, если приложение связано с вводом-выводом, добавление большего количества горутин может помочь, если приложение привязано к процессору, добавление большего количества горутин может даже замедлить работу приложения. - person kostya; 14.07.2015
comment
Интересно, я вижу. Интересный краткий обзор, между прочим, я обновил тестовую машину до 2 ядер и 4 ГБ ОЗУ, загрузил тем же стресс-тестированием и получил 300 из 300 000 возможных ответов на php, в то время как только вдвое больше (я использовал max procs) - около 120к, с Го. Приложение Go загружает оба ядра на 100% каждое во время обработки... Я не знаю, почему... - person uiwe83; 14.07.2015

Вы работаете с массивами и структурами, которые не являются типами указателей и не являются дескрипторами (например, срезами, картами или каналами). Таким образом, их передача всегда создает копию значения, а присвоение значения массива переменной копирует все элементы. Это медленно и дает огромный объем работы сборщику мусора.


Кроме того, вы используете только 1 ядро ​​процессора. Чтобы использовать больше, добавьте это в свою функцию main():

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(":8888", nil))
}

Изменить: Так было только до Go 1.5. Так как Go 1.5 runtime.NumCPU() используется по умолчанию.


Ваш код

var Posts [100]Post

Выделен массив с местом для 100 Posts.

Posts[i] = Post{i, "Sample Title", "Lorem Ipsum Dolor Sit Amet"}

Вы создаете значение Post с составным литералом, затем это значение копируется в i элемент массива. (избыточный)

var p Page

Это создает переменную типа Page. Это struct, поэтому выделяется его память, которая также содержит поле Posts [100]Post, поэтому выделяется еще один массив из 100 элементов.

p.Posts = Posts

Это копирует 100 элементов (сотню структур)!

tmpl.ExecuteTemplate(w, "index.html", p)

Это создает копию p (типа Page), поэтому создается еще один массив из 100 сообщений и копируются элементы из p, затем он передается в ExecuteTemplate().

А так как Page.Posts это массив, то скорее всего при его обработке (переборе в шаблонизаторе) из каждого элемента будет делаться копия (не проверял - не проверял).

Предложение по более эффективному коду

Некоторые вещи для ускорения вашего кода:

func handler(w http.ResponseWriter, r *http.Request) {
    type Post struct {
        Id int
        Title, Content string
    }

    Posts := make([]*Post, 100) // A slice of pointers

    // Fill posts
    for i := range Posts {
        // Initialize pointers: just copies the address of the created struct value
        Posts[i]= &Post{i, "Sample Title", "Lorem Ipsum Dolor Sit Amet"}
    }

    type Page struct {
        Title, Subtitle string
        Posts []*Post // "Just" a slice type (it's a descriptor)
    }

    // Create a page, only the Posts slice descriptor is copied
    p := Page{"Index Page of My Super Blog", "A blog about everything", Posts}

    tmpl := templates["index.html"]

    // Only pass the address of p
    // Although since Page.Posts is now just a slice, passing by value would also be OK 
    tmpl.ExecuteTemplate(w, "index.html", &p)
}

Пожалуйста, протестируйте этот код и сообщите о результатах.

person icza    schedule 12.07.2015
comment
Спасибо. Это очень интересно (правильное управление указателями, адресами и т. д.) и имеет для меня большой смысл. Удивительно, но я тестировал этот код еще несколько раз и все еще застрял на барьере 5-6k. Я обновлю вопрос с результатами инструмента профилирования. Я использую только одно ядро ​​​​на своей тестовой машине (поскольку у него всего 1), но все еще использую runtime.GOMAXPROCS. - person uiwe83; 12.07.2015
comment
Posts := make([]Post, 100) (кусок сообщений) может быть более эффективным в этом случае. - person kostya; 13.07.2015
comment
@kostya Возможно. Я колебался, что выбрать, потому что заполнение массива кажется быстрее, если элементы являются указателями, а в случае не указателей структуры будут скопированы. Также при обработке среза простой for range также копировал каждый элемент в переменную цикла. Нужен бенчмаркинг. - person icza; 13.07.2015
comment
В шутку мне посоветовали поместить создание и заполнение переменных в init(), хотя это все еще не решило проблему с ExecuteTemplate. Единственный способ, который на данный момент действительно улучшает производительность (155 тысяч обращений), — это кэширование шаблона в байтовом буфере при запуске программы, но это означает, что данные, загружаемые по запросу пользователя, всегда одни и те же. - person uiwe83; 13.07.2015
comment
Замечательно, что в Go есть библиотека для работы с шаблонами, похоже, если я использую какой-нибудь аналогичный пакет шаблонов в PHP (с этими причудливыми функциями безопасности), результаты могут быть такими же, не уверен. Тем не менее, во многих случаях я не думаю, что они мне нужны, потому что не будет никакого пользовательского контента, почему я должен так сильно терять производительность на простых {{ range }} моих сообщений в блоге. - person uiwe83; 13.07.2015
comment
Можете ли вы провести быстрый тест, заменив html/template на text/template, чтобы измерить снижение производительности из-за экранирования HTML? - person kostya; 14.07.2015
comment
@kostya да, я сделал, производительность значительно улучшилась до уровня PHP, хотя я знаю, что это стоит мне потери преимуществ автоматического экранирования. - person uiwe83; 14.07.2015

html/template работает медленно, потому что использует reflection, который еще не оптимизирован для скорости.

Попробуйте quicktemplate в качестве обходного пути медленного html/template. В настоящее время quicktemplate более чем в 20 раз быстрее, чем html/template, согласно эталонному тесту из его исходного кода.

person valyala    schedule 16.03.2016

PHP не отвечает на 5000 запросов одновременно. Запросы мультиплексируются в несколько процессов для последовательного выполнения. Это позволяет более эффективно использовать как ЦП, так и память. 5000 одновременных соединений могут иметь смысл для брокера сообщений или аналогичного, выполняющего ограниченную обработку небольших фрагментов данных, но мало смысла для любой службы, выполняющей реальный ввод-вывод или обработку. Если ваше приложение Go не находится за прокси какого-либо типа, который будет ограничивать количество одновременных запросов, вы захотите сделать это самостоятельно, возможно, в начале вашего обработчика, используя буферизованный канал или группу ожидания, а-ля https://blakemesdag.com/blog/2014/11/12/limiting-go-concurrency/.

person Brent Rowland    schedule 13.07.2015

Существует эталонный тест шаблона, который вы можете проверить на странице goTemplateBenchmark. Лично я считаю, что Hero лучше всего сочетает эффективность и удобочитаемость.

person João Wiciuk    schedule 23.01.2020