screenshot

Вот так выглядит среднестатистический запрос от пользователя, дело не в каком то конкретном запросе, а в масштабе происходящего, плюс что бы подсветить сколько мест передачи байтиков имеется.

Начинаем распутывать клубок с конца.

Предположим есть у нас некий сервис и он возвращает 200кб данных.

Каким бы быстрым ни был сервис, даже если там все отдается прямо из памяти, основное время займет именно передача байтиков по сети.

Делаем референс для проверки:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/kb/{kb:int}", (int kb) => new String('a', kb * 1024));
app.Run();

Таким образом у нас есть эндпоинт для проверки гипотез.

Для начала проверяем что оно в принципе работает:

curl localhost:5000/kb/2

Вернет целую кучу буковок а.

Далее делаем замер по объему:

curl -s -o /dev/null localhost:5000/kb/2 -w 'bytes: %{size_download} took: %{time_total}\n'

Вернет нам это дело ровно 2048, собственно сколько и запрашивали.

Теперь то же самое только с замером времени:

curl -s -o /dev/null localhost:5000/kb/2 -w 'bytes: %{size_download} took: %{time_total}\n'

В моем случае вернуло вот такое:

bytes: 2048 took: 0.009286

То есть, локально, мы выгрузили 2kb данных за 9ms

Далее в приложение добавляем компрессию

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddResponseCompression(); // POI
var app = builder.Build();
app.UseResponseCompression(); // POI
app.MapGet("/kb/{kb:int}", (int kb) => new String('a', kb * 1024));
app.Run();

Делаем запрос как и прежде, только на этот раз уже 200кб:

curl -s -o /dev/null localhost:5000/kb/200 -w 'bytes: %{size_download} took: %{time_total}\n'

и получаем:

bytes: 204800 took: 0.176370

любопытно, 176ms, локально, при том что сервер то по сути ничегошеньки не делает 🤷‍♂️

Теперь пробуем точно то же самое, но на этот раз говорим о том что мы готовы принимать сжатый ответ:

curl -H 'Accept-Encoding: gzip' -s -o /dev/null localhost:5000/kb/200 -w 'bytes: %{size_download} took: %{time_total}\n'

и получаем:

bytes: 935 took: 0.075951

не сказать что бы прямо космолет, но трафик сокращен в сотни раз, а это повлияло не только на время ответа которое на ровном месте стало х2 быстрее, но еще и повлияет на денюжку за трафик.

Как это все работает

Если клиент умеет работать с сжатыми ответами, в заголовках запроса он передает Accept-Encoding заголовок, который перечисляет все виды компрессии с которыми умеет работать клиент

Сервер в свою очередь, в случае использования компрессии в ответе передает заголовок Content-Encoding с указанием архиватора которым было сжато тело ответа.

По сути это то же самое что и content negotiation, только для сжатия.

На деле это выглядит так:

Передаем запрос без указания поддерживаемых архиваторов, в ответ получаем сырой ответ (в ответе нет заголовка Content-Encoding и данные "читаемые"):

curl -s -i localhost:5000/kb/1
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Sun, 16 Oct 2022 12:14:57 GMT
Server: Kestrel
Transfer-Encoding: chunked

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...

Передаем запрос с указанием того что мы умеем работать с zip:

curl -i localhost:5000/kb/1 -H 'Accept-Encoding: gzip'

В ответ мы получим вот такое:

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Sun, 16 Oct 2022 12:17:21 GMT
Server: Kestrel
Content-Encoding: gzip
Transfer-Encoding: chunked
Vary: Accept-Encoding

Warning: Binary output can mess up your terminal. Use "--output -" to tell
Warning: curl to output it to your terminal anyway, or consider "--output
Warning: <FILE>" to save to a file.

С одной стороны по заголовку Content-Encoding в ответе мы можем подтвердить что нам передали gzip, но что еще любопытнее curl предупреждаем о том что ему в ответ передали какие то байтики с которыми он не знает что делать и предлагает их сохранить в файл.

Что бы побороть это дело достаточно добавить аргумент --compressed после чего curl попробует распаковать ответ

