В последнее время микросервисные архитектуры пользуются определённой популярностью. От того, как взаимодействуют микросервисы, может зависеть производительность и масштабируемость решений, основанных на них. Это взаимодействие может быть синхронным или асинхронным. В материале, перевод которого мы представляем вашему вниманию сегодня, рассматриваются синхронные методы взаимодействия микросервисов. А именно, речь пойдёт об исследовании двух технологий: HTTP/1.1 и gRPC. Первая технология представлена стандартными HTTP-вызовами. Вторая основана на использовании высокопроизводительного RPC-фреймворка от Google. Автор материала предлагает взглянуть на код, необходимый для реализации взаимодействия микросервисов с использованием HTTP/1.1 и gRPC, провести замеры производительности, и выбрать технологию, которая позволяет организовать обмен данными между микросервисами наилучшим образом.



Приложение, работающее в обычном режиме


Начнём с малого и создадим систему из двух микросервисов, которые могут взаимодействовать друг с другом. Обратите внимание на то, что здесь не используется кластерный режим. Вот схема нашего приложения.


Архитектура приложения, работающего в обычном режиме

Приложение состоит из следующих компонентов:

  • Средство для тестирования системы (Load testing tool): jMeter.
  • Сервис А (Service A): микросервис, который выполняет запросы к сервису B и возвращает полученные от него ответы.
  • Сервис B (Service B): микросервис, отправляющий в ответ на запросы статические JSON-данные после 10-миллисекундной задержки, используемой для всех его API.
  • Виртуальные машины (VM 1 и VM 2): экземпляры Amazon EC2 t2.xlarge.

?HTTP/1.1


HTTP/1.1 — это стандартная технология организации взаимодействия микросервисов, которая применяется при использовании любых HTTP-библиотек вроде axios или superagent.

Вот код сервиса B, реализующий API нашей системы:

server.route({
  method: 'GET',
  path: '/',
  handler: async (request, h) => {
    const response = await new Promise((resolve) => {
      setTimeout(() => {
        resolve({
          id: 1,
          name: 'Abhinav Dhasmana',
          enjoys_coding: true,

        });
      }, 10);
    });
    return h.response(response);
  },
});

Вот код сервиса А, который обращается к сервису B, используя HTTP/1.1:

server.route({
  method: 'GET',
  path: '/',
  handler: async (request, h) => {
    try {
      const response = await Axios({
        url: 'http://localhost:8001/',
        method: 'GET',
      });
      return h.response(response.data);
    } catch (err) {
      throw Boom.clientTimeout(err);
    }
  },
});

Запустив эти микросервисы, мы можем воспользоваться возможностями jMeter для выполнения тестов производительности. Выясним, как система ведёт себя при работе с ней 50 пользователей, каждый из которых выполняет по 2000 запросов. Как можно видеть на следующем рисунке, медиана результатов измерения равна 37 мс.


Результаты исследования системы, работающей в обычном режиме и использующей HTTP/1.1, с помощью jMeter

?gRPC


gRPC использует по умолчанию технологию Protocol Buffers. Поэтому, применяя gRPC, в дополнение к коду двух сервисов, нам понадобится написать и код proto-файла, который описывает интерфейс взаимодействия модулей системы.

syntax = "proto3";

service SampleDataService {
  rpc GetSampleData (Empty) returns (SampleData) {}
}

message SampleData {
  int32 id = 1;
  string name = 2;
  bool enjoys_coding = 3;
}

message Empty {}

Теперь, так как теперь мы планируем использовать gRPC, надо переписать код сервиса B:

const grpc = require('grpc');

const proto = grpc.load('serviceB.proto');
const server = new grpc.Server();

const GetSampleData = (call, callback) => {
  setTimeout(() => {
    callback(null, {
      id: 1,
      name: 'Abhinav Dhasmana',
      enjoys_coding: true,
    });
  }, 10);
};

server.addService(proto.SampleDataService.service, {
  GetSampleData,
});

const port = process.env.PORT;
console.log('port', port);

server.bind(`0.0.0.0:${port}`, grpc.ServerCredentials.createInsecure());

server.start();
console.log('grpc server is running');

