Как стать автором
Обновить
556.28
Яндекс
Как мы делаем Яндекс

Как запустить 100+ компиляторов и выстоять. Опыт Яндекс.Контеста

Время на прочтение 14 мин
Количество просмотров 7.3K
Привет, это снова Павел Тыквин, разработчик Яндекс.Контеста. Контест больше всего известен как площадка для соревнований по программированию: прямо сейчас идёт квалификационный этап чемпионата Yandex Cup. Я уже писал на Хабре о том, как мы решаем одну из стоящих перед нами проблем: выравниваем время исполнения кода. Ну а в этой статье я приоткрою детали процесса проверки, расскажу, через какие этапы проходит код участников и какими методами мы оптимизируем этот процесс, а также — как мы добавили возможность решать задачи на том языке, с которым участник уже знаком (вне зависимости от способов тестирования внутри платформы).

Как происходит проверка решения


Возьмём для примера простейшую задачу: вам заданы два целых числа a и b, выведите a+b.

Допустим, решение будет таким:

source.cpp:
#include <bits/stdc++.h>

using namespace std;

int main() {
    int a, b;
    cin >> a >> b;
    cout << a + b << endl;
}

Как можно проверить это решение вручную без участия дополнительных сервисов и библиотек? Например, так:

Компиляция

g++ -O3 -fno-stack-limit -std=c++20 -x c++ source.cpp -o solution

Тестирование

test `echo 1 2 | ./solution` -eq 3 && echo “OK” || echo “WA”</li>
test `echo 2 6 | ./solution` -eq 8 && echo “OK” || echo “WA”</li>
test `echo -20 10 | ./solution` -eq -10 && echo “OK” || echo “WA”

Даже в ручном режиме необходимо скомпилировать решение:

  1. нужен g++ и stdlib с++20;
  2. должен быть прописан PATH для вызова g++;
  3. нужна сама строка компиляции, должны быть указаны нужные флаги;
  4. ну и естественно нам нужен файл с решением.

И протестировать его:

  1. нужен скомпилированный код;
  2. окружение — для запуска всё ещё нужна stdlib нужной версии;
  3. тесты — нужно знать, что будем подавать на вход решению и с чем будем сравнивать вывод — %%echo 1 2%%;
  4. чекер — нужно знать способ, которым мы будем сравнивать output решения с ожидаемым ответом. В нашем примере это -eq.


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

Напишите программу, которая угадывает задуманное целое число от 1 до 1000. Тестирующая система загадывает число и не сообщает программе. Угадать число нужно не более чем за 10 попыток. На каждую попытку пользователь отвечает, что загаданное число больше названного (вводит символ «>»), меньше названного («<») или угадано правильно («=»).

Решение:

# минимальное значение, которое может иметь загаданное число
lower_bound = 1 
# максимальное значение, которое может иметь загаданное число
upper_bound = 1000 
middle = (upper_bound + lower_bound) // 2
print(middle)
answer = input()
while answer != "=":
    if answer == '<':
        upper_bound = middle - 1
    elif answer == '>':
        lower_bound = middle + 1
    middle = (upper_bound + lower_bound) // 2
    print(middle)
    answer = input()

В этом примере есть некоторое взаимодействие между решением участника и тестирующей программой (интерактором). Такие задачи называются интерактивными — во время тестирования решения нужно будет запускать и решение участника, и интерактор.

Визуализация полного процесса проверки:


Тонкости подготовки окружения


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

Что нужно учитывать? Где-то есть участник, который только что отправил свою посылку на проверку. И нам нужно уменьшить оверхед на подготовку окружения, в идеале до нуля. При этом окружения под некоторые задачи могут занимать более 10 Гб. А значит всё, что нужно для выполнения посылки, уже должно быть на виртуалке. В противном случае к времени ожидания участника добавится ещё несколько минут, а в плохих случаях — и десятков минут. Когда речь идёт о соревновании с ограниченным временем (а это большинство наших соревнований), такие задержки становятся критичными. Нам важно обеспечить участникам равные и разумные условия.

Когда-то мы решали эту задачу достаточно наивно: давайте просто поставим на виртуалку все нужные компиляторы с нужными библиотеками нужных версий и будем это поддерживать. Долго сервис шёл именно по такому пути — в то время у нас было около 40 решающих машин, а участникам было доступно всего лишь несколько самых популярных языков программирования. У этого способа есть неоспоримый плюс — время на подготовку окружения равно нулю, ведь все нужные пакеты уже и так стоят на решающей машине.

