Как стать автором
Обновить
0
Ребреин
REBRAIN: Онлайн-практикумы для Инженеров

Принимаем 10 000 ивентов в Яндекс.Облаке. Часть 2

Время на прочтение14 мин
Количество просмотров4.5K
И снова здравствуйте! Продолжаем нашу серию статей про то, как мы щупали Яндекс.Облако.

Давайте вспомним план, по которому мы с вами двигаемся:

1 часть. Мы определились с ТЗ и архитектурой решения, написали приложение на golang.
2 часть (вы сейчас здесь). Выливаем наше приложение на прод, делаем масштабируемым и тестируем нагрузку.
3 часть. Попробуем разобраться, зачем нам нужно хранить сообщения в буфере, а не в файлах, а также сравним между собой kafka, rabbitmq и yandex queue service.
4 часть. Будем разворачивать Clickhouse кластер, писать стриминг для перекладывания туда данных из буфера, настроим визуализацию в datalens.
5 часть. Приведем всю инфраструктуру в должный вид — настроим ci/cd, используя gitlab ci, подключим мониторинг и service discovery с помощью consul и prometheus.



Ну что, переходим к нашим задачкам.

Выливаемся в production


В 1 части мы собрали приложение, оттестировали его, а также загрузили образ в приватный container registry, готовый для развертывания.

Вообще следующие шаги должны быть практически очевидными — создаем виртуалки, настраиваем load balancer и прописываем DNS имя с проксированием на cloudflare. Но боюсь, такой вариант не сильно соответствует нашему техническому заданию. Мы хотим уметь масштабировать наш сервис в случае увеличения нагрузки и выкидывать из него битые ноды, которые не могут обслуживать запросы.

Для масштабирования мы будем использовать группы машин (instance groups), доступные в compute cloud. Они позволяют создавать виртуалки по шаблону, следить за их доступностью с помощью health check’ов, а также автоматически увеличивать количество узлов в случае возрастания нагрузки. Подробнее здесь.

