Как разработчик программного обеспечения, вам постоянно приходится иметь дело с базами данных, сторонними приложениями и другими службами. И иногда, если разрабатываемое вами приложение предполагает большую активность, вам придется дважды подумать об архитектуре вашего приложения, потому что каждое приложение не будет вести себя одинаково при масштабировании. Поэтому весьма вероятно, что вы окажетесь в ситуации, когда вам захочется уменьшить количество соединений, которые вы открываете со своей базой данных, сократить количество раз, когда вы звоните в эту дорогостоящую стороннюю службу и т. д. Я был в такой же ситуации. Итак, я расскажу вам о решении, которое я нашел, которое будет полезно в таких сценариях.
Прежде всего, давайте рассмотрим простой веб-сервер go.
Пример 1
package main import ( "fmt" "io" "log" "net/http" "time" ) var cost int64 func main() { http.HandleFunc("/fetch_data", GetDataHandler) log.Println("Serving Port : 8000") http.ListenAndServe(":8000", nil) } func GetDataHandler(w http.ResponseWriter, r *http.Request) { fmt.Println(r.URL) data, _ := GetData() io.WriteString(w, data) } func GetData() (string, error) { time.Sleep(5 * time.Second) cost += 100 fmt.Printf("Cost : %d \n", cost) return fmt.Sprintf("Fetch called : %d", cost), nil }
Этот сервер имеет конечную точку с именем /fetch_data, которая инициирует вызов функции GetData(). Предположим, что эта функция GetData() представляет вызов базы данных или сторонний вызов, который мы хотели бы сделать. В этой функции GetData() я сделал ее спящей на 5 секунд, чтобы сделать вызов более реалистичным. Также каждый раз, когда вызывается эта функция, стоимость глобальной переменной увеличивается на 100. Это показывает, что каждый вызов GetData() стоит нам 100 долларов.
Чтобы проверить, как наш сервер справляется с интенсивным трафиком, я буду использовать инструмент нагрузочного тестирования (который также написан на Go) под названием Vegeta (https://github.com/tsenart/vegeta). В качестве нашего первого теста я буду вызывать конечную точку /fetch_data в течение 5 секунд со скоростью 10 запросов в секунду. Теперь запустим тест.
vegeta attack -targets=targets_normal.txt -duration=5s -rate=10 | tee results_normal.bin | vegeta report
target_normal.txt содержит тестируемые методы REST и URL-адреса.
GET http://localhost:8000/fetch_data
Теперь давайте посмотрим на результаты.
Requests [total, rate] 50, 10.20 Duration [total, attack, wait] 9.909676807s, 4.903610868s, 5.006065939s Latencies [mean, 50, 95, 99, max] 5.005698559s, 5.006212501s, 5.008185721s, 5.013624665s, 5.013624665s Bytes In [total, mean] 941, 18.82 Bytes Out [total, mean] 0, 0.00 Success [ratio] 100.00% Status Codes [code:count] 200:50 Error Set:
результаты показывают, что выполнено 50 успешных запросов. И если мы посмотрим на журналы сервера, мы увидим, что общая стоимость этих 50 запросов составила 5000 долларов США. Текущий дизайн приложения вызывает GetData() каждый раз, когда приходит запрос, даже если все запросы абсолютно одинаковы. Что, если мы сможем один раз выполнить функцию GetData() и удовлетворить все запросы?
Вот тут-то и пригодится пакет sync/singleflight.
Пример 2
package main import ( "fmt" "io" "log" "net/http" "time" "golang.org/x/sync/singleflight" ) var cost int64 func main() { var requestGroup singleflight.Group http.HandleFunc("/fetch_data", RequestGroup{r: &requestGroup}.GetDataHandler) log.Println("Serving Port : 8001") http.ListenAndServe(":8001", nil) } type RequestGroup struct { r *singleflight.Group } func (rg RequestGroup) GetDataHandler(w http.ResponseWriter, r *http.Request) { fmt.Println(r.URL) value, err, _ := rg.r.Do(r.URL.String(), func() (interface{}, error) { go func() { time.Sleep(5000 * time.Millisecond) rg.r.Forget(r.URL.String()) }() return GetData() }) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } data := value.(string) io.WriteString(w, data) } func GetData() (string, error) { time.Sleep(5 * time.Second) cost += 100 fmt.Printf("Cost : %d \n", cost) return fmt.Sprintf("Fetch called : %d", cost), nil }
Мы будем использовать функцию Do() в пакете singleflight. Функция Do() принимает строку в качестве ключа и функцию, которая возвращает ошибку, и интерфейс в качестве входных данных. Singleflight гарантирует, что для данного ключа произойдет только одно выполнение переданной функции. Что касается ключа, я буду использовать URL-адрес запроса. В данном случае это будет /fetch_data. В функции Do() мы запустим процедуру go в фоновом режиме, а затем попросим singleflight забыть запрос через 5 секунд. Это означает, что любой запрос, который поступает в течение 5 секунд после поступления первого запроса, будет обслужен с тем же ответом от функции GetData().
Теперь запустим тот же нагрузочный тест и посмотрим, как поведет себя приложение.
vegeta attack -targets=targets_singleflight.txt -duration=5s -rate=10 | tee results_singleflight.bin | vegeta report
Давайте посмотрим на результаты.
Requests [total, rate] 50, 10.20 Duration [total, attack, wait] 5.014053656s, 4.900833893s, 113.219763ms Latencies [mean, 50, 95, 99, max] 2.561878061s, 2.560777702s, 4.8108588s, 5.013957585s, 5.013957585s Bytes In [total, mean] 900, 18.00 Bytes Out [total, mean] 0, 0.00 Success [ratio] 100.00% Status Codes [code:count] 200:50 Error Set:
Все выглядит очень похоже (хотя тест на сервере с singleflight занял меньше времени для обработки 100 запросов. И это хорошо :) ) по сравнению с предыдущим тестом. Но когда мы проверяем журналы сервера, мы видим, что по сравнению с 5000 долларов США на этот раз мы потратили только 100 долларов США. Это довольно значительная экономия средств. Давайте запустим нагрузочный тест на 10 секунд для обоих приложений.
---- Starting attack ----- ---- Attacking Datasource Server ---- Requests [total, rate] 100, 10.10 Duration [total, attack, wait] 14.904334343s, 9.902888604s, 5.001445739s Latencies [mean, 50, 95, 99, max] 5.00261956s, 5.002215223s, 5.00566831s, 5.009361413s, 5.012719579s Bytes In [total, mean] 1892, 18.92 Bytes Out [total, mean] 0, 0.00 Success [ratio] 100.00% Status Codes [code:count] 200:100 Error Set: ---- Attacking Datasource with singleflight ---- Requests [total, rate] 100, 10.10 Duration [total, attack, wait] 10.106814205s, 9.903963803s, 202.850402ms Latencies [mean, 50, 95, 99, max] 2.555588866s, 2.556200382s, 4.807365963s, 5.008225608s, 5.014383781s Bytes In [total, mean] 1800, 18.00 Bytes Out [total, mean] 0, 0.00 Success [ratio] 100.00% Status Codes [code:count] 200:100 Error Set: ---- Ending attack -----
В этом случае приложение без одиночной проверки обошлось нам в 10 000 долл. США, тогда как другое приложение с однократной отправкой обошлось всего в 200 долларов США.
Последние мысли ..
Я был очень удивлен, обнаружив, что примеров/статей об этом пакете всего несколько. Надеюсь, вы найдете эту статью полезной в своих ежедневных приключениях по решению проблем.
Весь код для этой статьи находится в этом репозитории.