Однако у такого подхода есть неизбежная проблема роста. На платформе растёт количество соревнований, например, за 2020 год создано и проведено около 5000. Это почти вдвое больше, чем в 2019-м. Кроме того, мы хотим дать возможность новым участникам решать задачи на тех языках, к которым они привыкли. Нам важно, чтобы в соревнованиях могли участвовать все разработчики.

Немного цифр: сейчас у нас почти 600 виртуалок, которые занимаются проверкой решений, мы поддерживаем 22 «языка» в 124 версиях и наборах библиотек.

Вот какие языки поддерживаются
Free Basic
C
C#
C++
D
Delphi
Go
Haskell
Java
Kotlin
JS
Kumir
Swift
Pascal
Perl
PHP
Python
R
Ruby
Rust
Scala
GNU bash
SQLite

Добавление нового компилятора или обновление существующего было одной из наших регулярных задач. Проще говоря, у нас был зоопарк окружений, которые постоянно добавлялись и обновлялись, и нам нужно было поддерживать его на 600 машинах так, чтобы они не конфликтовали друг с другом. Конечно же, в определённый момент мы отказались от этого подхода и пошли в сторону контейнеризации — в сторону Docker.

Доставка окружений с помощью Docker


Идея проста: давайте собирать наши окружения в docker-образы и выкачивать на решающую машинку нужные во время проверки. По одному образу на каждый поддерживаемый язык программирования. Так мы получим удобный инструмент для добавления новых компиляторов и обновления старых из коробки, интерфейс для загрузки окружений и переиспользование уже скачанных слоёв в нескольких образах.

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

Значит, нам нужно уметь комбинировать несколько докерных образов в один и получать окружение для запуска любой комбинации компиляторов. Например: участник написал решения на Java, а чекер написан на Python 3.9. Соответственно, в окружении должна быть и JDK, и Python 3.9 с нужными библиотеками. И это просто пример. Комбинации могут быть любыми. Получается, нам нужно как-то объединить содержимое нескольких докерных образов.

Чуть глубже погружаемся в докер


Как известно, докер-образ состоит из множества слоёв (каждый из которых представляет собой дельту изменений файловой системы) и манифеста, который описывает порядок этих слоёв. Для работы с докером есть хороший API. С его помощью можно получить и манифест, и слои образа.

