DevGang
Авторизоваться

Шаблоны параллелизма Go: Context

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

В Google мы разработали пакет context, который упрощает передачу значений области запроса, сигналов отмены и крайних сроков через границы API всем горутинам, участвующим в обработке запроса. Пакет общедоступен как context. В этой статье описывается, как использовать пакет, и приводится полный рабочий пример.

Контекст

Ядро пакета context - это тип Context:

// Контекст передает крайний срок, сигнал отмены и значения области запроса 
// через границы API. Его методы безопасны для одновременного использования 
// 
несколькими горутинами. type Context interface { 
// Done возвращает канал, который закрывается, когда этот контекст отменяется 
// или истекает время ожидания. 
    Done() <-chan struct{}

// Err указывает, почему этот контекст был отменен после 
    закрытия 
// канала Done .
    Err() error

    // Крайний срок возвращает время, когда этот контекст будет отменен, если таковой имеется.
    Deadline() (deadline time.Time, ok bool)

    // Value возвращает значение, связанное с ключом, или nil, если его нет.
    Value(key interface{}) interface{}
}

Метод Done возвращает канал, который действует как сигнал аннулирования аккаунта функций, работающих от имени Context: когда канал закрыт, функции должны отказаться от своей работы и возвращения. Метод Err возвращает ошибку, указывающую, почему Context было отменено. 

Context не имеет метода Cancel  по той же причине, по которой канал Done является только приемным: функция, принимающая сигнал отмены, обычно не является той, которая посылает сигнал. В частности, когда родительская операция запускает горутины для подопераций, эти подоперации не должны иметь возможности отменить родительскую операцию. Вместо этого функция WithCancel (описанная ниже) позволяет отменить новое значение Context.

Context безопасен для одновременного использования несколькими горутинами. Код может передать одиночный код Context любому количеству горутин и отменить его Context чтобы сигнализировать всем им.

Метод Deadline позволяет функциям определить, следует ли им вообще начинать работу; если осталось слишком мало времени, это может оказаться нецелесообразным. Код также может использовать крайний срок для установки тайм-аутов для операций ввода-вывода.

Value позволяет Context переносить данные в пределах запроса. Эти данные должны быть безопасными для одновременного использования несколькими горутинами.

Производные контексты

Пакет context предоставляет функции для Dérivé новых значений Context из уже существующих. Эти значения образуют дерево: когда Context отменяется, все производные Contexts от него также отменяются.

Background это корень любого дерева Context; никогда не отменяется:

// Фон возвращает пустой контекст. Он никогда не отменяется, у него нет сроков 
// и нет значений. Фон обычно используется в main, init и tests, 
// а также в качестве контекста верхнего уровня для входящих запросов.
func Background() Context

WithCancel и WithTimeout возвращают производные значения Context, которые можно отменить раньше, чем родительские ContextContext связанные с входящим запросом, как правило, отменяются, когда возвращается обработчик запросов. WithCancel также полезно для отмены избыточных запросов при использовании нескольких реплик. WithTimeout полезен для установки крайнего срока для запросов к внутренним серверам:  

// WithCancel возвращает копию родительского элемента, канал Done которого закрывается, как только 
// parent.Done закрывается или вызывается отмена.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// CancelFunc отменяет контекст. 
type CancelFunc func()

// WithTimeout возвращает копию родительского элемента, канал Done которого закрывается, как только 
// parent.Done закрывается, вызывается отмена или истекает тайм-аут. 

//Крайний срок нового контекста - это более ранний из текущих + тайм-аут и крайний срок родителя, 
// если есть. Если таймер все еще работает, функция отмены 
// освобождает его ресурсы.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithValue предоставляет собой способ связать значения в области запроса с Context:  

// WithValue возвращает копию родительского элемента, метод Value которого возвращает значение val для ключа.
func WithValue(parent Context, key interface{}, val interface{}) Context

Лучший способ увидеть, как использовать пакет context - это проработанный пример.

Пример: веб-поиск Google

Наш пример - HTTP-сервер, который обрабатывает URL-адреса, например /search?q=golang&timeout=1s, перенаправляя запрос "golang" в Google Web Search API и отображая результаты. Параметр timeout указывает серверу отменить запрос после этого продолжительность истечет.

Код разделен на три пакета:

  1. server предоставляет main функцию и обработчик для /search.
  2. userip предоставляет функции для извлечения IP-адреса пользователя из запроса и связывания его с файлом Context.
  3. Google предоставляет функцию Search для отправки запроса в Google.