Встает только один вопрос — какой шаблон использовать для виртуальной машины? Конечно, можно установить linux, настроить его, сделать образ и залить в хранилище образов в Яндекс.Облаке. Но для нас это долгий и непростой путь. В процессе рассмотрения различных образов, доступных при создании виртуалки, мы наткнулись на интересный экземпляр — container optimized image (https://cloud.yandex.ru/docs/cos/concepts/). Он позволяет запустить один docker контейнер в сетевом режиме host. То есть при создании виртуалки указывается примерно такая спецификация для образа container optimized image:

spec:
  containers:
    - name: api
      image: vozerov/events-api:v1
      command:
        - /app/app
      args:
        - -kafka=kafka.ru-central1.internal:9092
      securityContext:
        privileged: false
      tty: false
      stdin: false
      restartPolicy: Always

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

Схема вырисовывается вполне интересная:

  1. Мы создаем instance group с автоматическим масштабированием при превышении 60% cpu usage.
  2. В качестве шаблона указываем виртуальную машину с образом container optimized image и параметрами для запуска нашего Docker-контейнера.
  3. Создаем load balancer, который будет смотреть на нашу instance group и автоматически обновляться при добавлении или удалении виртуальных машин.
  4. Приложение будет мониториться и как instance group, и самим балансировщиком, который выкинет недоступные виртуалки из балансировки.

Звучит как план!

Попробуем создать instance group с помощью terraform. Все описание лежит в instance-group.tf, прокомментирую основные моменты:

  1. service account id будет использоваться для создания и удаления виртуалок. Кстати, нам его придется создать.

    service_account_id = yandex_iam_service_account.instances.id
  2. Данные для указания спецификации контейнера вынесены в отдельный файл spec.yml, где помимо прочего указывается образ, который необходимо запустить. Поскольку внутренний registry у нас появился во время написания данной статьи, а гит-репозиторий я выложил в паблик на гитхаб — на данный момент там прописан образ с публичного docker hub. Чтобы использовать образ из нашего репозитория, достаточно прописать путь до него —

    	    
            metadata = {
    	  docker-container-declaration = file("spec.yml")
    	  ssh-keys = "ubuntu:${file("~/.ssh/id_rsa.pub")}"
    	}
  3. Укажем еще один service account id, он будет использоваться образом container optimized image, чтобы пройти аутентификацию в container registry и скачать образ нашего приложения. Сервисный аккаунт с доступом к registry мы уже создавали, поэтому будем использовать его:

    service_account_id = yandex_iam_service_account.docker.id
  4. Scale policy. Самая интересная часть:

    autoscale {
      initialsize = 3
      measurementduration = 60
      cpuutilizationtarget = 60
      minzonesize = 1
      maxsize = 6
      warmupduration = 60
      stabilizationduration = 180
    }

    Здесь мы определяем политику масштабирования наших виртуалок. Есть два варианта — либо fixed_scale с фиксированным числом виртуальных машин, либо auth_scale.

    Основные параметры такие:

    initial size — первоначальный размер группы;
    measurement_duration — период изменения целевого показателя;
    cpu_utilization_target — целевое значение показателя, при большем значении которого надо расширить группу;
    min_zone_size — минимальное количество нод в одной зоне доступности — планировщик не будет удалять ноды, чтобы их количество стало ниже этого значения;
    max_size — максимальное количество нод в группе;
    warmup_duration — период прогрева, в течение которого планировщик не будет брать значения метрик с данной ноды, а будет использовать среднее значение в группе;
    stabilization_duration — период стабилизации — после увеличения количества нод в группе в течение указанного промежутка времени балансировщик не будет удалять созданные машины, даже если целевой показатель опустился ниже порогового значения.

    Теперь простыми словами о наших параметрах. На начальном этапе создаем 3 машинки (initial_size), минимум по одной в каждой зоне доступности (min_zone_size). Измеряем показатель cpu раз в минуту на всех машинках (measurement_duration). Если среднее значение превышает 60% (cpu_utilization_target), то создаем новые виртуалки, но не больше шести (max_size). После создания в течение 60 секунд не собираем метрики с новой машинки (warmup_duration), поскольку во время старта она может использовать достаточно много cpu. А 120 секунд после создания новой машинки не удаляем ничего (stabilization_duration), даже если среднее значение метрик в группе стало ниже 60% (cpu_utilization_target).

    Подробнее о масштабировании — https://cloud.yandex.ru/docs/compute/concepts/instance-groups/policies#auto-scale-policy
  5. Allocation policy. Указывает, в каких зонах доступности создавать наши виртуальные машины, — используем все три доступных зоны.

     allocationpolicy {
      zones = ["ru-central1-a", "ru-central1-b", "ru-central1-c"]
    }
    
  6. Политика деплоя машинок:

    
    deploy_policy {
      maxunavailable = 1
      maxcreating = 1
      maxexpansion = 1
      maxdeleting = 1
    }

    max_creating — максимальное число одновременно создаваемых машин;
    max_deleting — максимальное число одновременно удаляемых машин;
    max_expansion — на какое число виртуалок можно превысить размер группы при обновлении;
    max_unavailable — максимальное число виртуалок в статусе RUNNING, которые могут быть удалены;

    Подробнее о параметрах — https://cloud.yandex.ru/docs/compute/concepts/instance-groups/policies#deploy-policy
  7. Настройки балансировщика:

     load_balancer {
      target_group_name = "events-api-tg"
    }

    При создании instance group можно создать и целевую группу для балансировщика нагрузки. Она будет направлена на относящиеся к ней виртуальные машины. В случае удаления ноды будут выводиться из балансировки, а при создании — добавляться в балансировку после прохождения проверок состояния.

Вроде все основное прописали — давайте создавать сервисный аккаунт для instance group и, собственно, саму группу.

vozerov@mba:~/events/terraform (master *) $ terraform apply -target yandex_iam_service_account.instances -target yandex_resourcemanager_folder_iam_binding.editor

... skipped ...

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

vozerov@mba:~/events/terraform (master *) $ terraform apply -target yandex_compute_instance_group.events_api_ig

... skipped ...

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Группа создалась — можно посмотреть и проверить:

vozerov@mba:~/events/terraform (master *) $ yc compute instance-group list
+----------------------+---------------+------+
|          ID          |     NAME      | SIZE |
+----------------------+---------------+------+
| cl1s2tu8siei464pv1pn | events-api-ig |    3 |
+----------------------+---------------+------+

vozerov@mba:~/events/terraform (master *) $ yc compute instance list
+----------------------+---------------------------+---------------+---------+----------------+-------------+
|          ID          |           NAME            |    ZONE ID    | STATUS  |  EXTERNAL IP   | INTERNAL IP |
+----------------------+---------------------------+---------------+---------+----------------+-------------+
| ef3huodj8g4gc6afl0jg | cl1s2tu8siei464pv1pn-ocih | ru-central1-c | RUNNING | 130.193.44.106 | 172.16.3.3  |
| epdli4s24on2ceel46sr | cl1s2tu8siei464pv1pn-ipym | ru-central1-b | RUNNING | 84.201.164.196 | 172.16.2.31 |
| fhmf37k03oobgu9jmd7p | kafka                     | ru-central1-a | RUNNING | 84.201.173.41  | 172.16.1.31 |
| fhmh4la5dj0m82ihoskd | cl1s2tu8siei464pv1pn-ahuj | ru-central1-a | RUNNING | 130.193.37.94  | 172.16.1.37 |
| fhmr401mknb8omfnlrc0 | monitoring                | ru-central1-a | RUNNING | 84.201.159.71  | 172.16.1.14 |
| fhmt9pl1i8sf7ga6flgp | build                     | ru-central1-a | RUNNING | 84.201.132.3   | 172.16.1.26 |
+----------------------+---------------------------+---------------+---------+----------------+-------------+

vozerov@mba:~/events/terraform (master *) $

Три ноды с кривыми именами — это наша группа. Проверяем, что приложения на них доступны:

vozerov@mba:~/events/terraform (master *) $ curl -D - -s http://130.193.44.106:8080/status
HTTP/1.1 200 OK
Date: Mon, 13 Apr 2020 16:32:04 GMT
Content-Length: 3
Content-Type: text/plain; charset=utf-8

ok
vozerov@mba:~/events/terraform (master *) $ curl -D - -s http://84.201.164.196:8080/status
HTTP/1.1 200 OK
Date: Mon, 13 Apr 2020 16:32:09 GMT
Content-Length: 3
Content-Type: text/plain; charset=utf-8

ok
vozerov@mba:~/events/terraform (master *) $ curl -D - -s http://130.193.37.94:8080/status
HTTP/1.1 200 OK
Date: Mon, 13 Apr 2020 16:32:15 GMT
Content-Length: 3
Content-Type: text/plain; charset=utf-8

ok
vozerov@mba:~/events/terraform (master *) $

К слову, можно зайти на виртуалки с логином ubuntu и посмотреть логи контейнера и как он запущен.

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

vozerov@mba:~/events/terraform (master *) $ yc load-balancer target-group list
+----------------------+---------------+---------------------+-------------+--------------+
|          ID          |     NAME      |       CREATED       |  REGION ID  | TARGET COUNT |
+----------------------+---------------+---------------------+-------------+--------------+
| b7rhh6d4assoqrvqfr9g | events-api-tg | 2020-04-13 16:23:53 | ru-central1 |            3 |
+----------------------+---------------+---------------------+-------------+--------------+

vozerov@mba:~/events/terraform (master *) $

Давайте уже создадим балансировщик и попробуем пустить на него трафик! Этот процесс описан в load-balancer.tf, ключевые моменты:

  1. Указываем, какой внешний порт будет слушать балансировщик и на какой порт отправлять запрос к виртуальным машинам. Указываем тип внешнего адреса — ip v4. На данный момент балансировщик работает на транспортном уровне, поэтому балансировать он может только tcp / udp соединения. Так что прикручивать ssl придется либо на своих виртуалках, либо на внешнем сервисе, который умеет https, — например, cloudflare.

     listener {
    	  name = "events-api-listener"
    	  port = 80
    	  target_port = 8080
    	  external_address_spec {
    	    ipversion = "ipv4"
    	  }
    	}
  2. healthcheck {
    	  name = "http"
    	  http_options {
    	    port = 8080
    	    path = "/status"
    	  }
            }

    Healthchecks. Здесь мы указываем параметры проверки наших узлов — проверяем по http url /status на порту 8080. Если проверка будет провалена, то машинка будет выкинута из балансировки.

    Подробнее про балансировщик нагрузки — cloud.yandex.ru/docs/load-balancer/concepts. Из интересного — вы можете подключить услугу защиты от DDOS на балансировщике. Тогда на ваши сервера будет приходить уже очищенный трафик.

Создаем:

vozerov@mba:~/events/terraform (master *) $ terraform apply -target yandex_lb_network_load_balancer.events_api_lb

... skipped ...

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Вытаскиваем ip созданного балансировщика и тестируем работу:
vozerov@mba:~/events/terraform (master *) $ yc load-balancer network-load-balancer get events-api-lb
id:
folder_id:
created_at: "2020-04-13T16:34:28Z"
name: events-api-lb
region_id: ru-central1
status: ACTIVE
type: EXTERNAL
listeners:
- name: events-api-listener
  address: 130.193.37.103
  port: "80"
  protocol: TCP
  target_port: "8080"
attached_target_groups:
- target_group_id:
  health_checks:
  - name: http
    interval: 2s
    timeout: 1s
    unhealthy_threshold: "2"
    healthy_threshold: "2"
    http_options:
      port: "8080"
      path: /status

Теперь можем покидать в него сообщения:

vozerov@mba:~/events/terraform (master *) $ curl -D - -s -X POST -d '{"key1":"data1"}' http://130.193.37.103/post
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 13 Apr 2020 16:42:57 GMT
Content-Length: 41

{"status":"ok","partition":0,"Offset":1}
vozerov@mba:~/events/terraform (master *) $ curl -D - -s -X POST -d '{"key1":"data1"}' http://130.193.37.103/post
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 13 Apr 2020 16:42:58 GMT
Content-Length: 41

{"status":"ok","partition":0,"Offset":2}
vozerov@mba:~/events/terraform (master *) $ curl -D - -s -X POST -d '{"key1":"data1"}' http://130.193.37.103/post
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 13 Apr 2020 16:43:00 GMT
Content-Length: 41

{"status":"ok","partition":0,"Offset":3}
vozerov@mba:~/events/terraform (master *) $

Отлично, все работает. Остался последний штрих, чтобы мы были доступны по https — подключим cloudflare с проксированием. Если вы решили обойтись без cloudflare, можете пропустить данный шаг.

vozerov@mba:~/events/terraform (master *) $ terraform apply -target cloudflare_record.events

... skipped ...

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Тестируем через HTTPS:

vozerov@mba:~/events/terraform (master *) $ curl -D - -s -X POST -d '{"key1":"data1"}' https://events.kis.im/post
HTTP/2 200
date: Mon, 13 Apr 2020 16:45:01 GMT
content-type: application/json
content-length: 41
set-cookie: __cfduid=d7583eb5f791cd3c1bdd7ce2940c8a7981586796301; expires=Wed, 13-May-20 16:45:01 GMT; path=/; domain=.kis.im; HttpOnly; SameSite=Lax
cf-cache-status: DYNAMIC
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
server: cloudflare
cf-ray: 5836a7b1bb037b2b-DME

{"status":"ok","partition":0,"Offset":5}
vozerov@mba:~/events/terraform (master *) $

Все наконец-то работает.

Тестируем нагрузку


Нам остался, пожалуй, самый интересный шаг — провести нагрузочное тестирование нашего сервиса и получить некоторые цифры — например, 95 персентиль времени обработки одного запроса. Также было бы неплохо протестировать автомасштабирование нашей группы узлов.

Перед запуском тестирования стоит сделать одну простую штуку — добавить наши application ноды в prometheus, чтобы следить за количеством запросов и временем обработки одного запроса. Поскольку мы пока не добавляли никакого service discovery (мы сделаем это в 5 статье серии), просто пропишем static_configs на нашем monitoring сервере. Узнать его ip можно стандартным способом через yc compute instance list, после чего дописать в /etc/prometheus/prometheus.yml следующие настройки:

  - job_name: api
    metrics_path: /metrics
    static_configs:
    - targets:
      - 172.16.3.3:8080
      - 172.16.2.31:8080
      - 172.16.1.37:8080

IP-адреса наших машинок также можно взять из yc compute instance list. Рестартуем prometheus через systemctl restart prometheus и проверяем, что ноды успешно опрашиваются, зайдя в веб-интерфейс, доступный на порту 9090 (84.201.159.71:9090).

Давайте еще добавим дашборд в графану из папки grafana. Заходим в Grafana по порту 3000 (84.201.159.71:3000) и с логином / паролем — admin / Password. Далее добавляем локальный prometheus и импортируем dashboard. Собственно, на этом моменте подготовка завершена — можно закидывать нашу инсталляцию запросами.

Для тестирования будем использовать yandex tank (https://yandex.ru/dev/tank/) с плагином для overload.yandex.net, который позволит нам визуализировать данные, полученные танком. Все необходимое для работы находится в папке load изначального гит-репозитория.

Немного о том, что там есть:

  1. token.txt — файл с API ключиком от overload.yandex.net — его можно получить, зарегистрировавшись на сервисе.
  2. load.yml — конфигурационный файл для танка, там прописан домен для тестирования — events.kis.im, тип нагрузки rps и количество запросов 15 000 в секунду в течение 3-х минут.
  3. data — специальный файл для генерации конфига в формате ammo.txt. В нем прописываем тип запроса, url, группу для отображения статистики и собственно данные, которые необходимо отправлять.
  4. makeammo.py — скрипт для генерации ammo.txt файла из data-файла. Подробнее про скрипт — yandextank.readthedocs.io/en/latest/ammo_generators.html
  5. ammo.txt — итоговый ammo файл, который будет использоваться для отправки запросов.

Для тестирования я взял виртуалку за пределами Яндекс.Облака (чтобы все было честно) и создал ей DNS запись load.kis.im. Накатил туда docker, поскольку танк мы будем запускать, используя образ https://hub.docker.com/r/direvius/yandex-tank/.

Ну что же, приступаем. Копируем нашу папку на сервер, добавляем токен и запускаем танк:

vozerov@mba:~/events (master *) $ rsync -av load/ cloud-user@load.kis.im:load/

... skipped ...

sent 2195 bytes  received 136 bytes  1554.00 bytes/sec
total size is 1810  speedup is 0.78

vozerov@mba:~/events (master *) $ ssh load.kis.im -l cloud-user
cloud-user@load:~$ cd load/
cloud-user@load:~/load$ echo "TOKEN" > token.txt
cloud-user@load:~/load$ sudo docker run -v $(pwd):/var/loadtest --net host --rm -it direvius/yandex-tank -c load.yaml ammo.txt
No handlers could be found for logger "netort.resource"
17:25:25 [INFO] New test id 2020-04-13_17-25-25.355490
17:25:25 [INFO] Logging handler <logging.StreamHandler object at 0x7f209a266850> added
17:25:25 [INFO] Logging handler <logging.StreamHandler object at 0x7f209a20aa50> added
17:25:25 [INFO] Created a folder for the test. /var/loadtest/logs/2020-04-13_17-25-25.355490
17:25:25 [INFO] Configuring plugins...
17:25:25 [INFO] Loading plugins...
17:25:25 [INFO] Testing connection to resolved address 104.27.164.45 and port 80
17:25:25 [INFO] Resolved events.kis.im into 104.27.164.45:80
17:25:25 [INFO] Configuring StepperWrapper...
17:25:25 [INFO] Making stpd-file: /var/loadtest/ammo.stpd
17:25:25 [INFO] Default ammo type ('phantom') used, use 'phantom.ammo_type' option to override it

... skipped ...

Все, процесс запущен. В консоли это выглядит примерно так:



А мы ждем завершения процесса и наблюдаем за временем ответа, количеством запросов и, конечно, за автоматическим масштабированием нашей группы виртуальных машин. Мониторить группу виртуалок можно через веб-интерфейс, в настройках группы виртуальных машин есть вкладка «Мониторинг».



Как можно заметить — наши ноды не догрузились даже до 50% CPU, поэтому тест с автомасштабированием придется повторить. А пока глянем на время обработки запроса в Grafana:



Количество запросов — порядка 3000 на одну ноду — немного до 10 000 не догрузили. Время ответа радует — порядка 11 ms на запрос. Единственный, кто выделяется, — 172.16.1.37 — у него время обработки запроса в два раза меньше. Но это и логично — он находится в той же зоне доступности ru-central1-a, что и kafka, которая сохраняет сообщения.

Кстати, отчет по первому запуску доступен по ссылке: https://overload.yandex.net/265967.

Итак, давайте запустим тест повеселее — добавим параметр instances: 2000, чтобы добиться 15 000 запросов в секунду, и увеличим время теста до 10 минут. Итоговый файл будет выглядеть так:

overload:
  enabled: true
  package: yandextank.plugins.DataUploader
  token_file: "token.txt"
phantom:
  address: 130.193.37.103
  load_profile:
    load_type: rps
    schedule: const(15000, 10m)
  instances: 2000
console:
  enabled: true
telegraf:
  enabled: false

Внимательный читатель обратит внимание, что я поменял адрес на IP балансировщика — связано это с тем, что cloudflare начал меня блочить за огромное количество запросов с одного ip. Пришлось натравить tank напрямую на балансер Яндекс.Облака. После запуска можно наблюдать такую картину:



Использование CPU выросло, и планировщик принял решение увеличить количество нод в зоне B, что и сделал. Это можно увидеть по логам instance group:

vozerov@mba:~/events/load (master *) $ yc compute instance-group list-logs events-api-ig
2020-04-13 18:26:47    cl1s2tu8siei464pv1pn-ejok.ru-central1.internal  1m AWAITING_WARMUP_DURATION -> RUNNING_ACTUAL
2020-04-13 18:25:47    cl1s2tu8siei464pv1pn-ejok.ru-central1.internal 37s OPENING_TRAFFIC -> AWAITING_WARMUP_DURATION
2020-04-13 18:25:09    cl1s2tu8siei464pv1pn-ejok.ru-central1.internal 43s CREATING_INSTANCE -> OPENING_TRAFFIC
2020-04-13 18:24:26    cl1s2tu8siei464pv1pn-ejok.ru-central1.internal  6s DELETED  -> CREATING_INSTANCE
2020-04-13 18:24:19    cl1s2tu8siei464pv1pn-ozix.ru-central1.internal  0s PREPARING_RESOURCES -> DELETED
2020-04-13 18:24:19    cl1s2tu8siei464pv1pn-ejok.ru-central1.internal  0s PREPARING_RESOURCES -> DELETED
2020-04-13 18:24:15    Target allocation changed in accordance with auto scale policy in zone ru-central1-a: 1 -> 2
2020-04-13 18:24:15    Target allocation changed in accordance with auto scale policy in zone ru-central1-b: 1 -> 2

... skipped ...

2020-04-13 16:23:57    Balancer target group b7rhh6d4assoqrvqfr9g created
2020-04-13 16:23:43    Going to create balancer target group

Также планировщик решил увеличить количество серверов и в других зонах, но у меня закончился лимит на внешние ip-адреса :) Их, кстати, можно увеличить через запрос в техподдержку, указав квоты и желаемые значения.

Заключение


Нелегкой выдалась статья — и по объему, и по количеству информации. Но самый трудный этап мы прошли и сделали следующее:

  1. Подняли мониторинг и кафку.
  2. Создали группу виртуалок с автомасштабированием, на которых запускается наше приложение.
  3. Подвязали к load balancer’у cloudflare для использования ssl сертификатов.

В следующий раз займемся сравнением и тестированием rabbitmq / kafka / yandex queue service.
Stay tuned!

*Этот материал есть в видеозаписи открытого практикума REBRAIN & Yandex.Cloud: Принимаем 10 000 запросов в секунду на Яндекс Облако — https://youtu.be/cZLezUm0ekE

Если вам интересно посещать такие мероприятия онлайн и задавать вопросы в режиме реального времени, подключайтесь к каналу DevOps by REBRAIN.

Отдельное спасибо хотим сказать Yandex.Cloud за возможность проведения такого мероприятия. Ссылочка на них

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

P.S. У нас есть 2 бесплатных аудита в месяц, возможно, именно ваш проект будет в их числе.
Теги:
Хабы:
+7
Комментарии5

Публикации

Информация

Сайт
rebrainme.com
Дата регистрации
Дата основания
Численность
11–30 человек
Местоположение
Россия

Истории