curl -i localhost:5000/kb/1 -H 'Accept-Encoding: gzip' --compressed
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Sun, 16 Oct 2022 12:19:57 GMT
Server: Kestrel
Content-Encoding: gzip
Transfer-Encoding: chunked
Vary: Accept-Encoding

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa....

В современном мире популярен архиватор brotli, пробуем с ним:

curl -i localhost:5000/kb/1 -H 'Accept-Encoding: br' --compressed
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: Sun, 16 Oct 2022 12:24:57 GMT
Server: Kestrel
Content-Encoding: br
Transfer-Encoding: chunked
Vary: Accept-Encoding

curl: (61) Unrecognized content encoding type. libcurl understands deflate, gzip content encodings.

Как видно из сообщения, сервер, из коробки с ним работает, а вот curl не умеет.

Offtopic: как понять используется ли сжатие со стороны сервера?

using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddResponseCompression();
builder.Services.AddHttpLogging(options => options.LoggingFields = HttpLoggingFields.All); // POI
var app = builder.Build();
app.UseHttpLogging(); // POI // FIXME: I should go after compression, left here by intent to observe compressed response bodies
app.UseResponseCompression();
app.MapGet("/kb/{kb:int}", (int kb) => new String('a', kb * 1024));
app.Run();

Добавив логирование запросов мы будем видеть вот такое:

info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[1]
      Request:
      Protocol: HTTP/1.1
      Method: GET
      Scheme: http
      PathBase:
      Path: /kb/1
      Accept: */*
      Host: localhost:5000
      User-Agent: curl/7.79.1
      Accept-Encoding: gzip
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'HTTP: GET /kb/{kb:int}'
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2]
      Response:
      StatusCode: 200
      Content-Type: text/plain; charset=utf-8
      Content-Encoding: [Redacted]
      Vary: [Redacted]
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'HTTP: GET /kb/{kb:int}'
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[4]
      ResponseBody: JL�!0#5����U|

Примечание: я намерянно добавил мидлварь не в том порядке, что бы в логах были “крякозяблы” оно прям супер наглядно показывает когда ответ сжатый, плюс тут же в логах видно когда в ответе указывается заголовок content encoding говорящий о том что применено сжатие.

Ок, а зачем мне понимать эту теорию?

Дело в том что даже если мы включим сжатие в самом сервисе, или накрутим его в ingress или будем ходить по “наружке” через cloudflare (где это дело есть из коробки) самого сжатия не будет, т.к. по умолчанию клиент не передает заголовок accept encoding, а как следствие гоняются не сжатые байтики.

В самом простом случае, что бы все это дело заработало нужно что то типа:

builder.Services
    .AddHttpClient("Api", client =>
    {
        client.Timeout = TimeSpan.FromSeconds(2);
        client.DefaultRequestHeaders.AcceptEncoding.Add(new System.Net.Http.Headers.StringWithQualityHeaderValue("gzip"));
        client.DefaultRequestHeaders.AcceptEncoding.Add(new System.Net.Http.Headers.StringWithQualityHeaderValue("deflate"));
    })
    .ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler
    {
        AutomaticDecompression = DecompressionMethods.All
    });

Тут мы включаем поддержку декомпресии (уж не знаю почему, но из эксперимента, она вроде как не включена по умолчанию), а так же к каждому запросу подсовываем нужный заголовок.

Что нужно сделать?

Рассклад такой что nginx (в нашем случае это cloudflare и ingress в кубере) умеет автоматом сжимать

Так вот - в текущей имплементации ingress эта фича выключена, но что еще важнее - мы ж переключились на “внутрянку”, а как следствие запросы идут напрямую.

Выглядит так что нам нужно не только внести правки в конфигурацию клиентов но и добавить сжатие ответов на уровне сервисов.

Какие альтернативы?

Предпологаеться что мы будем прикручивать internal ingress

По мимо базовых метрик которые появятся из коробки для любого сервиса

Мы так же, могли бы включить сжатие ответов на нем

Тогда не нужно было бы настраивать сжатие в самих сервисах и оно бы применилось всюду

Но, в любом случае, что там, что там, нужно будет все перенастраивать все на свете, как минимум клиенты, а как следствие, вероятно, есть смысл сделать сразу в сервисах, т.к. это будет работать всегда и всюду, все зависимости от того как будет устроено в будущем.