Обратите внимание на некоторые особенности этого кода:

  • Командой const server = new grpc.Server(); мы создаём grpc-сервер.
  • Командой server.addService(proto... мы добавляем сервис к серверу.
  • Команда server.bind(`0.0.0.0:${port}... служит для привязки порта и учётных данных.

Теперь перепишем сервис A с использованием gRPC:

const protoPath = `${__dirname}/../serviceB/serviceB.proto`;
const proto = grpc.load(protoPath);

const client = new proto.SampleDataService('localhost:8001', grpc.credentials.createInsecure());
const getDataViagRPC = () => new Promise((resolve, reject) => {
  client.GetSampleData({}, (err, response) => {
    if (!response.err) {
      resolve(response);
    } else {
      reject(err);
    }
  });
});

server.route({
  method: 'GET',
  path: '/',
  handler: async (request, h) => {
    const allResults = await getDataViagRPC();
    return h.response(allResults);
  },
});

Среди особенностей этого кода можно отметить следующие:

  • Командой const client = new proto.SampleDataService... мы создаём grpc-клиент.
  • Удалённый вызов выполняется с помощью команды client.GetSampleData({}....

Теперь протестируем то, что у нас получилось, с помощью jMeter.


Результаты исследования системы, работающей в обычном режиме и использующей gRPC, с помощью jMeter

Проведя несложные расчёты, можно выяснить, что решение, использующее gRPC, оказывается на 27% быстрее решения, использующего HTTP/1.1.

Приложение, работающее в кластерном режиме


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


Архитектура приложения, работающего в кластерном режиме

Если сравнить эту архитектуру с рассмотренной ранее, можно отметить следующие изменения:

  • Здесь имеется балансировщик нагрузки (Load Balancer), в роли которого используется NGINX.
  • Сервис B теперь присутствует здесь в трёх экземплярах, которые прослушивают разные порты.

Подобная архитектура характерна для реальных проектов.

Исследуем HTTP/1.1 и gRPC в новой среде.

?HTTP/1.1


При использовании в кластерной среде микросервисов, применяющих HTTP/1.1, их код менять не придётся. Нужно лишь настроить nginx для организации балансировки трафика сервиса B. В нашем случае, для того, чтобы это сделать, нужно привести файл /etc/nginx/sites-available/default к такому виду:

upstream httpservers {
   server ip_address:8001; 
   server ip_address:8002; 
   server ip_address:8003; 
}
server {
   listen 80; 

   location / {
      proxy_pass http://httpservers;
   }
}

Запустим теперь то, что у нас получилось, и посмотрим на результаты тестирования системы с использованием jMeter.


Результаты исследования системы, работающей в кластерном режиме и использующей HTTP/1.1, с помощью jMeter

Медиана в данном случае равна 41 мс.

?gRPC


Поддержка gRPC появилась в nginx 1.13.10. Поэтому нам понадобится самая свежая версия nginx, для установки которой обычная команда sudo apt-get install nginx не подходит.

Также тут мы не используем Node.js в кластерном режиме, так как в таком режиме gRPC не поддерживается.

Для того чтобы установить самую свежую версию nginx, воспользуйтесь следующей последовательностью команд:

sudo apt-get install -y software-properties-common
sudo add-apt-repository ppa:nginx/stable
sudo apt-get update
sudo apt-get install nginx

Кроме того, нам понадобятся SSL-сертификаты. Самоподписанный сертификат можно создать с помощью openSSL:

openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost'   -keyout localhost-privatekey.pem -out localhost-certificate.pem

Для использования gRPC нужно отредактировать файл /etc/nginx/sites-available/default:

upstream httpservers {
   server ip_address:8001; 
   server ip_address:8002; 
   server ip_address:8003; 
}
server {
   listen 80; 

   location / {
      proxy_pass http://httpservers;
   }
}

Теперь всё готово для того, чтобы испытать кластерное gRPC-решение с помощью jMeter.


Результаты исследования системы, работающей в кластерном режиме и использующей gRPC, с помощью jMeter

В данном случае медиана равна 28 мс, а это, в сравнении с аналогичным показателем, полученным при исследовании кластерного HTTP/1.1-решения, на 31% быстрее.

Итоги


Результаты исследования показывают, что приложение, основанное на микросервисах, и использующее gRPC, оказывается примерно на 30% производительнее аналогичного приложения, в котором для обмена данными между микросервисами используется HTTP/1.1. Исходный код проектов, рассмотренных в этом материале, можно найти здесь.

Уважаемые читатели! Если вы занимаетесь разработкой микросервисов, просим рассказать о том, как вы организуете обмен данными между ними.

Комментарии (3)


  1. mmMike
    30.08.2018 14:20

    Результаты исследования показывают, что приложение, основанное на микросервисах, и использующее gRPC, оказывается примерно на 30% производительнее аналогичного приложения, в котором для обмена данными между микросервисами используется HTTP/1.1.

    Когда вижу всякие сравнения по производительности микросервисов сразу этот личный опыт вспоминается… Результаты производительности железки (HSM через TCP/IP):


    • Обращение по локальному соединению — 10ms на операцию.
    • По внутренней сетке днем — 50..60ms
    • По внутренней сетке в обед — 250..550ms на операцию (обед… в Интернете все лазят)
    • По внутренней сетке вечером (рабочий день закончен) — 30-40ms


  1. RidgeA
    30.08.2018 23:20

    По-моему, с конфигом nginx для балансировки grpc случилась копипаста...


    не нашел в мобильной версии сайта как написать в личку....


  1. Jorixxx
    31.08.2018 11:18

    хочется ну хоть сколько-нибудь реалистичную конфигурацию. с ретраями запросов(которые можно поретраить), с логированием запросов. ну т.е. сам обмен данными это даже не полбеды в микросервисах. гораздо важнее поддержка отказоустойчивости и человекопонятность происходящего во время аварий. именно эти вещи держат многих на http/1.1