Серверная программа

Программа сервера обрабатывает запросы, например /search?q=golang, обслуживая первые несколько результатов поиска Google для golang. Он регистрируется handleSearch для обработки конечной точки /search. Обработчик Context создает начальный вызов ctx и принимает меры к его отмене при возврате обработчика. Если запрос включает параметр URL timeoutContext автоматически отменяется по истечении тайм-аута:

func handleSearch(w http.ResponseWriter, req *http.Request) {
    // ctx - это Контекст для этого обработчика. Вызов cancel закрывает 
    канал
    // ctx.Done, который является сигналом отмены для запросов, 
    // запущенных этим обработчиком. 
    var (
        ctx    context.Context
        cancel context.CancelFunc
    )
    timeout, err := time.ParseDuration(req.FormValue("timeout"))
    if err == nil {
        // У запроса есть время ожидания, поэтому создайте контекст, который равен 
        / / отменяется автоматически по истечении тайм-аута. 
        ctx, cancel = context.WithTimeout(context.Background(), timeout)
    } else {
        ctx, cancel = context.WithCancel(context.Background())
    }
    defer cancel() // Cancel ctx as soon as handleSearch returns.

Обработчик извлекает запрос из запроса и извлекает IP-адрес клиента, вызывая пакет userip. IP-адрес клиента необходим для внутренних запросов, поэтому handleSearch прикрепляет его к ctx:  

 // Проверяем поисковый запрос.
    query := req.FormValue("q")
    if query == "" {
        http.Error(w, "no query", http.StatusBadRequest)
        return
    }

    // Сохраняем IP-адрес пользователя в ctx для использования кодом в других пакетах. 
    userIP, err := userip.FromRequest(req)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    ctx = userip.NewContext(ctx, userIP)

Обработчик вызовов google.Search с ctx и query:  

// Запускаем поиск Google и распечатываем результаты. 
    start := time.Now()
    results, err := google.Search(ctx, query)
    elapsed := time.Since(start)

Если поиск завершается успешно, обработчик отображает результаты:

 if err: = resultsTemplate.Execute (w, struct { 
        Results google.Results 
        Timeout, Elapsed time.Duration 
    } { 
        Results: results, 
        Timeout: timeout, 
        Elapsed: elapsed, 
    }); err! = nil { 
        log.Print (err) 
        return 
    }

Userrip пакета

Пакет userip предоставляет функции для извлечения IP-адреса пользователя из запроса и связывания его с файлом ContextContext обеспечивает сопоставление "ключ-значение", в котором ключи и значения имеют тип interface{}. Типы ключей должны поддерживать равенство, а значения должны быть безопасными для одновременного использования несколькими горутинами. Такие пакеты, как userip скрывают детали этого сопоставления и предоставляют строго типизированный доступ к определенному значению Context.

Чтобы избежать конфликтов ключей, userip определяет неэкспортированный тип key и использует значение этого типа в качестве контекстного ключа:

// Тип ключа не экспортируется, чтобы предотвратить конфликты с контекстными ключами, 
// 
определенными в других пакетах. введите ключ int

// userIPkey - это контекстный ключ для IP-адреса пользователя. Его нулевое 
// значение произвольно. Если бы в этом пакете были определены другие ключи контекста, они 
// 
имели бы разные целочисленные значения.
const userIPKey key = 0

FromRequest извлекает значение userIP из http.Request:  

func FromRequest (req * http.Request) (net.IP, error) { 
    ip, _, err: = net.SplitHostPort (req.RemoteAddr) 
    if err! = nil { 
        return nil, fmt.Errorf ("userip:% q не IP: порт ", req.RemoteAddr) 
    }

NewContext возвращает новый Context который содержит предоставленное значение userIP:  

func NewContext (ctx context.Context, userIP net.IP) context.Context { 
    return context.WithValue (ctx, userIPKey, userIP) 
}

FromContext извлекает userIP из Context:  

func FromContext(ctx context.Context) (net.IP, bool) {
    // ctx.Value возвращает nil, если ctx не имеет значения для ключа; 
    // утверждение типа net.IP возвращает ok = false для nil.
    userIP, ok := ctx.Value(userIPKey).(net.IP)
    return userIP, ok
}

Пакет google

Функция google.Search отправляет HTTP-запрос к API веб-поиска Google и анализирует результат в формате JSON. Он принимает Context параметр ctx и немедленно возвращается, если ctx.Done закрывается во время выполнения запроса.

Запрос API веб-поиска Google включает поисковый запрос и IP-адрес пользователя в качестве параметров запроса:

func Search(ctx context.Context, query string) (Results, error) {
    // Готовим запрос Google Search API.
    req, err := http.NewRequest("GET", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil)
    if err != nil {
        return nil, err
    }
    q := req.URL.Query()
    q.Set("q", query)

    // Если ctx передает IP-адрес пользователя, перенаправить его на сервер. 
    // API Google используют IP-адрес пользователя, чтобы отличать запросы, инициированные сервером, 
    // от 
запросов конечного пользователя.
    if userIP, ok := userip.FromContext(ctx); ok {
        q.Set("userip", userIP.String())
    }
    req.URL.RawQuery = q.Encode()

Search использует вспомогательную функцию httpDo для отправки HTTP-запроса и отмены его, если ctx.Done закрыт во время обработки запроса или ответа. Search передает закрытие для обработки HTTP-ответа httpDo:  

var results Results
    err = httpDo(ctx, req, func(resp *http.Response, err error) error {
        if err != nil {
            return err
        }
        defer resp.Body.Close()

        // Анализировать результат поиска JSON. 
        // https://developers.google.com/web-search/docs/#fonje 
        var data struct {
            ResponseData struct {
                Results []struct {
                    TitleNoFormatting string
                    URL               string
                }
            }
        }
        if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
            return err
        }
        for _, res := range data.ResponseData.Results {
            results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL})
        }
        return nil
    })
    // httpDo ожидает возврата предоставленного нами закрытия, поэтому 
    // читать здесь результаты безопасно.
    return results, err