Посмотрим на примере одного из наших компиляторов
{
   "schemaVersion": 1,
   "name": "contest/compilers/idao2021",
   "tag": "base-track-2_v2",
   "architecture": "amd64",
   "fsLayers": [
      {
         "blobSum": "sha256:51ae359e96f0e6d324e9078a3ece6ef363b8f89cc2f66a8456c8c04a940d6e53"
      },
      {
         "blobSum": "sha256:207cc68f4ffd56411712e878da214391f799fcb76af871f48b2252eb434d54b6"
      },
      {
         "blobSum": "sha256:c0eef81609058076ca5e812463fd86d9942edea3e887260d39909f99c7fe558e"
      },
      {
         "blobSum": "sha256:036fd4ca4f5c5deee5021cd655f9d711ba2b70c24e270aa3c01cae937267ac1a"
      },
      {
         "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
      },
      {
         "blobSum": "sha256:3563871736b2edc6a77b60b10a7f1a7d762fedcd13886373aa290a491114cd8d"
      },
      {
         "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
      },
      {
         "blobSum": "sha256:3217f7c42f9440a65c159326bb06c6fe83f9cfa745a25668a6a3dc06732d63c0"
      },
      {
         "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
      },
      {
         "blobSum": "sha256:27e29f63878d809559ceba531389e227c2b5cbe9b86bc9ac5b6dbafc26ef3a7d"
      },
      {
         "blobSum": "sha256:e0a5040427feb008958cf06bc0fd95593e1ae63058219d77d8fbbf50cb3af7b8"
      },
      {
         "blobSum": "sha256:91b66f28e0f5355971d67074d108f09194e5ab8653ff93f623d353b8f851559f"
      }
   ],
   "history": [
      {
         "v1Compatibility": "{\"architecture\":\"amd64\",\"config\":{\"Hostname\":\"\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/conda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"CONDA_AUTO_UPDATE_CONDA=false\"],\"Cmd\":null,\"Image\":\"sha256:d6aca2309536874024406e17bd860154771f9edab2673a93ffd754fb2e043e64\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"container_config\":{\"Hostname\":\"\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/conda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"CONDA_AUTO_UPDATE_CONDA=false\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) COPY dir:90a04825e2a5956b663d0da222d640d2c85bb4887a9bfd055b632207985bf0f4 in /opt/. \"],\"Image\":\"sha256:d6aca2309536874024406e17bd860154771f9edab2673a93ffd754fb2e043e64\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"created\":\"2021-02-26T20:14:09.653968151Z\",\"docker_version\":\"19.03.13\",\"id\":\"883cbf03c5215f16fe4e9af245933954d2dbb3e0795d06ec975c86faed5a73f0\",\"os\":\"linux\",\"parent\":\"3b0b60f28177489b7b3559dddaa4be6bef1399644f6e57004076c5fe25c7ff94\"}"
      },
      {
         "v1Compatibility": "{\"id\":\"3b0b60f28177489b7b3559dddaa4be6bef1399644f6e57004076c5fe25c7ff94\",\"parent\":\"98745b97e709dc2f0a6dbccb78c6fda0e26af101e167cb4681daac174e7d82a0\",\"created\":\"2021-02-25T15:30:09.48470141Z\",\"container_config\":{\"Cmd\":[\"|1 DEBIAN_FRONTEND=noninteractive /bin/sh -c pip install -r requirements.txt     \\u0026\\u0026 rm -rf /root/.cache\"]}}"
      },
      {
         "v1Compatibility": "{\"id\":\"98745b97e709dc2f0a6dbccb78c6fda0e26af101e167cb4681daac174e7d82a0\",\"parent\":\"54dca58a3b2e48ad6a5b51db76f82e9c4ac14baf7501a715d27a40228c1cf42f\",\"created\":\"2021-02-25T14:36:59.180850517Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) COPY file:cf69df63b2664ddb31f84a0b28e685e8301aac8b5ca2c80696412310300b6405 in requirements.txt \"]}}"
      },
      {
         "v1Compatibility": "{\"id\":\"54dca58a3b2e48ad6a5b51db76f82e9c4ac14baf7501a715d27a40228c1cf42f\",\"parent\":\"38f02d2a280c8df8dc97822b1d06afa5abd9434ba9816a724b9899b37d8d1857\",\"created\":\"2021-02-25T14:36:54.214431183Z\",\"container_config\":{\"Cmd\":[\"|1 DEBIAN_FRONTEND=noninteractive /bin/sh -c conda install -y -c pytorch     torchvision     numpy scipy pandas scikit-learn joblib tqdm ipython pip cython numba \\u0026\\u0026     pip install statsmodels pqdict xlearn ml_metrics tsfresh mlxtend h5py tempita \\u0026\\u0026     pip install xgboost lightgbm catboost \\u0026\\u0026     pip install tensorflow \\u0026\\u0026     pip install keras     \\u0026\\u0026 conda clean -ya\"]}}"
      },
      {
         "v1Compatibility": "{\"id\":\"38f02d2a280c8df8dc97822b1d06afa5abd9434ba9816a724b9899b37d8d1857\",\"parent\":\"f322970e485ded2c99e9f5807000e4123f17167ebe65df31f11a1dacef763af2\",\"created\":\"2021-02-25T14:24:42.6007957Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop)  ENV PATH=/usr/conda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"]},\"throwaway\":true}"
      },
      {
         "v1Compatibility": "{\"id\":\"f322970e485ded2c99e9f5807000e4123f17167ebe65df31f11a1dacef763af2\",\"parent\":\"4e236223ccfcfcb88a01bc8038927fb003a094021158edb274f888b47f681e37\",\"created\":\"2021-02-25T14:24:41.840774695Z\",\"container_config\":{\"Cmd\":[\"|1 DEBIAN_FRONTEND=noninteractive /bin/sh -c curl -sLo ~/miniconda.sh -O  https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh  \\u0026\\u0026      chmod +x ~/miniconda.sh \\u0026\\u0026      ~/miniconda.sh -b -p /usr/conda \\u0026\\u0026      rm ~/miniconda.sh\"]}}"
      },
      {
         "v1Compatibility": "{\"id\":\"4e236223ccfcfcb88a01bc8038927fb003a094021158edb274f888b47f681e37\",\"parent\":\"391a1b10fed9a0500c4d673784a4a0454a43e56440f91061d70b31baa9413584\",\"created\":\"2021-02-25T14:24:00.946714723Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop)  ENV CONDA_AUTO_UPDATE_CONDA=false\"]},\"throwaway\":true}"
      },
      {
         "v1Compatibility": "{\"id\":\"391a1b10fed9a0500c4d673784a4a0454a43e56440f91061d70b31baa9413584\",\"parent\":\"6eef95503618cf18089bd3533f88070409464dba8f6d816dd85704a422cc8b22\",\"created\":\"2021-02-25T14:24:00.07189637Z\",\"container_config\":{\"Cmd\":[\"|1 DEBIAN_FRONTEND=noninteractive /bin/sh -c apt-get update \\u0026\\u0026 apt-get install -y     libx11-6     gdebi-core     libapparmor1      libcurl4-openssl-dev     build-essential     gnupg2     cmake     \\u0026\\u0026 rm -rf /var/lib/apt/lists/*\"]}}"
      },
      {
         "v1Compatibility": "{\"id\":\"6eef95503618cf18089bd3533f88070409464dba8f6d816dd85704a422cc8b22\",\"parent\":\"fa34e0102c013df7746ced279e658968126c0da2488a5f5e30e34a8b67ba14ff\",\"created\":\"2021-02-25T14:22:20.04321914Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop)  ARG DEBIAN_FRONTEND=noninteractive\"]},\"throwaway\":true}"
      },
      {
         "v1Compatibility": "{\"id\":\"fa34e0102c013df7746ced279e658968126c0da2488a5f5e30e34a8b67ba14ff\",\"parent\":\"e19d114a4069123d8ea31652d2f9bf19f13c69dadaa6e3e6f5d49f471dc44fae\",\"created\":\"2021-02-09T12:26:22.362344323Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c locale-gen en_US.UTF-8 \\u0026\\u0026 update-locale\"]}}"
      },
      {
         "v1Compatibility": "{\"id\":\"e19d114a4069123d8ea31652d2f9bf19f13c69dadaa6e3e6f5d49f471dc44fae\",\"parent\":\"a6ff22232898c55b65243fa3334afe797dc8c95689170917ed8d43a0f844d5b8\",\"created\":\"2021-02-09T12:26:20.126790354Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c apt-get update -qq \\u0026\\u0026     apt-get install -y unzip locales \\u0026\\u0026     apt-get clean \\u0026\\u0026     rm -rf /var/lib/apt/lists/*\"]}}"
      },
      {
         "v1Compatibility": "{\"id\":\"a6ff22232898c55b65243fa3334afe797dc8c95689170917ed8d43a0f844d5b8\",\"comment\":\"Imported from Arcadia\",\"created\":\"2021-01-29T22:39:09.637789Z\",\"container_config\":{\"Cmd\":[\"\"]}}"
      }
   ],
   "signatures": [
      {
         "header": {
            "jwk": {
               "crv": "P-256",
               "kid": "4LWK:R4UB:VAUJ:DB3Q:MYYJ:7ADA:F2JX:AYNT:WWBE:5E7R:44ML:CLNG",
               "kty": "EC",
               "x": "i_RQJaARlq1Ja3wTcAAnWG0sH2g6J6KCpFZQwV_ESq4",
               "y": "V6pnZYrx249hGS29Ute3h-OvaoNy3nx6oR_n_l0S0FQ"
            },
            "alg": "ES256"
         },
         "signature": "zcTdTjzMH8hpdo7hLsQFysow7Cu9Hbf4CDO7q1g9LNxA72qn8UhIqLqQ2n-sFbUxHT4dt6GQBbX3tXJyDOTREA",
         "protected": "eyJmb3JtYXRMZW5ndGgiOjc3NzEsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAyMS0wOS0yNlQyMDoxNjowNVoifQ"
      }
   ]
}


