Низкая производительность html/ шаблона в 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 использует 5k одновременных соединений в секунду в течение 1 минуты. Проблема в том, что через несколько секунд после запуска теста я получаю высокую среднюю задержку (почти 10 с), и в результате 5000 успешных ответов, и тест останавливается, потому что он достигает 50% ошибок (тайм-ауты).

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

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

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

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

Мой шаблон кода (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)

6 ответов

Решение

Есть две основные причины, по которым эквивалентное приложение использует 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 или нет. Когда вы добавляете код для работы с базой данных в обработчик запросов, я подозреваю, что время визуализации шаблона вряд ли будет заметно.

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

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

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


Также вы используете только 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 Post с выделяется.

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)
}

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

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

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

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

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

Типизированные строки — ваш друг, если вы хотите ускорить html/template. Иногда бывает полезно предварительно отрендерить повторяющиеся HTML-фрагменты.

Предполагая, что большая часть времени тратится на рендеринг этих 100 объектов Post, может иметь смысл выполнить их предварительный рендеринг.

Другие вопросы по тегам