Функцией httpDo выполняется запрос HTTP и обрабатывает свой ответ в новой горутине. Она отменяет запрос, если ctx.Done закрыт до выхода из горутины:  

func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
    // Выполните HTTP-запрос в горутине и передайте ответ f.
    c := make(chan error, 1)
    req = req.WithContext(ctx)
    go func() { c <- f(http.DefaultClient.Do(req)) }()
    select {
    case <-ctx.Done():
        <-c // Дождитесь возврата f.
        return ctx.Err()
    case err := <-c:
        return err
    }
}

Адаптация кода для контекстов

Многие серверные инфраструктуры предоставляют пакеты и типы для переноса значений области запроса. Мы можем определить новые реализации интерфейса Context для моста между кодом, использующим существующие фреймворки, и кодом, который ожидает параметр Context.

Например, пакет Gorilla github.com/gorilla/context позволяет обработчикам связывать данные с входящими запросами, обеспечивая сопоставление HTTP-запросов с парами ключ-значение. В gorilla.go мы предоставляем  реализацию Context, метод Value которой возвращает значения, связанные с конкретным HTTP-запросом в пакете Gorilla.

Другие пакеты предоставляют поддержку отмены, аналогичную Context. Например, Tomb предоставляет  метод Kill, который сигнализирует об отмене, закрывая канал DyingTomb также предоставляет методы ожидания выхода этих горутин, аналогичные sync.WaitGroup. В tomb.go мы предоставляем реализацию Context, которая отменяется, когда отменяется его родительский объект Context, либо объект предоставлен Tomb.

Вывод

В Google мы требуем, чтобы программисты Go передавали параметр Context в качестве первого аргумента каждой функции на пути вызова между входящими и исходящими запросами. Это позволяет коду Go, разработанному многими разными командами, хорошо взаимодействовать. Он обеспечивает простой контроль тайм-аутов и отмены и гарантирует, что критические значения, такие как учетные данные безопасности, должным образом передаются программами Go.

Фреймворки серверов, которые хотят развиваться Context должны обеспечивать реализации моста Context между своими пакетами и теми, которые ожидают параметр Context. Тогда их клиентские библиотеки примут Context от вызывающего кода. За счет создания общего интерфейса для данных в области запроса и отмены Context разработчикам пакетов становится проще делиться кодом для создания масштабируемых служб.

Источник:

#Golang #Optimisation
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

Поделитесь своим опытом, расскажите о новом инструменте, библиотеке или фреймворке. Для этого не обязательно становится постоянным автором.

Попробовать

Оплатив хостинг 25$ в подарок вы получите 100$ на счет

Получить