Отсюда нам нужно получить две важные вещи: хэши и порядок слоёв образа из fsLayers, а ещё — переменные окружения из Env. И на самом деле это всё, что нам нужно от докера для подготовки окружений. Мы сами выкачиваем из docker-registry слои образов и метаинформацию об их последовательности. Слои — по сути обычные тарники.

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

Этапы подготовки окружения:

  • получаем список нужных образов (допустим, нам нужно окружение Java + Python 3.9);
  • получаем манифесты (список слоёв) каждого образа;
  • выкачиваем недостающие слои;
  • монтируем слои нескольких образов в единую директорию. Эта директория и станет корнем файловой системы контейнера исполнения решения.

Монтаж множества директорий в одну мы делаем примерно так же, как это делает докер — с помощью OverlayFS.

Объединённые файловые системы на примере OverlayFS


OverlayFS позволяет накладывать одно дерево каталогов (обычно доступное в режиме «чтение-запись») на другое, но с доступом только для чтения. Все изменения переходят на верхний слой с возможностью записи. © ArchWiki

Звучит круто! Давайте отвлечёмся от докера и попробуем вручную смонтировать OverlayFS.
Подготовим каталоги:

.
├── lower
│ ├── file.a
│ └── file.b
├── merged
├── upper
│ ├── file.a
│ └── file.c
└── workdir

содержимое lower и upper будем объединять в merged. Workdir — служебный каталог, в котором каталоги объединятся перед атомарным перемещением в merged.

Смонтируем upper и lower в merged:
mount -t overlay -o lowerdir=./lower,upperdir=./upper,workdir=./workdir overlay ./merged

И получим в каталоге merged вот в таком виде:

├── merged
│ ├── file.a
│ ├── file.b
└─└── file.c

файл file.a был взят из каталога upper.

Во время подготовки контейнера докер делает примерно то же самое. Это даже можно проверить с помощью команды docker inspect.

Запустим какой-нибудь докер-контейнер и выполним docker inspect {container_id}

$ docker run ubuntu

В секции GraphDriver увидим уже знакомые нам термины:

        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/8f76fee.../diff",
                "MergedDir": "/var/lib/docker/overlay2/8f76fee.../merged",
                "UpperDir": "/var/lib/docker/overlay2/8f76fee.../diff",
                "WorkDir": "/var/lib/docker/overlay2/8f76fee.../work"
            },
            "Name": "overlay2"
        }

OverlayFS на живом примере


Допустим, у нас есть задача, в которой для запуска чекера используется Python 3.9, а для запуска решения участника — Java 11.

Слои докерных образов для Java 11 и Python 3.9:
Java 11 Python 3.9
91b66f28e0f5355971d67074d108f09194e5ab8653ff93f623d353b8f851559f 91b66f28e0f5355971d67074d108f09194e5ab8653ff93f623d353b8f851559f
e0a5040427feb008958cf06bc0fd95593e1ae63058219d77d8fbbf50cb3af7b8 e0a5040427feb008958cf06bc0fd95593e1ae63058219d77d8fbbf50cb3af7b8
27e29f63878d809559ceba531389e227c2b5cbe9b86bc9ac5b6dbafc26ef3a7d 27e29f63878d809559ceba531389e227c2b5cbe9b86bc9ac5b6dbafc26ef3a7d
ce0fb3a26a16672f5bd3d1c59bfb43ca20ab0f810e5688c38d0c3d404410724a df3d10133e37e06c7973b724110da65f30af8c1cfa9699924b8539a82427534f
3a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
Merged
91b66f28e0f5355971d67074d108f09194e5ab8653ff93f623d353b8f851559f
e0a5040427feb008958cf06bc0fd95593e1ae63058219d77d8fbbf50cb3af7b8
27e29f63878d809559ceba531389e227c2b5cbe9b86bc9ac5b6dbafc26ef3a7d
ce0fb3a26a16672f5bd3d1c59bfb43ca20ab0f810e5688c38d0c3d404410724a
a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
df3d10133e37e06c7973b724110da65f30af8c1cfa9699924b8539a82427534f
Обратите внимание — эти образы достаточно близки, и мы можем переиспользовать бóльшую часть их слоёв.

Какие задачи мы решили таким образом


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

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

Несколько примеров необычных задач


Универсальные возможности платформы позволяют проводить проверку очень разноплановых заданий. Вы можете ознакомиться и порешать примеры:


Надеюсь, статья показалась вам интересной. Любопытно, применяет ли кто-то подобные подходы для решения своих задач, да и просто хотелось бы узнать ваше мнение о материале — пишите в комментариях.
Теги:
Хабы:
+24
Комментарии 6
Комментарии Комментарии 6

Публикации

Информация

Сайт
www.ya.ru
Дата регистрации
Дата основания
Численность
свыше 10 000 человек
Местоположение
Россия
Представитель