{{countBasket}}
От экспертов «1С-Рарус»: Подробное руководство по контейнеризации. Применение Docker для 1С. Часть 3
От экспертов «1С-Рарус»: Подробное руководство по контейнеризации. Применение Docker для 1С. Часть 3

От экспертов «1С‑Рарус»: Подробное руководство по контейнеризации. Применение Docker для 1С. Часть 3

30.10.2026
213 мин
108

Оглавление

  1. Введение
  2. Глава первая, в которой мы начинаем изучать сети
  3. Глава вторая, в которой мы переходим к плетению сетей
  4. Глава третья, в которой мы строим собственный TCP-сервер и учимся анализировать его трафик
  5. Глава четвертая, в которой мы соединяем в одну сеть два и более разрозненных контейнера
  6. Глава пятая, в которой мы поднимаем кластер из трех серверов 1С
  7. Заключение

Введение

Здравствуйте, друзья. В предыдущей статье Открыть ссылку в новом окне мы подробно изучили как развернуть среду контейнеризации на Linux и в Windows, а также для подсистемы WSL. Рассмотрели популярные docker клиенты. С помощью VSCode создали проекты для сборки и запуска контейнеров для 1С, Postgres, MSSQL. Однако часто рабочие среды не ограничиваются одним запущенным экземпляром 1С. Для корпоративного сегмента предлагается технология развертывания кластера 1С, который состоит из нескольких связанных между собой серверов. Основная сложность состоит в том, что эти серверы могут располагаться в разных сетях, но это не должно мешать взаимодействию запущенных на них сервисов 1С. В данной статье мы рассмотрим особенности построения такой архитектуры с помощью контейнеризации Docker.

Глава первая, в которой мы начинаем изучать сети

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

Общая модель сетей OSI

Вернемся в 70-е годы XX века. Это эпоха стремительного развития компьютерной индустрии и, в то же время — эпоха жесткой конкурентной борьбы. Каждый крупный производитель вычислительной техники — будь то IBM, Digital Equipment Corporation (DEC) или Hewlett-Packard (HP) — создавал собственные сетевые архитектуры исключительно для взаимодействия своих устройств.

Они использовали уникальные кабели, разные форматы данных и собственные правила передачи сигналов, чтобы подмять как можно больший кусок рынка под себя. Когда стало понятно, что в одиночку борьбу стандартов не выиграть, крупные компании стали объединяться. К 1977 году оформились две основные конкурирующие силы: Международная организация по стандартизации (ISO) и Международный консультативный комитет по телефонии и телеграфии (CCITT). Первую организацию спонсировали производители оборудования, вторую операторы телефонной связи. Но даже им пришлось договориться.

И в итоге в 1984 году появился совместный стандарт, получивший название Базовая эталонная модель взаимодействия открытых систем (Open Systems Interconnection, сокращенно — OSI), и одноименный протокол ISO/OSI, на который и обязали перейти всех производителей оборудования и ПО.

Модель OSI разделяет единый процесс передачи данных на семь строго иерархических уровней.

Семь уровней модели OSI

Каждый верхний уровень пользуется услугами нижнего и не вникает в тонкости его внутренней работы.

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

Стек протоколов TCP/IP

Неизвестно, как бы повернулась история развития ИТ-индустрии, если бы параллельно созданию OSI не велась разработка стека TCP/IP военным ведомством США (DARPA).

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

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

К тому же TCP/IP был бесплатным, что окончательно склонило на его сторону коммерческие и научные организации, которые в конечном счете просто взбунтовались против директив OSI.

Стек протоколов TCP/IP устроен проще, чем теоретическая модель OSI, и состоит всего из четырех уровней. Инженеры объединили близкие по смыслу функции, чтобы избавиться от излишних издержек при передаче данных.

  • Уровень сетевого доступа (Network Access Layer).
  • Межсетевой уровень (Internet Layer).
  • Транспортный уровень (Transport Layer).
  • Прикладной уровень (Application Layer).

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

Уровень сетевого доступа (Network Access Layer)

Самый нижний, базовый уровень TCP/IP — Уровень сетевого доступа. Он вобрал в себя функции Физического и Канального уровней модели OSI.

На физическом уровне происходит взаимодействие с аппаратной частью:

  • Преобразование цифровых данных (последовательности битов) в физические сигналы (электрические импульсы, свет или радиоволны).
  • Передача этих сигналов через соответствующую среду (медный провод, оптическое волокно, воздух).
  • Обратное преобразование полученных сигналов в биты данных.

Передача сигналов на физическом уровне

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

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

В рамках канального уровня происходит:

  • Формирование кадра данных.
  • Кодирование кадра для его устойчивой передачи.
  • Контроль целостности кадра (обнаружение ошибок и отсекание поврежденных данных).
  • Адресация устройств с помощью аппаратных MAC-адресов, чтобы кадр дошел до нужного получателя в локальной сети.

К устройствам, используемым на канальном уровне, относятся: коммутаторы, мосты. Они как раз и объединяют в единую сеть все остальные устройства.

Рассмотрим подробнее упрощенный пример передачи данных:

  • Предположим, у нас в сети есть 4 устройства. Каждое из них имеет сетевой адаптер со своим уникальным аппаратным адресом от 00:01 до 00:04.
  • Все устройства подключены к одному коммутатору.

    Схема подключения устройств к коммутатору

  • Пусть устройство 00:01 передает сообщение «Привет, мир!» устройству 00:03.
  • На выходе сообщение преобразовывается в кадр — специальную структуру, к которой, помимо самого текста сообщения, добавляется заголовок с адресом получателя 00:03 и адресом отправителя 00:01, а также контрольная сумма.
  • Кадр преобразуется в физические сигналы и передается на коммутатор.

    Преобразование кадра в физические сигналы

  • Коммутатор сверяется со своей внутренней таблицей адресов, находит порт №1, к которому подключен компьютер 00:03, и направляет кадры строго в его сторону.
  • Компьютер 00:03 принимает кадр, проверяет его целостность и преобразует обратно в исходное сообщение.

    Приём кадра данных компьютером

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

Устройство мост (Bridge) — это предшественник коммутатора. Исторически он использовался для соединения двух сегментов сети между собой и умел изолировать их трафик. На практике физические мосты сегодня почти не встречаются, однако эта концепция крайне широко применяется в виртуализации и контейнеризации, где Docker создает виртуальные мосты для связи контейнеров.

Межсетевой уровень (Internet Layer)

Компьютеры и коммутаторы образуют локальную сеть. Однако когда возникает вопрос соединения нескольких локальных сетей между собой, уровня сетевого доступа становится недостаточно. Здесь вступает в силу Межсетевой уровень TCP/IP, который соответствует Сетевому уровню модели OSI.

Межсетевой уровень объединяет разрозненные локальные сети в единую систему с помощью IP-адресации.

IP-адрес — это числовой адрес устройства в сети. Он выполняет две функции: идентифицирует узел (устройство) и определяет, к какой сети этот узел относится. Существует две версии IP-протокола IPv4 и IPv6, для простоты изложения мы будем рассматривать версию протокола IPv4.

IP-адрес для 4 версии протокола состоит из четырех чисел (октетов), каждый октет принимает значение от 0 до 255.

Сначала идет адрес сети, потом — адрес устройства в рамках данной сети. Для того, чтобы понять, где адрес сети, а где адрес устройства, к адресу добавляется маска сети, которая выполняет роль разделителя.

Например, устройство с адресом 192.168.0.1/24, (или 192.168.0.1 255.255.255.0), находится в сети с адресом 192.168.0.0/24 и имеет номер 1.

Чтобы понять, как разделяются адрес сети и адрес компьютера, необходимо перевести их в двоичную форму:

Разделение IP-адреса на адрес сети и адрес узла

Таким образом главной задачей межсетевого уровня становится маршрутизация, а именно:

  • Передача данных между сетями по IP-адресам.
  • Определение наилучшего пути для доставки данных от отправителя к получателю через множество промежуточных узлов (маршрутизаторов).

При этом протокол IP отвечает только за передачу пакета по адресу, но не за гарантию доставки, порядок и целостность — за это отвечают протоколы более верхнего уровня.

Важнейшим устройством в межсетевом взаимодействии является маршрутизатор.

Схема работы маршрутизатора

Маршрутизатор имеет как минимум два сетевых адаптера: одним он соединяется со своей сетью, другим адаптером он соединяется с другими маршрутизаторами организуя тем самым стыковочную подсеть.

Взаимодействие между маршрутизаторами можно настроить двумя способами:

  1. Статическая маршрутизация, когда вручную указывается, к какому маршрутизатору какая сеть привязана. Как правило она используется для настройки сетевого взаимодействия в рамках одной организации.
  2. Динамическая маршрутизация, когда между маршрутизаторами постоянно идет автоматический обмен информацией, у кого какие есть локальные сети. По собранной информации составляется таблица маршрутизации. Как правило данный вид маршрутизации применяется в глобальной сети интернет.

Расширим наш предыдущий пример:

  • Предположим, что у нас есть 2 локальные сети, в каждой из которых есть по 4 компьютера и по 1 маршрутизатору.
  • Адрес первой сети 192.168.0.0/24, адрес второй сети 192.168.1.0/24, адрес стыковочной сети 192.168.2.0/24.
  • Чтобы IP-адресация работала необходимо:

    • На каждом компьютере задать:
      • Индивидуальный IP-адрес.
      • Маску сети.
      • IP-адрес шлюза (маршрутизатора сети).
    • На каждом маршрутизаторе задать таблицу маршрутизации:

      • На маршрутизаторе 192.168.2.1: сеть 192.168.1.0/24 — через шлюз 192.168.2.2.
      • На маршрутизаторе 192.168.2.2: сеть 192.168.0.0/24 — через шлюз 192.168.2.1.
  • Пусть устройство 192.168.0.1 передает сообщение «Привет, мир!» устройству 192.168.1.3.
  • Для передачи формируется IP-пакет, в котором указывается IP-адрес отправителя, IP-адрес получателя, само сообщение, время жизни пакета TTL и другие технические данные.

    Формирование IP-пакета

  • Так как IP-адрес получателя относится к другой сети, то IP-пакет отправляется сразу на маршрутизатор: на выходе сетевого адаптера пакет преобразуется в кадр, после чего следует передача на коммутатор, с коммутатора на маршрутизатор (MAC‑адрес 00:05).
  • Маршрутизатор разбирает кадр, анализирует пакет, берет из него IP-адрес получателя, и по своей таблице маршрутизации определяет, на какой соседний маршрутизатор отправить: сетевая часть IP-адреса пакета должна совпасть с адресом сети в таблице маршрутизации. Пакет опять преобразуется в кадр и передается дальше.
  • Маршрутизатор-получатель снова из кадра достает пакет, и анализирует IP-адрес получателя: к его сети относится или нет. Если относится к его сети, то пакет упаковывается в кадр и отправляется на коммутатор для доставки компьютеру 192.168.1.3 (MAC-адрес 01:03).

    Передача IP-пакета через маршрутизаторы

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

Иногда таблицы маршрутизации соседних маршрутизаторов могут быть несогласованными (сбой синхронизации, неверная настройка статической маршрутизации). В таких случаях пакет попадает в цикл маршрутизации — передается по кругу от маршрутизатора к маршрутизатору. Чтобы такие пакеты не странствовали бесконечно долго, на этапе отправки задается время жизни пакета TTL (Windows — 128, Linux и MacOS — 64): оно уменьшается после прохода через каждый маршрутизатор, и как только счетчик заканчивается, пакет удаляется.

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

Транспортный уровень (Transport Layer)

Транспортный уровень TCP/IP соответствует Транспортному уровню OSI и берет на себя задачу надежной связи между взаимодействующими приложениями.

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

Соответственно, если мы расширим наш пример, то получим следующую процедуру передачи сообщения:

  • Предположим, что у нас есть 2 сети, в каждой из которых есть по 4 компьютера и по 1 маршрутизатору.
  • На компьютере 192.168.0.1 установлено «приложение 1», на компьютере 192.168.1.3 установлено «приложение 2».
  • Приложение 1 передает данные через порт 50000, приложение 2 принимает данные через порт 50001.
  • Пусть «приложение 1» передает сообщение «Привет, мир!» «приложению 2».
  • Способ передачи сообщений сильно зависит от выбранного протокола: TCP или UDP.
  • TCP (Transmission Control Protocol) — Протокол с установлением соединения.
    • Протокол TCP обменивается между отправителем и получателем сегментами. Каждый сегмент упаковывается в IP-пакет и передается получателю. Сегмент содержит:
      • порты отправителя и получателя,
      • номер последовательности,
      • системные данные,
      • полезную нагрузку.

        Структура сегмента TCP

    • Сначала, перед отправкой нашего сообщения, с помощью обмена служебными сегментами создается виртуальный канал связи между отправителем 192.168.0.1:50000 и получателем 192.168.1.3:50001.

      Установка виртуального канала TCP

    • Далее само сообщение разбивается на байты, каждый байт нумеруется. В сегменте записывается номер первого байта из помещенной в него последовательности байт.
    • При обмене получатель 192.168.1.3:50001 отчитывается о получении каждого сегмента отправителю 192.168.0.1:50000.
    • Если какой-то сегмент не дошел, он отправляется от 192.168.0.1:50000 заново.
    • На стороне получателя сегменты собираются обратно в сообщение согласно порядку нумерации байт.
  • UDP (User Datagram Protocol) — Протокол без установления соединения.

    • Сообщение просто преобразуется в датаграмму, которая отправляется без подтверждения доставки.

      Структура UDP-датаграммы

    • Процедуры передачи IP-пакетов и кадров остаются без изменения.

За счет простоты UDP, как правило, работает значительно быстрее, чем TCP. Его используют там, где скорость важнее идеальной точности (например, при передаче видеотрансляций или в системах точного времени NTP).

Также стоит отметить, что в случае протокола TCP сообщение разбивается на сегменты из-за технического ограничения сети — максимального размера полезной нагрузки кадра MTU (в стандарте Ethernet он равен 1500 байт). Однако UDP отправляет датаграмму одним блоком, и если сообщение больше MTU, то срабатывает механизм фрагментации на уровне IP, и датаграмма разбивается на несколько IP-пакетов. Как правило, приложения на UDP должны следить за размером отправляемых данных, но если этого не происходит, вероятность потери передаваемой информации возрастает в разы из-за отсутствия контроля доставки.

Трансляция адресов — NAT

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

Схема трансляции адресов NAT

Как же маршрутизатору понять, на какой компьютер отправить входящий запрос? Для этого служит механизм трансляции адресов NAT, с помощью которого в пакетах IP-адреса локальных компьютеров подменяются на IP-адреса маршрутизаторов и обратно.

Трансляция адресов NAT разделяется на два направления:

  1. SNAT (Source NAT) — подмена адреса отправителя в исходящих пакетах. Работает автоматически и обеспечивает исходящие соединения.
  2. DNAT (Destination NAT) — подмена адреса получателя во входящих пакетах. Требует ручной настройки правил проброса портов от маршрутизатора до компьютера с необходимым приложением.

Так как для передачи пакета маршрутизатором нужно знать и IP-адрес, и порт приложения, то можно сказать, что механизм NAT работает на стыке двух уровней TCP и IP.

Давайте перепишем наш пример работы сети с учетом NAT:

  • Итак, у нас есть 2 сети, в каждой из которых есть по 4 компьютера и по 1 маршрутизатору.
  • На компьютере 192.168.0.1 установлено «приложение 1», на компьютере 192.168.1.3 установлено «приложение 2».
  • Приложение 1 передает данные через порт 50000, приложение 2 принимает данные через порт 50001.
  • Помимо всех описанных ранее настроек, добавлены следующие:
    • Маршрутизаторы подключены к интернету, им назначены публичные IP-адреса: 85.80.35.40 для первой сети, 45.60.7.90 для второй сети.
    • В обоих сетях на маршрутизаторах включен NAT.
    • Статическая маршрутизация в этом случае не требуется.
    • В маршрутизаторе 45.60.7.90 вручную прописаны правила проброса портов, что все запросы, поступившие на порт 50001, отправляются устройству 192.168.1.3 на соответствующий порт.
  • Пусть «приложение 1» передает сообщение «Привет, мир!» «приложению 2». Однако «приложение 1» не знает про устройство 192.168.1.3, оно знает только публичный адрес маршрутизатора второй сети 45.60.7.90, поэтому запрос отправляется на адрес 45.60.7.90:50001.
  • Маршрутизатор первой сети получает IP-пакет и подменяет в нем адрес отправителя на свой публичный IP-адрес 85.80.35.40, делает соответствующую запись в таблице NAT (механизм SNAT) и передает пакет дальше на внешний IP-адрес 45.60.7.90.
  • Когда маршрутизатор 45.60.7.90 получает IP-пакет, он достает из него упакованный сегмент и смотрит в настроенные правила проброса портов, после чего в IP-пакете меняет адрес получателя (механизм DNAT).
  • IP-пакет передается на соответствующий компьютер 192.168.1.3 для обработки его «приложением 2».

    Передача пакета после трансляции адресов

  • Согласно протоколу TCP при получении сегмента необходимо отправить подтверждение, поэтому получатель 192.168.1.3 отправляет сегмент-подтверждение обратно на адрес 85.80.35.40.
  • Маршрутизатор подменяет адрес 192.168.1.3 на свой 45.60.7.90 и отправляет пакет на адрес 85.80.35.40.
  • Маршрутизатор 85.80.35.40 помнит, что до этого он отправлял IP-пакет от 192.168.0.1, и поэтому при получении подтверждения меняет IP-адрес получателя 85.80.35.40 на 192.168.0.1 и передает пакет далее.
  • В итоге получаем целостную картину работы протокола TCP в рамках механизма NAT.

Прикладной уровень (Application Layer)

На верхнем уровне архитектуры TCP/IP находятся протоколы взаимодействия между приложениями, грубо говоря, это те языки, на которых приложения общаются между собой через протокол TCP/IP. На этом уровне умещаются сразу три уровня OSI (Сеансовый, Представления и Прикладной уровни). Фактически вместо нашего сообщения «Привет, мир!» на прикладном уровне отправляются целые структуры.

Среди протоколов прикладного уровня можно выделить: HTTP/HTTPS (порты 80/443) для доступа к веб-сервисам, SSH и SFTP (порт 22) для удаленного управления и передачи файлов, AMQP (порт 5672) для обмена сообщениями RabbitMQ, специализированные сетевые службы самой «1С» (порты 1540, 1541, 1542, 1545, 1560-1591).

Также стоит отметить, что на прикладном уровне живут приложения, которые обеспечивают базовые сетевые функции, без которых взаимодействие по сети не было бы возможным:

  • DHCP — через этот сервис устройствам раздаются сетевые настройки: IP-адрес, маска, шлюз и адреса DNS-серверов.
  • DNS (порт 53) — данный сервис организует перевод доменных имен в IP-адреса.

Виртуализация сетевых устройств Linux

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

Еще в 1998 году в сетевом стандарте IEEE 802.1Q была закреплена технология VLAN (Virtual Local Area Network). Производители сетевого оборудования (прежде всего Cisco) научились разделять один физический коммутатор на несколько виртуальных на уровне его микропрограммы. Кадры стали маркироваться специальными цифровыми метками — тегами. Это позволило внутри одного физического провода передавать трафик изолированных и независимых друг от друга виртуальных сетей.

В 2001 году компания VMware выпустила ESX Server — гипервизор первого типа, который устанавливался напрямую на «железо» без операционной системы-прослойки. В рамках этой платформы логика работы сетевого оборудования была полностью перенесена в программный код. Появились программные виртуальные коммутаторы (vSwitch), использовавшие те же принципы тегирования и разделения сред.

С 2007 по 2013 год полностью оформилась общая концепция виртуальных сетей, а механизмы виртуализации сетевых устройств были окончательно закреплены в связке с механизмами изоляции в ядре Linux. Итогом этой эволюции стало появление концепции программно-определяемых сетей SDN (Software-Defined Networking), в которой сетевую архитектуру стали описывать на двух уровнях:

  1. Underlay — физические провода, коммутаторы и маршрутизаторы, задача которых состоит в быстрой доставке пакетов и кадров.
  2. Overlay — программный слой, который на лету создает виртуальные цифровые ландшафты поверх физической базы.

В рамках этой технологии были выделены ключевые виртуальные сетевые устройства:

  • Виртуальный адаптер (TAP/TUN-интерфейсы) — программная сетевая карта. При этом TAP работает с кадрами на канальном уровне, а TUN работает с IP-пакетами на межсетевом уровне.
  • Виртуальная пара интерфейсов (veth-пара / Virtual Ethernet) — программный кабель, который всегда создается в виде пары взаимосвязанных концов. Все, что попадает в один конец такого виртуального провода, мгновенно и без изменений выходит из другого.
  • Виртуальный мост (Linux Bridge) — программный аналог сетевого коммутатора. Он работает на канальном уровне модели OSI и умеет пересылать кадры между подключенными к нему устройствами на основе их MAC-адресов.

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

Виртуализация сетевых устройств Windows

Виртуализация Windows с точки зрения сети очень похожа на виртуализацию Linux. Для создания сетей также используются виртуальные устройства:

  • Виртуальный сетевой адаптер (vNIC).
  • Виртуальный коммутатор Hyper-V:
    • Внутренний виртуальный коммутатор (Internal vSwitch).
    • Внешний виртуальный коммутатор (External vSwitch).

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

Однако есть одно и принципиальное различие. Вспомним, что архитектура ядра Windows является гибридной. Поэтому за основные функции работы с сетью, включая стек TCP/IP, отвечает само ядро, а вот управляет виртуализацией отдельная системная служба HNS (Host Networking Service), работающая в пользовательском режиме. В связи с чем для Windows контейнера такого же уровня изоляции, как в Linux, достичь нельзя.

Глава вторая, в которой мы переходим к плетению сетей

Теория теорией, но как это все работает на практике? Здесь мы плавно подходим к технологии контейнеризации Docker, в которой рассмотрим различные варианты работы с виртуальными сетями.

Организация сетей в Docker

В первых версиях Docker использовались только встроенные механизмы виртуализации сети ядра Linux. Однако по мере развития технологии была сформулирована общая концепция сетей для контейнеров CNM (Container Network Model). В рамках нее появились следующие возможности:

  • Семейство команд docker network, с помощью которых можно создавать изолированные друг от друга виртуальные сети.
  • Специализированные драйверы сетей, с помощью которых создаются виртуальные пространства с разными параметрами работы.

Концепция CNM была унифицирована и для Linux, и для Windows, за исключением некоторых отличий, о которых мы расскажем далее.

В рамках данной концепции было выделено 5 типов сетей:

  • none,
  • host,
  • bridge,
  • macvlan,
  • overlay.

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

  • Хост — это сервер, на котором запущены контейнеры.
  • Внешняя сеть — это сеть, в которой функционирует хост под своим IP-адресом.
  • Внутренняя сеть — это сеть, в которой контейнеры взаимодействуют друг с другом.
  • ПК1 — компьютер с Docker. IP-адрес 192.168.0.101, доменное имя PC1.
  • ПК2 — второй компьютер с Docker. IP-адрес 192.168.0.102, доменное имя PC2.
  • ПК3 — третий компьютер с Docker. IP-адрес 192.168.0.103, доменное имя PC3.
  • WSL — подсистема Linux на Windows.
  • 1С:Скрипт — программный продукт 1С:Предприятие.Элемент скрипт.
  • VSCode — бесплатный кросс-платформенный редактор исходного кода от Microsoft.

Подготовка начального образа для анализа сетей

Для начала подготовим саму среду для работы с Docker: В качестве операционной системы для контейнеризации будем использоваться WSL Ubuntu. Данный вариант удобен с точки зрения быстрой настройки и тестирования сетевого взаимодействия.

О настройке среды для работы с Docker в WSL мы подробно рассказывали во второй части Открыть ссылку в новом окне нашей серии статей.
  • Нам необходимо 2 (а лучше 3) компьютера с ОС Windows (10 или 11 версии), которые работают в одной сети. Будем их называть ПК1, ПК2, ПК3.
    IP-адреса компьютеров у вас свои, а в статье будут использоваться соответственно адреса 192.168.0.101, 192.168.0.102, 192.168.0.103.
  • На каждом настроим WSL Ubuntu с установленной платформой Docker.
  • Установим 1С:Скрипт версии 9.1 и выше.
  • Установим VSCode для разработки контейнеров и плагин для работы с 1С:Скрипт (1C:Enterprise.Element Script).

Зачем нам нужен 1С:Скрипт? В версии 9.1 были реализованы объекты для работы с протоколом TCP. С помощью нового функционала можно не только реализовать TCP-клиент, но и настроить TCP-сервер, что нам позволит организовать свой простенький обмен.

Инициализируем проект в WSL на ПК1:

  • Зайдем в терминал WSL из powershell:
wsl
  • Создадим каталог, например, /srv/test_tcp с помощью команды в терминале WSL:
sudo mkdir -p "/srv/test_tcp"
  • Для того, чтобы можно было работать с проектом под текущим пользователем из VSCode, необходимо сменить владельца каталога (под sudo каталог создается под владельцем root):
sudo chown vasia:vasia "/srv/test_tcp"
  • Чтобы узнать своего пользователя (и подставить его вместо vasia) нужно выполнить:
whoami

Откроем VSCode, присоединимся к WSL и откроем созданный проект /srv/test_tcp.

Проект /srv/test_tcp в VSCode

Внимание Важно! Все операции в рамках WSL мы будем выполнять через терминал VSCode, чтобы не путаться с контекстом исполнения.

В панели «Проводник» создадим новые файлы:

  • dockerfile — файл описания образа.
  • docker-compose.yml — файл описания запуска контейнера.
  • context/pre_install — файл установки приложений в образе.
  • docker-compose-up.sh — файл запуска контейнера.
  • docker-compose-down.sh — файл остановки контейнера.

Файлы проекта в панели «Проводник»

Заполним созданные файлы.

dockerfile

FROM debian:bookworm-slim
ARG  DEBIAN_FRONTEND=noninteractive \
    DEBCONF_NONINTERACTIVE_SEEN=true
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN --mount=from=context,source=pre_install,target=/context/pre_install \
    set -eux; \
    . /context/pre_install; \
    install_packs
ENTRYPOINT [ "bash" ]

context/pre_install

#!/usr/bin/env bash

install_packs() {

    apt-get update \
        && apt-get install -yq --no-install-recommends locales procps curl ca-certificates unzip iproute2 tcpdump tshark xxd;
    apt-get clean \
        && rm -rf /var/lib/apt/lists/*;
    install -m 0755 -d /etc/apt/keyrings;
    sed -i -e "s/# ru_RU.UTF-8.*/ru_RU.UTF-8 UTF-8/" /etc/locale.gen \
        && dpkg-reconfigure --frontend=noninteractive locales \
        && update-locale LANG=ru_RU.UTF-8;

}

docker-compose.yml

services:
test_tcp:
image: oc/script/test-tcp:1.0.1
container_name: test_tcp
build:
dockerfile: dockerfile
context: .
additional_contexts:
- context=context
command: ["-c", "sleep infinity"]
tty: true

docker-compose-up.sh

#!/bin/bash
docker compose -f docker-compose.yml up -d --build

docker-compose-down.sh

#!/bin/bash
docker compose -f docker-compose.yml down

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

sudo chmod 766 *.sh

Обратите внимание, что в нашем проекте нет скрипта сборки образа, но при этом мы добавили секцию build в файл compose. С помощью данной секции можно описать все необходимые параметры сборки образа.

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

Теперь все готово для запуска контейнера, давайте приступим к изучению первого типа сети.

Сеть типа «пустая» (none)

Здесь все просто, контейнер поднимается без какой-либо сетевой связи с хостом.

Опишем в файле docker compose данный тип сети.

docker-compose.yml

services:
test_tcp:
image: oc/script/test-tcp:1.0.1
container_name: test_tcp
build:
dockerfile: dockerfile
context: .
additional_contexts:
- context=context
command: ["-c", "sleep infinity"]
network_mode: "none"
tty: true

Запустим контейнер:

sudo ./docker-compose-up.sh
✔ Image oc/script/test-tcp:1.0.1 Built
0.1s  
✔ Container test_tcp Started Started

Docker compose сообщил, что создан образ, контейнер запущен.

Присоединимся к контейнеру:

sudo docker exec -it test_tcp bash

И выполним команду:

ip ad
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
  link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  inet 127.0.0.1/8 scope host lo
    valid_lft forever preferred_lft forever
  inet6 ::1/128 scope link
    valid_lft forever preferred_lft forever

Виртуальных сетевых адаптеров в контейнере нет.

lo (loopback) — это виртуальный сетевой интерфейс, который «закольцован» сам на себя. То есть все, что через него отправляется, тут же возвращается обратно. Нужен этот интерфейс для сетевого взаимодействия приложений внутри одной машины по адресу 127.0.0.1 (localhost).

Выйдем из контейнера (exit) и остановим его выполнение:

sudo ./docker-compose-down.sh

Сеть типа хост (Host)

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

Схема работы сети типа хост

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

Однако для Windows-контейнеров данный тип сети недоступен: для них всегда создается свой сетевой интерфейс, который соединяется с виртуальным коммутатором.

Чтобы потестировать возможности host, изменим файл docker-compose.yml и добавим в него информацию о трех контейнерах.

services:
    test_tcp_1:
        image: oc/script/test-tcp:1.0.1
        container_name: test_tcp_1
        build:
            dockerfile: dockerfile
            context: .
            additional_contexts:
              - context=context
        command: ["-c", "sleep infinity"]
        network_mode: "host"
        tty: true
    test_tcp_2:
        image: oc/script/test-tcp:1.0.1
        container_name: test_tcp_2
        build:
            dockerfile: dockerfile
            context: .
            additional_contexts:
              - context=context
        command: ["-c", "sleep infinity"]
        network_mode: "host"
        tty: true
    test_tcp_3:
        image: oc/script/test-tcp:1.0.1
        container_name: test_tcp_3
        build:
            dockerfile: dockerfile
            context: .
            additional_contexts:
              - context=context
        command: ["-c", "sleep infinity"]
        network_mode: "host"
        tty: true

После чего запустим наши контейнеры:

sudo ./docker-compose-up.sh

Зайдем в один из них:

sudo docker exec -it test_tcp_1 bash

И выполним команду:

ip ad
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
  link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  inet 127.0.0.1/8 scope host lo
    valid_lft forever preferred_lft forever
  inet 10.255.255.254/32 brd 10.255.255.254 scope global lo
    valid_lft forever preferred_lft forever
  inet6 ::1/128 scope host
    valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
  link/ether 00:15:5d:cb:27:27 brd ff:ff:ff:ff:ff:ff
  inet 172.25.136.120/20 brd 172.25.143.255 scope global eth0
    valid_lft forever preferred_lft forever
  inet6 fe80::215:5dff:fecb:2727/64 scope link
    valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
  link/ether 5e:a9:ae:4a:1a:c5 brd ff:ff:ff:ff:ff:ff
  inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
  valid_lft forever preferred_lft forever
  inet6 fe80::5ca9:aeff:fe4a:1ac5/64 scope link
    valid_lft forever preferred_lft forever

Помимо сети lo, добавилось еще два интерфейса eth0, docker0 — это интерфейсы хоста:

  • eth0 — физическая сетевая карта.
  • docker0 — виртуальный коммутатор, который создается автоматически при запуске docker.

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

Сеть типа Macvlan

Иногда требуется, чтобы контейнер выглядел для внешней сети как полноценное физическое устройство с собственным IP‑адресом и уникальным MAC‑адресом. Это не частая задача, но все же в практике она встречается.

Windows-контейнеры внутри себя содержат службы, например WinRM, доступ к которым предоставляется строго по определенным портам. Размещая такие контейнеры во внутренней сети хоста, мы не можем просто пробросить порты этих служб во внешнюю сеть, так как они будут конфликтовать с такими же службами на хосте. Соответственно мы получаем контейнер, часть функционала которого закрыта. Для разрешения таких ограничений контейнеры можно разместить в сети типа macvlan.

Это очень интересный тип сети, давайте рассмотрим, как он реализуется в Linux:

  • На физической сетевой карте хоста включается режим PROMISC. В обычном режиме сетевая карта принимает кадры, предназначенные только для ее MAC‑адреса. В режиме PROMISC она начинает принимать все подряд кадры.
  • Как только запускается контейнер, для него создается виртуальный интерфейс со своим MAC-адресом прямо поверх физической карты хоста.
  • Когда на физическую карту приходит кадр, ядро Linux смотрит на его MAC‑адрес, и пересылает в соответствующий виртуальный сетевой интерфейс, то есть сетевая карта превращается в коммутатор.

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

Схема работы сети Macvlan

Windows сеть такого типа называется transparent. Однако если в случае Linux используется довольно простая надстройка над драйвером физической карты, то в Windows создается целое виртуальное устройство Внешний виртуальный коммутатор (External vSwitch), через который и осуществляется общение контейнеров с внешней сетью. Вследствие этого Windows-контейнеры работают медленнее, но при этом стабильнее.

Если мы будем рассматривать наш случай — контейнеры в WSL, то такую сеть настроить не получится. Точнее сама сеть в WSL поднимется, но доступ до нее будет ограничен. Режим macvlan требует прямого обращения к физической карте, а WSL прячется за внутренним виртуальным коммутатором Windows, что входит в противоречие к логике механизма. Поэтому, кратко познакомившись с ней, пойдем дальше.

Сеть типа мост (Bridge)

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

Схема работы сети типа мост

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

  • На хосте создается виртуальный сетевой интерфейс docker0 с уникальным MAC-адресом, который выполняет еще роль коммутатора.
  • Сетевому интерфейсу задается сетевой адрес 172.17.0.1/16, что сразу определяет следующие сетевые настройки для контейнеров:
    • адрес сети  172.17.0.0;
    • шлюз по умолчанию 172.17.0.1;
    • диапазон доступных адресов для контейнеров  от 172.17.0.2 до 172.17.255.254 (65533 адреса).
  • При запуске контейнера в нем создается виртуальный сетевой адаптер со своим  MAC-адресом, которому автоматически присваивается IP‑адрес из указанного диапазона.
  • Сетевой адаптер контейнера соединяется с виртуальным коммутатором с помощью veth‑пары.
  • Для того чтобы трафик внутренней сети не мог уйти во внешнюю сеть сам по себе, хост начинает выступать в виде маршрутизатора с включенным NAT.
  • Для того, чтобы приложение из контейнера было доступно во внешней сети, в настройках запуска контейнера прописывается проброс портов.

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

Доступ к контейнерам через IP-адрес хоста

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

Для Windows сеть такого типа называется nat. Концептуально она выглядит также: создается внутренний виртуальный коммутатор с которым соединяются виртуальные сетевые адаптеры контейнеров.

Изменим файл docker-compose.yml, убрав из него описание сети, по умолчанию включится bridge.

services:
    test_tcp_1:
        image: oc/script/test-tcp:1.0.1
        container_name: test_tcp_1
        build:
            dockerfile: dockerfile
            context: .
            additional_contexts:
              - context=context
        command: ["-c", "sleep infinity"]
        tty: true
    test_tcp_2:
        image: oc/script/test-tcp:1.0.1
        container_name: test_tcp_2
        build:
            dockerfile: dockerfile
            context: .
            additional_contexts:
              - context=context
        command: ["-c", "sleep infinity"]
        tty: true
    test_tcp_3:
        image: oc/script/test-tcp:1.0.1
        container_name: test_tcp_3
        build:
            dockerfile: dockerfile
            context: .
            additional_contexts:
              - context=context
        command: ["-c", "sleep infinity"]
        tty: true

После чего запустим наши контейнеры:

sudo ./docker-compose-up.sh
✔ Image oc/script/test-tcp:1.0.1 Built 31.4s
✔ Network test_tcp_default Created 0.0s
✔ Container test_tcp_1 Created
✔ Container test_tcp_2 Created
✔ Container test_tcp_3 Created

Выйдет сообщение о создании виртуальной сети test_tcp_default.

Получим список активных сетей на хосте:

sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
d6e4e4150a18     bridge bridge     local
38f289344470     host host local
9a46b8607ebd     none null local
e3f082bc6a07     test_tcp_default     bridge local

Мы видим все сети, которые доступны на данный момент: три сети по умолчанию, и одну только что созданную сеть. Сети по умолчанию создаются при установке docker и обеспечивают функционирование контейнеров при минимальных сетевых настройках.

Проанализируем сеть test_tcp_default подробней:

sudo docker network inspect test_tcp_default

[
    {
        "Name": "test_tcp_default",
        "Id": "0524eb10d0e650d0f40873749d0cbc8379d06d30c1522f080844fbdfde0d116f",
        "Created": "2026-10-22T23:59:00.68658391+04:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv4": true,
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.16.0.0/16",
                    "IPRange": "",
                    "Gateway": "172.16.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Options": {},
        "Labels": {
            "com.docker.compose.config-hash": "490e765b010adc8d572b340d41ce33e978f8129ba03345135bb6503734ad6ff7",
            "com.docker.compose.network": "default",
            "com.docker.compose.project": "test_tcp",
            "com.docker.compose.version": "5.0.0"
        },
        "Containers": {
            "41a87bbd8715ba533a72c565a6038ead7a3b4056a2e694ff099b79322e2de483": {
                "Name": "test_tcp_3",
                "EndpointID": "04b6a5b8c7a50a4df4b80cd9fd5bf60cb88db15ae6e282b000cb6ec92352c4a5",
                "MacAddress": "c6:b2:f7:35:dc:e2",
                "IPv4Address": "172.16.0.2/16",
                "IPv6Address": ""
            },
            "780dc22de7a8972d3807f19a621b69291dd5ea96f84cce63a231c61b596c9da5": {
                "Name": "test_tcp_1",
                "EndpointID": "05feb016197a57a2ad282b0157000b21ad53a728c8347238fe939702b33122a4",
                "MacAddress": "8e:5d:5d:c1:0a:21",
                "IPv4Address": "172.16.0.4/16",
                "IPv6Address": ""
            },
            "f58ae6f8637fba01efd5b15fcf275f886bbd4004a1ca368e547890921d437c3b": {
                "Name": "test_tcp_2",
                "EndpointID": "9c18311d84e01c28dc257f60b7dc4280e03c28b62ef5cab7f6943cdc9d6e18c3",
                "MacAddress": "8e:84:da:bb:66:c5",
                "IPv4Address": "172.16.0.3/16",
                "IPv6Address": ""
            }
        },
        "Status": {
            "IPAM": {
                "Subnets": {
                    "172.16.0.0/16": {
                        "IPsInUse": 6,
                        "DynamicIPsAvailable": 65530
                    }
                }
            }
        }
    }
]

Здесь можно увидеть:

  • Основные настройки сети:
    • "Subnet": "172.16.0.0/16".
    • "IPRange": "".
    • "Gateway": "172.16.0.1".
  • Параметры сетевых интерфейсов запущенных контейнеров:
    • "MacAddress": "c6:b2:f7:35:dc:e2".
    • "IPv4Address": "172.16.0.2/16".

Сетевые параметры легко меняются в файле docker-compose.yml. Давайте опишем сеть с другим адресом, и зафиксируем IP-адрес для контейнеров:

services:
    test_tcp_1:
        image: oc/script/test-tcp:1.0.1
        container_name: test_tcp_1
        build:
            dockerfile: dockerfile
            context: .
            additional_contexts:
              - context=context
        command: ["-c", "sleep infinity"]
        networks:
          test_tcp:
            ipv4_address: 10.10.0.2
        tty: true
    test_tcp_2:
        image: oc/script/test-tcp:1.0.1
        container_name: test_tcp_2
        build:
            dockerfile: dockerfile
            context: .
            additional_contexts:
              - context=context
        command: ["-c", "sleep infinity"]
        networks:
          test_tcp:
            ipv4_address: 10.10.0.3
        tty: true
    test_tcp_3:
        image: oc/script/test-tcp:1.0.1
        container_name: test_tcp_3
        build:
            dockerfile: dockerfile
            context: .
            additional_contexts:
              - context=context
        command: ["-c", "sleep infinity"]
        networks:
          test_tcp:
            ipv4_address: 10.10.0.4
        tty: true

networks:
  test_tcp:
    name: test_tcp
    ipam:
      config:
        - subnet: 10.10.0.0/24
          gateway: 10.10.0.1

Перезапустим контейнеры:

sudo ./docker-compose-up.sh
✔ Network test_tcp Created 0.3s
✔ Container test_tcp_3 Recreated 10.5s
✔ Container test_tcp_2 Recreated 10.6s
✔ Container test_tcp_1 Recreated

Если на хосте запустить docker network ls, то команда покажет уже 5 сетей. На самом деле, если бы мы выполнили docker compose down без изменения файла docker-compose.yml, то сеть test_tcp_default удалилась бы автоматически. Чтобы удалить неиспользуемую сеть используется команда:

sudo docker network rm test_tcp_default

Сети, созданные по-умолчанию: none, host, bridge — удалить нельзя.

Получим на хосте снова список сетевых интерфейсов:

ip ad

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet 10.255.255.254/32 brd 10.255.255.254 scope global lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:15:5d:b0:9e:25 brd ff:ff:ff:ff:ff:ff
    inet 172.25.136.120/20 brd 172.25.143.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::215:5dff:feb0:9e25/64 scope link
       valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether f6:de:f6:e1:26:b2 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::f4de:f6ff:fee1:26b2/64 scope link
       valid_lft forever preferred_lft forever
4: br-f208b934ccb2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether f2:df:39:0d:37:8c brd ff:ff:ff:ff:ff:ff
    inet 10.10.0.1/24 brd 10.10.0.255 scope global br-f208b934ccb2
       valid_lft forever preferred_lft forever
5: veth5a811f5@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-f208b934ccb2 state UP group default
    link/ether e6:1d:da:9e:9e:d0 brd ff:ff:ff:ff:ff:ff link-netnsid 5
    inet 10.10.0.2/24 brd 10.10.0.255 scope global br-f208b934ccb2
       valid_lft forever preferred_lft forever
6: veth82cc0cd@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-f208b934ccb2 state UP group default
    link/ether de:fe:57:70:54:04 brd ff:ff:ff:ff:ff:ff link-netnsid 6
    inet 10.10.0.3/24 brd 10.10.0.255 scope global br-f208b934ccb2
       valid_lft forever preferred_lft forever
7: vethe829de5@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-f208b934ccb2 state UP group default
    link/ether 92:ea:b3:cd:db:66 brd ff:ff:ff:ff:ff:ff link-netnsid 7
    inet 10.10.0.4/24 brd 10.10.0.255 scope global br-f208b934ccb2
       valid_lft forever preferred_lft forever

Видим, что здесь добавилось еще два устройства:

  1. br-f208b934ccb2 — это коммутатор нашей сети, которую мы определили в docker-compose.yml.
  2. veth*@if2 — а это сетевые veth-пары, они соединяют запущенные контейнеры с коммутатором:
    • Один конец вставлен в master br-f208b934ccb2.
    • Другой конец присоединен к сетевому интерфейсу контейнера if2 (link-netnsid 5).

Если просто остановить контейнер:

sudo docker stop test_tcp_1

То одно из сетевых устройств veth уйдет. Если свернуть compose среду:

sudo ./docker-compose-down.sh

То будут удалены все veth-пары и устройство br-f208b934ccb2.

Глава третья, в которой мы строим собственный TCP-сервер и учимся анализировать его трафик

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

Установка 1С:Скрипт в образе docker

TCP-сервер мы реализуем с помощью 1С:Предприятие.Элемент Скрипт.

Для установки 1С:Скрипт воспользуемся каналом, предложенным сайтом 1С для разработчиков (developer.1c.ru). Для этого в личном кабинете зайдем в раздел 1С:Предприятие.Элемент скрипт.

Раздел 1С:Предприятие.Элемент скрипт на developer.1c.ru

В данном разделе сгенерируем токен доступа к дистрибутиву.

Генерация токена доступа к дистрибутиву

Для задания версии 1С:Скрипт и токена для сборки образа добавим в проект 2 файла:

Файл переменных .env

SCRIPT_VERSION=10.0.4

Файл секретов .secret

SCRIPT_API_KEY=9d**********bb

Скрипт установки 1С:Скрипта по токену пропишем в новом файле context/install:

#!/usr/bin/env bash

install_packs() {

    archiveFile="script_x_linux_${SCRIPT_VERSION}.zip"

    curl -H "X-Developer-1c-Api:${SCRIPT_API_KEY}" -o "/tmp/${archiveFile}" "https://developer.1c.ru/applications/Console/api/v1/download/elementscript/${SCRIPT_VERSION}/linux"

    mkdir -p /tmp/1c-element-script
    unzip -o "/tmp/${archiveFile}" -d /tmp/1c-element-script
    rm "/tmp/${archiveFile}"

    mkdir -p /var/opt/script/
    mv /tmp/1c-element-script/* /var/opt/script/
    rm -r /tmp/1c-element-script

    chmod 755 /var/opt/script/executor
    chmod 755 /var/opt/script/bin/executor-x

    ln -s /var/opt/script/executor /bin;

    executor -v

}

В dockerfile добавим новый аргумент сборки SCRIPT_VERSION и создадим новый слой RUN для установки 1С:Скрипт.

FROM debian:bookworm-slim
ARG  DEBIAN_FRONTEND=noninteractive \
    DEBCONF_NONINTERACTIVE_SEEN=true
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN  --mount=from=context,source=pre_install,target=/context/pre_install \
    set -eux; \
    . /context/pre_install; \
    install_packs
ARG SCRIPT_VERSION
RUN --mount=from=context,source=install,target=/context/install \
    --mount=type=secret,id=script_api_key \
    . /run/secrets/script_api_key; \
    set -eux; \
    . /context/install; \
    install_packs
ENTRYPOINT [ "bash" ]

В файле docker-compose.yml оставим один контейнер и добавим разделы передачи аргументов и секретов.

services:
    test_tcp:
        image: oc/script/test-tcp:1.0.2
        container_name: test_tcp
        build:
            dockerfile: dockerfile
            context: .
            additional_contexts:
              - context=context
            args:
              SCRIPT_VERSION: ${SCRIPT_VERSION}
            secrets:
              - script_api_key
        command: ["-c", "sleep infinity"]
        networks:
test_tcp:
ipv4_address: 10.10.0.5
        tty: true

secrets:
  script_api_key:
    file: .secret
networks:
test_tcp:
name: test_tcp
ipam:
config:
- subnet: 10.10.0.0/24
gateway: 10.10.0.1

Обратите внимание, что секреты мы передаем через файл: во время сборки образа происходит инициализация указанных в нем переменных.

Запустим сборку образов, проверим, что образ был создан без ошибок (в случае ошибок платформа вам об этом ответственно заявит):

sudo docker compose build

Запуск TCP-сервера на 1С:Скрипт

Для тестирования TCP-протокола реализуем простейший TCP-сервер, который будет принимать сообщения в образе docker, и TCP-клиент, через который мы будем передавать наши сообщения со второго компьютера.

Создадим в нашем проекте файл test_tcp.sbsl со следующим кодом:

метод Скрипт(type = "client", address: Строка, port: Число)

если type == "server"
СерверTCP(address, port)
иначе если type == "client"
КлиентTCP(address, port)
иначе
возврат
;
;

метод КлиентTCP(Адрес: Строка, Порт: Число)

пока Истина
знч Сообщение = Консоль.СчитатьСтроку("Введите сообщение:")
если Сообщение == "exit"
прервать
;
исп Соединение = новый СоединениеTcp(Адрес = Адрес, Порт = Порт)
знч ЧтениеДанных = новый ЧтениеДанных(Соединение.ПолучитьПотокЧтения())
исп ПотокЗаписи = Соединение.ПолучитьПотокЗаписи()
ПотокЗаписи.Записать("%{Сообщение}\н", Кодировка.Utf8)
ПотокЗаписи.СброситьБуферы()
знч Ответ = ЧтениеДанных.ПрочитатьСтроку(Кодировка.Utf8, "\н")
Консоль.Записать("Ответ: %{Ответ}\н")
;
;

метод СерверTCP(Адрес: Строка, Порт: Число)

Консоль.Записать("Поднят TCP сервер %{Адрес}:%{Порт}")

исп Сервер = новый БлокирующийСерверTcp(Адрес = Адрес, Порт = Порт)

пока Истина
исп Соединение = Сервер.ОжидатьСоединения()
знч ЧтениеДанных = новый ЧтениеДанных(Соединение.ПолучитьПотокЧтения())
знч Сообщение = ЧтениеДанных.ПрочитатьСтроку(Кодировка.Utf8, "\н")
Консоль.Записать("%{Сообщение}")
исп ПотокЗаписи = Соединение.ПолучитьПотокЗаписи()
ПотокЗаписи.Записать("OK\н", Кодировка.Utf8)
ПотокЗаписи.СброситьБуферы()
Соединение.Закрыть()
;
;

В данном скрипте реализовано 2 режима работы:

  1. Режим сервера.
  2. Режим клиента.

В качестве параметров можно передать адрес и порт.

Изменим dockerfile, добавив секцию COPY:

FROM debian:bookworm-slim
ARG DEBIAN_FRONTEND=noninteractive \
    DEBCONF_NONINTERACTIVE_SEEN=true
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN --mount=from=context,source=pre_install,target=/context/pre_install \
    set -eux; \
    . /context/pre_install; \
    install_packs
ARG SCRIPT_VERSION
RUN --mount=from=context,source=install,target=/context/install \
    --mount=type=secret,id=script_api_key \
    . /run/secrets/script_api_key; \
    set -eux; \
    . /context/install; \
    install_packs
COPY [ "test_tcp.sbsl", "/test_tcp.sbsl" ]
ENTRYPOINT [ "executor" ]

Также в секцию ENTRYPOINT добавим вызов executor, чтобы любые команды с docker-compose направлялись сразу в 1С:Скрипт.

Изменим в файле docker-compose.yml секцию command, добавив:

  • Путь к файлу скрипта.
  • Режим запуска скрипта «server».
  • IP-адрес «0.0.0.0».
  • Порт, который будет передаваться через переменную TCP_SERVER_PORT.

IP-адрес 0.0.0.0 — это не localhost, как можно ошибочно посчитать, а служебное обозначение со значением «все адреса» или «любой». Если бы мы указали localhost (127.0.0.1), то TCP-сервер не смог бы получать сообщения с eth0-интерфейса, а только от других приложений в этом же контейнере по lo-интерфейсу. Когда же мы создаем TCP-сервер по адресу 0.0.0.0, мы ему говорим, чтобы он получал сообщения от всех интерфейсов по указанному порту.

Для того, чтобы наше приложение было доступно на хосте, необходимо также добавить проброс портов в секции ports:

services:
    test_tcp:
        image: oc/script/test-tcp:1.0.3
        container_name: test_tcp
        build:
            dockerfile: dockerfile
            context: .
            additional_contexts:
              - context=context
            args:
              SCRIPT_VERSION: ${SCRIPT_VERSION}
            secrets:
              - script_api_key
        command: [ "/test_tcp.sbsl", "server", "0.0.0.0", "${TCP_SERVER_PORT}" ]
        networks:
          test_tcp:
            ipv4_address: 10.10.0.5
        ports:
          - "${TCP_SERVER_PORT}:${TCP_SERVER_PORT}"
        tty: true

secrets:
  script_api_key:
    file: .secret

networks:
  test_tcp:
    name: test_tcp
    ipam:
      config:
        - subnet: 10.10.0.0/24
          gateway: 10.10.0.1

Переменную TCP_SERVER_PORT определим в файле .env:

SCRIPT_VERSION="10.0.4"
TCP_SERVER_PORT="50000"

Запустим наш контейнер:

./docker-compose-up.sh

И запустим команду лога сообщений:

docker logs test_tcp

Если все хорошо, должно выйти сообщение «Поднят TCP сервер 0.0.0.0:50000»:

Сообщение о запуске TCP-сервера в логах

Запуск TCP-клиента на 1С:Скрипт

Проверим, что с ПК2 есть доступ до TCP-сервера. Для этого выполним команду в powershell:

Test-NetConnection 192.168.0.101 -Port 50000

На самом деле эта команда должна выдать ошибку таймаута, потому что по умолчанию WSL недоступен во внешней сети. WSL управляется гипервизором Windows и, также как для Windows-контейнеров, для него создается отдельный внутренний виртуальный коммутатор. Если посмотреть повнимательней на интерфейс eth0 в WSL, то его IP-адрес, например 172.25.136.120/20, будет отличаться от IP-адреса ПК1.

В итоге мы получаем следующий сетевой путь до контейнера:

  • Физическая сетевая карта ПК1.
  • Виртуальный коммутатор 172.25.128.1/20 для WSL.
  • Виртуальный сетевой адаптер WSL 172.25.136.20/20.
  • Виртуальный коммутатор docker 10.10.0.0/12.
  • Виртуальный сетевой адаптер контейнера 10.10.0.5/12.

Посмотрим на ПК1 доступные сетевые интерфейсы, запустив в консоли powershell команду:

Ipconfig
Настройка протокола IP для Windows
Адаптер Ethernet Ethernet:
   DNS-суффикс подключения . . . . . :
   Локальный IPv6-адрес канала . . . : fe80::dd3e:3892:a5e2:dd84%24
   IPv4-адрес. . . . . . . . . . . . : 192.168.0.101
   Маска подсети . . . . . . . . . . : 255.255.255.0
   Основной шлюз. . . . . . . . . : 192.168.0.1
Адаптер Ethernet vEthernet (WSL (Hyper-V firewall)):
   DNS-суффикс подключения . . . . . :
   Локальный IPv6-адрес канала . . . : fe80::362a:6a12:a3bc:5046%49
   IPv4-адрес. . . . . . . . . . . . : 172.25.128.1
   Маска подсети . . . . . . . . . . : 255.255.240.0
   Основной шлюз. . . . . . . . . :

Здесь отображаются часть нашей цепочки: физическая сетевая карта и виртуальный коммутатор WSL.

Однако передать сообщение в контейнер из внешней сети мы не можем: WSL живет в NAT-сети Hyper-V, и входящие сообщения извне по умолчанию не транслируются. Доступ с самого ПК1 к сервисам WSL работает через механизм localhost forwarding. Поэтому мы можем отправлять TCP-сообщения через localhost.

Для соединения сети WSL с внешней сетью пропишем вручную маршрутизацию. Для этого в powershell наберем следующую команду:

netsh interface portproxy add v4tov4 listenport=50000 listenaddress=0.0.0.0 connectport=50000 connectaddress=127.0.0.1

Здесь мы говорим следующее: все входящие соединения, пришедшие на любой локальный IP-адрес (listenaddress=0.0.0.0) и порт 50000 (listenport=50000), будут перенаправлены на адрес 127.0.0.1 (connectaddress=127.0.0.1) и тот же порт 50000 (connectport=50000).

Мы специально не стали прописывать IP-адрес WSL, так как он устанавливается автоматически и может смениться.

Проверить, что правило применилось, можно командой:

netsh interface portproxy show all
Прослушивать ipv4: Подключиться к ipv4:
Адрес Порт Адрес Порт
--------------- ---------- --------------- ----------
0.0.0.0 50000 127.0.0.1 50000

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

netsh interface portproxy delete v4tov4 listenport=50000 listenaddress=0.0.0.0

Но это еще не все, необходимо добавить правило в брандмауэр Windows, разрешающее входящие соединения по порту 50000.

New-NetFirewallRule -DisplayName "TCP 50000 to WSL" -Direction Inbound -Protocol TCP -LocalPort 50000 -Action Allow

После этого можно проверить соединение с нашим контейнером с компьютера ПК2 еще раз:

Test-NetConnection 192.168.0.101 -Port 50000
ComputerName : test
RemoteAddress : 192.168.0.101
RemotePort : 50000
SourceAddress : 192.168.0.101
TcpTestSucceeded : True

Теперь все готово к запуску TCP-клиента и проверки соединения с TCP-сервером. В powershell на ПК2 выполним команду:

executor <ПутьКФайлу>\test_tcp.sbsl client 192.168.0.101 50000

И здесь же в консоли введем сообщение «Hello world!».

В ответ получим «ОК» — значит, сеанс связи с TCP-сервером прошел успешно.

Если в WSL на ПК1 запустить команду:

docker logs test_tcp

То в логе будет запись:

«Hello world!»

Анализ трафика TCP

Очень часто при анализе работы сетевых приложений (будь они установлены в контейнере или просто в операционной системе — не важно) большую сложность представляет анализ того, какие запросы по сети к ним поступают. Незнание особенностей функционирования протокола TCP/IP и неумение работать с инструментами, анализирующими TCP-трафик, могут затянуть расследование ошибок на долгие дни.

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

tcpdump — консольный анализатор сетевого трафика. Перехватывает пакеты с сетевого интерфейса и выводит их содержимое прямо в терминал.

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

Про более глубокий анализ трафика 1С написано в статье «Зависимость скорости работы форм от структуры и объема данных, перехват трафика в экспертизе 1С Открыть ссылку в новом окне».

Итак:

  • У нас есть 2 компьютера — ПК1 и ПК2.
  • На ПК1 запущен TCP-сервис.
  • На ПК2 запущен TCP-клиент.
  • Приложения tcpdump и tshark уже установлены в образе контейнера (мы их внесли в файл pre_install в самом начале).

Подключимся к нашему контейнеру:

sudo docker exec -it test_tcp bash
Внимание Важно! Стоит отметить, что при запуске docker exec комнада из секции ENTRYPOINT [ «executor» ] не выполняется, а запускается просто отдельное новое окно bash.

В контейнере запустим захват входящего TCP-трафика командой:

tcpdump -i eth0 -n port 50000

После чего с ПК2 в запущенном ранее TCP-клиенте отправим сообщение, например «Test»:

Введите сообщение: Test
Ответ: ОК

В контейнере получим следующий вывод:

Вывод tcpdump при захвате TCP-трафика

Первые 3 строчки — это системные команды установки соединения:

07:09:01.248849 IP 10.10.0.1.40332 > 10.10.0.5.50000: Flags [S], seq 1266075934, win 64240, options [mss 1460,sackOK,TS val 2063683610 ecr 0,nop,wscale 7], length 0

07:09:01.248866 IP 10.10.0.5.50000 > 10.10.0.1.40332: Flags [S.], seq 1139718081, ack 1266075935, win 65160, options [mss 1460,sackOK,TS val 2932208111 ecr 2063683610,nop,wscale 7], length 0

07:09:01.248883 IP 10.10.0.1.40332 > 10.10.0.5.50000: Flags [.], ack 1, win 502, options [nop,nop,TS val 2063683610 ecr 2932208111], length 0

  • Клиент инициирует соединение (Флаг [S] — SYN).
  • Сервер подтверждает получение SYN от клиента (Флаг [S.] — SYN-ACK).
  • Клиент завершает рукопожатие, подтверждая SYN сервера (Флаг [.] — ACK).

Далее идет обмен данными:

07:09:01.290975 IP 10.10.0.1.40332 > 10.10.0.5.50000: Flags [P.], seq 1:6, ack 1, win 502, options [nop,nop,TS val 2063683652 ecr 2932208111], length 5

07:09:01.290984 IP 10.10.0.5.50000 > 10.10.0.1.40332: Flags [.], ack 6, win 510, options [nop,nop,TS val 2932208153 ecr 2063683652], length 0

07:09:01.291391 IP 10.10.0.5.50000 > 10.10.0.1.40332: Flags [P.], seq 1:6, ack 6, win 510, options [nop,nop,TS val 2932208154 ecr 2063683652], length 5

07:09:01.291408 IP 10.10.0.1.40332 > 10.10.0.5.50000: Flags [.], ack 6, win 502, options [nop,nop,TS val 2063683653 ecr 2932208154], length 0

  • Клиент отправляет 5 байт данных (Флаг [P.] — PUSH-ACK).
  • Сервер подтверждает получение 5 байт от клиента (Флаг [.] - ACK).
  • Сервер отправляет клиенту свой ответ размером 5 байт (Флаг [P.] — PUSH-ACK).
  • Клиент подтверждает получение данных от сервера (Флаг [.] — ACK).

Завершается обмен закрытием соединения:

07:09:01.291422 IP 10.10.0.5.50000 > 10.10.0.1.40332: Flags [F.], seq 6, ack 6, win 510, options [nop,nop,TS val 2932208154 ecr 2063683653], length 0 07:09:01.291575 IP 10.10.0.1.40332 > 10.10.0.5.50000: Flags [F.], seq 6, ack 7, win 502, options [nop,nop,TS val 2063683653 ecr 2932208154], length 0 07:09:01.291578 IP 10.10.0.5.50000 > 10.10.0.1.40332: Flags [.], ack 7, win 510, options [nop,nop,TS val 2932208154 ecr 2063683653], length 0
  • Сервер решает закрыть соединение первым (Флаг [F.] — FIN-ACK).
  • Клиент одновременно подтверждает закрытие от сервера и отправляет свой флаг для закрытия сессии (Флаг [F.] — FIN‑ACK).
  • Сервер подтверждает получение FIN от клиента (Флаг [.] — ACK).
  • Соединение полностью закрыто.

Как мы видим, в дампе очень много технических сообщений. Чтобы получить пакеты только с полезной нагрузкой, и увидеть содержимое этой нагрузки, нужно перезапустить нашу команду с дополнительными флагами (для закрытия текущего сеанса анализа нужно нажать Ctrl + C):

tcpdump -i eth0 -A -s0 -l 'tcp port 50000 and (ip[2:2] - ((ip[0] & 0xf) << 2) - ((tcp[12] & 0xf0) >> 2)) > 0'

Где:

  • '(ip[2:2] - ((ip[0] & 0xf) << 2) - ((tcp[12] & 0xf0) >> 2)) > 0' — добавлено ограничение на длину пакета.
  • -A — флаг вывода сообщений в ASCII формате.
  • -s0 –l — вывод без задержек всего поступившего текста.

Отправим сообщение с клиента и получим следующий вывод:

listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
12:47:07.459591 IP 10.10.0.1.45557 > 543ac7f4dd8d.50001: Flags [P.], seq 3231980246:3231980252, ack 2265132727, win 502, options [nop,nop,TS val 1676564604 ecr 3783032698], length 6
E..:+ @.@...
.....Q.............F.....
c.X|.|.zTest

12:47:07.459908 IP 543ac7f4dd8d.50001 > 10.10.0.1.45557: Flags [P.], seq 1:4, ack 7, win 510, options [nop,nop,TS val 3783032713 ecr 1676564604], length 3
E..7..@.@...
...Q...............C.....
.|..c.X|OK

Здесь можем увидеть сообщения с переданным текстом «Test» и полученным ответом «OK». tcpdump — это базовая утилита для анализа трафика, однако она все же выводит лог в сыром виде. Есть множество дополнительных флагов для фильтрации сообщений, но это усложняет внешний вид команды и не всегда приводит к нужному результату без дополнительной обработки. К тому же кириллица выводится в нечитаемом виде.

Куда более удобная и функциональная утилита tshark, давайте запустим ее:

tshark -i eth0 -o 'data.show_as_text:TRUE' -Y 'tcp.len > 0' -T fields -e frame.time -e data.len -e data.text

Где:

  • -o 'data.show_as_text:TRUE' -Y 'tcp.len > 0 — выводить сообщения только с текстом.
  • -T fields -e frame.time -e data.len -e data.text — выводимая информация, время размер сообщения, текст.

При выводе получим довольно простой вывод:

** (tshark:1347) 09:09:56.289539 [Main MESSAGE] -- Capture started.
** (tshark:1347) 09:09:56.289586 [Main MESSAGE] -- File: "/tmp/wireshark_eth0E9JPX3.pcapng"
Oct 21, 2026 09:10:09.981949007 UTC 5 Test\n
Oct 21, 2026 09:10:09.982203768 UTC 3 OK\n

tshark предлагает более понятный и универсальный функционал фильтрации сообщений и определения порядка вывода параметров сообщения.

Однако в такой форме нельзя получать сообщения в кириллице, немного преобразуем команду и добавим в нее обработку выводимых строк с помощью xxd:

tshark -i eth0 -l -Y 'tcp.len>0' -T fields -e frame.time -e tcp.len -e tcp.payload \
| xargs -d '\n' -n1 sh -c 'IFS=$(printf "\t"); set -- $1; printf "%s\t%s\t" "$1" "$2"; echo "$3" | xxd -r -p; echo' _

Здесь у нас сообщения выводятся в сыром hex-формате, после чего преобразуются в кириллицу. Запустим команду и передадим сообщение «Тест»:

** (tshark:1377) 09:13:57.489692 [Main MESSAGE] -- Capture started.
** (tshark:1377) 09:13:57.489739 [Main MESSAGE] -- File: "/tmp/wireshark_eth0H0TTX3.pcapng"
Oct 21, 2026 09:14:12.559845302 UTC 9 Тест
Oct 21, 2026 09:14:12.560120405 UTC 3 OK

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

Напоследок запустим анализ трафика в таком виде:

tshark -i eth0 -f "tcp port 50000" -Y "tcp.len > 0" -V

И отправим сообщение с TCP-клиента.

Наше сообщение придет на ПК1 в виде довольно большой структуры.

Структура пакета в выводе tshark

На самом деле — это структура вложенности: сообщение — сегмент — IP-пакет — кадр. Очень интересное визуальное представление всей той теории, которую мы с вами ранее прошли.

  • Кадр:
Ethernet II, Src: be:b2:13:15:c7:ab, Dst: 12:f2:fa:72:5a:f8
     Destination (Куда): 12:f2:fa:72:5a:f8
     Source (Откуда): be:b2:13:15:c7:ab
     Type: IPv4 (0x0800) -> Указывает, что внутри лежит IP-пакет
  • IP-пакет:
Internet Protocol Version 4, Src: 10.10.0.1, Dst: 10.10.0.5
    Version: 4
     Header Length: 20 bytes -> Размер самого IP-заголовка
     Total Length: 57 -> Размер IP-пакета вместе с TCP-сегментом внутри
     Protocol: TCP (6) -> Указывает, что внутри лежит TCP-сегмент
     Source Address (Отправитель): 10.10.0.1
     Destination Address (Получатель): 10.10.0.5
  • Сегмент:
Transmission Control Protocol, Src Port: 45012, Dst Port: 50000, Seq: 1, Ack: 1, Len: 5
    Source Port (Порт отправителя): 45012
     Destination Port (Порт получателя): 50000
     Sequence Number: 1 -> Порядковый номер байта в потоке
     Header Length: 32 bytes -> Размер TCP-заголовка (с опциями)
     Flags: 0x018 (PSH, ACK) -> Пакет подтверждает получение (ACK) и просит протолкнуть данные (PSH)
     TCP Segment Len: 5 -> Размер полезных данных (сообщения) внутри сегмента
  • Сообщение:
Data (5 bytes)
    HEX-дамп: 54 65 73 74 0a
     Текст:    Test\n

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

Глава четвертая, в которой мы соединяем в одну сеть два и более разрозненных контейнера

Цель, к которой мы с вами стремимся, — организация устойчивой инфраструктуры из нескольких взаимодействующих контейнеров. Давайте определим её параметры, чтобы обосновать наш финальный выбор и пояснить, почему другие варианты настроек не подходят. А в качестве финального примера развернем кластер 1С, состоящий из следующих узлов: сервер СУБД, сервер лицензирования 1С, рабочий сервер 1С и дополнительный сервер 1С для распределения нагрузки.

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

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

Помимо высокой автоматизации запуска контейнеров, такой инфраструктуре требуется стабильная сеть. Ни один из разобранных ранее типов сетей в таком режиме работать стабильно не будет:

  • host — в данном случае контейнер привязывается к IP-адресу хоста, при переносе на другой хост все сетевые настройки слетают.
  • macvlan — здесь контейнер привязывается к IP-адресу физической сети хоста, и если другой сервер находится в другой сети, то снова изменяется IP-адрес.
  • bridge — контейнеры находятся в изолированных внутренних сетях хоста, и сервисы с разных серверов видят друг друга только через NAT по адресам хостов: единое адресное пространство разваливается на островки.

Во всех случаях контейнер всегда остается привязанным к хосту.

Привязка контейнеров к хостам

А хотелось бы, чтобы контейнеры работали в едином адресном пространстве и видели друг друга в одной локальной сети, находясь физически на разных серверах. Роль такой сети в Docker выполняет overlay.

Сеть Overlay

Идея overlay-сети органично вырастает из архитектуры предыдущих типов сетей. Она разрывает жесткую связь контейнеров с конкретным хостом, создавая единое виртуальное пространство. Хосты объединяются в один большой виртуальный коммутатор, к которому и подключаются контейнеры. Физически между серверами ходят обычные кадры реальной сети, но внутри них спрятаны IP-пакеты виртуальной сети, через которую общаются контейнеры.

Схема overlay-сети

Мы возвращаемся как бы к самому простому и понятному уровню сетевого взаимодействия.

Технологическая основа Docker Overlay базируется на передаче пакетов во внешнюю сеть через специальный интерфейс VXLAN. Работает это следующим образом:

  • Предположим, приложение из Контейнера 1 передает сообщение «Привет, мир!» Контейнеру 2. Контейнеры при этом запущены на разных физических хостах.
  • Это сообщение последовательно упаковывается в Данные → Сегмент → IP-пакет → Кадр по всем правилам стека протоколов TCP/IP.
  • Данный кадр передается через виртуальный сетевой интерфейс контейнера на виртуальный коммутатор overlay-сети.
  • С виртуального коммутатора пакет попадает в интерфейс VXLAN.
  • В рамках этого интерфейса к кадру добавляется заголовок VXLAN, после чего вся конструкция упаковывается в обычную UDP-датаграмму и отправляется по физической сети на другой хост, где развернут контейнер-получатель.
  • На втором хосте «посылка» идентифицируется по заголовку VXLAN, распаковывается в оригинальный кадр, отправляется на внутренний виртуальный коммутатор overlay-сети и далее — на виртуальный сетевой адаптер второго контейнера.

Схема overlay-сети

На основании чего VXLAN понимает, на какой именно хост нужно отправить UDP-датаграмму? Он использует специальную таблицу пересылки — FDB (Forwarding Database). Она работает прямо в ядре Linux и связывает виртуальную сеть с физической. В этой таблице записано строгое соответствие: «Виртуальный MAC-адрес контейнера -> Физический IP-адрес хоста, на котором этот контейнер запущен».

Виртуальный MAC-адрес (внутри Overlay) Физический IP хоста (в реальной сети)
02:42:0a:00:00:02 (Контейнер 2) 192.168.0.102 (Хост 2)
02:42:0a:00:00:03 (Контейнер 3) 192.168.0.103 (Хост 3)

А вот заполнение этой таблицы происходит с помощью другого мощного механизма Docker Swarm.

Docker Swarm

Docker Swarm — это встроенный в Docker инструмент для оркестрации контейнеров. Проще говоря, это технология, которая позволяет объединить несколько физических или виртуальных серверов (нод) в единый виртуальный суперкомпьютер (кластер) и управлять контейнерами на них из одной точки.

Если обычный Docker управляет контейнерами на одном конкретном сервере, то Docker Swarm делает это на десятках и сотнях серверов одновременно.

С помощью Docker Swarm мы можем построить отказоустойчивую инфраструктуру. Вот ключевые возможности, которые мы получаем:

  • Отказоустойчивость (High Availability): если один из серверов физически отключится или сломается, Swarm мгновенно заметит это и автоматически перезапустит все работавшие на нем контейнеры на других живых серверах.
  • Масштабирование (Scaling): Swarm может запускать несколько однотипных реплик контейнера и автоматически распределять между ними нагрузку.
  • Обновления без простоев (Rolling Updates): при обновлении приложений контейнеры перезапускаются по очереди. Если что-то пойдет не так, сработает механизм автоматического отката (rollback) к стабильной версии.
  • Overlay-сеть: именно в рамках кластера Swarm между контейнерами на разных хостах создается и функционирует overlay-сеть. Swarm сам заполняет таблицу FDB на всех нодах и берет на себя функцию встроенного DNS-сервера для контейнеров.
Кто-то при прочтении статьи может спросить: «Почему не Kubernetes?». Kubernetes — в разы более сложная технология, которая на начальном этапе изучения оркестрации может отбить всё желание разбираться. Docker Swarm намного дружелюбнее к новичкам, разворачивается за пару минут и идеально подходит для средних систем с несколькими десятками контейнеров.

Итак, у нас есть компьютеры, на которых настроены WSL и Docker. Объединим их в один кластер. Но перед запуском команд оркестратора необходимо донастроить саму среду WSL на каждом узле.

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

В конфигурационном файле Windows по адресу C:\Users\<Ваше_Имя_Пользователя>\.wslconfig необходимо добавить следующий блок (если файла нет, создайте его вручную):

[wsl2]
networkingMode=mirrored

После этого WSL необходимо полностью перезапустить в командной строке Windows (PowerShell):

wsl --shutdown
wsl

Включение этого параметра переводит сеть WSL из режима NAT в режим зеркалирования (Mirrored). Этот параметр расширяет сетевые интерфейсы Windows-хоста прямо внутрь WSL. Благодаря этому контейнеры и службы внутри WSL становятся доступны во внешней сети по реальному IP-адресу компьютера без сложной маршрутизации.

При запуске wsl в режиме зеркалирования может возникнуть ошибка:

wslwsl: Внутренняя ошибка.Код ошибки:
CreateInstance/CreateVm/ConfigureNetworking/0x8007054fwsl: Не удалось настроить сеть (networkingMode Mirrored). Выполняется возврат к networkingMode None.

Это известная проблема. Она возникает, когда Служба сети узла Windows (HNS) не может выделить необходимый диапазон портов для зеркального режима из-за конфликта с другими программами (например, гипервизорами, VPN или прокси). Сторонний софт часто сдвигает стандартный динамический диапазон портов Windows.

Чтобы проверить текущий диапазон, выполните в PowerShell от имени администратора:

netsh int ipv4 show dynamicport tcp
Протокол tcp Динамический диапазон портов
---------------------------------
Начальный порт : 1024
Число портов : 65534

Если в выводе вы увидите «Начальный порт: 1024», это значит, что диапазон сбит. Необходимо вернуть настройки Windows к значениям по умолчанию командами:

netsh interface ipv4 set dynamicportrange protocol=tcp startport=49152 numberofports=16384
netsh interface ipv6 set dynamicportrange protocol=tcp startport=49152 numberofports=16384

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

Для корректной коммуникации узлов Swarm между собой необходимо открыть системные порты в брандмауэре Windows. Выполните в PowerShell от имени администратора следующие команды на всех компьютерах будущих нод:

New-NetFirewallRule -DisplayName "Docker Swarm Management (TCP)" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 2377
New-NetFirewallRule -DisplayName "Docker Swarm Discovery (TCP)" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 7946
New-NetFirewallRule -DisplayName "Docker Swarm Discovery (UDP)" -Direction Inbound -Action Allow -Protocol UDP -LocalPort 7946
New-NetFirewallRule -DisplayName "Docker Swarm Overlay VXLAN (UDP)" -Direction Inbound -Action Allow -Protocol UDP -LocalPort 4789

Чтобы избежать конфликт порта 50000 необходимо удалить ранее настроенную маршрутизацию командой:

netsh interface portproxy delete v4tov4 listenport=50000 listenaddress=0.0.0.0

Вот теперь всё готово для создания самого кластера!

Создание кластера Docker Swarm

Сначала необходимо на одном из компьютеров (пусть это будет ПК1) инициализировать менеджер кластера.

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

sudo ./docker-compose.down

После чего выполним команду инициализации. Обратите внимание: в случае использования WSL нам необходимо явно передать реальный IP-адрес хоста во внешней сети через параметр --advertise-addr:

sudo docker swarm init --advertise-addr 192.168.0.101
Swarm initialized: current node (j9v8...) is now a manager.
To add a worker to this swarm, run the following command:
    docker swarm join --token SWMTKN-1-39gq8... 192.168.0.101:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

После инициализации в консоли появится готовая команда с токеном для добавления рабочих узлов в кластер. Если вы случайно очистили консоль и не запомнили токен, его всегда можно запросить повторно командой:

sudo docker swarm join-token worker
To add a worker to this swarm, run the following command:
    docker swarm join --token SWMTKN-1-39gq8... 192.168.0.101:2377

Теперь в терминале WSL на ПК2 выполним команду добавления узла:

sudo docker swarm join --token SWMTKN-1-39gq8... 192.168.0.101:2377
This node joined a swarm as a worker.

И на ПК3 выполним точно такую же команду:

sudo docker swarm join --token SWMTKN-1-39gq8... 192.168.0.101:2377
This node joined a swarm as a worker.

Вот таким незамысловатым образом мы настроили среду оркестрации. Чтобы посмотреть итоговый состав нашего кластера, выполним на менеджере (ПК1) команду:

sudo docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
6uyy8du8jgi913m2pkt4ql8xm * PC1 Ready Active Leader 29.8.0
s5v9paqtrlvs9uh8h1r6xfnla PC2 Ready Active   29.8.0
w2kbcpg8fcpkdryxkdsjrzb4e PC33 Ready Active   29.8.0

Давайте посмотрим, как изменились сетевые интерфейсы на узле ПК3 после входа в Swarm:

ip ad
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0@if411: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether bc:24:11:50:21:75 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 192.168.0.102/23 brd 192.168.63.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::be24:11ff:fe50:2175/64 scope link
       valid_lft forever preferred_lft forever
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 7a:f8:aa:6c:f9:f6 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
13: docker_gwbridge: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 9e:b9:09:28:4a:09 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 brd 172.18.255.255 scope global docker_gwbridge
       valid_lft forever preferred_lft forever
    inet6 fe80::9cb9:9ff:fe28:4a09/64 scope link
       valid_lft forever preferred_lft forever
15: vethd49e18a@if14: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP group default
    link/ether 56:01:9b:a0:2f:bc brd ff:ff:ff:ff:ff:ff link-netnsid 2
    inet6 fe80::5401:9bff:fea0:2fbc/64 scope link
       valid_lft forever preferred_lft forever

Здесь, помимо стандартного моста docker0, появились новые системные компоненты Swarm:

  • docker_gwbridge — это виртуальный шлюз-коммутатор кластера. Он необходим для того, чтобы контейнеры из изолированных overlay-сетей могли выходить во внешнюю сеть хоста и интернет (через механизм NAT).
  • vethd49e18a@if14 — это интерфейс виртуального кабеля (veth-пара). Одним концом он подключен к коммутатору docker_gwbridge, а вторым уходит в изолированное сетевое пространство имен хоста (ingress_sbox). Там развернут встроенный балансировщик, который принимает входящий трафик из внешнего мира и распределяет его по контейнерам. Самостоятельно интерфейсы overlay-сетей (VXLAN) на хосте не видны — Docker изолирует их ради безопасности.

Если вы хотите запустить docker swarm в среде Proxmox на LXC контейнерах, то вот вам небольшая подсказка (проверено на Proxmox 9.1):

  • В настройках контейнеров должны быть указаны следующие параметры:

    unprivileged: 0
    lxc.apparmor.profile: unconfined
  • При запуске контейнеров должна быть выполнена следующая команда:

    nsenter --net=/run/docker/netns/ingress_sbox sysctl -w net.ipv4.ip_forward=1

Запуск контейнеров в Docker Swarm

В Docker Swarm центральным понятием вместо отдельного контейнера становится стек. Стек — это не просто набор контейнеров, а единый манифест всей системы — всё, что должно работать в кластере. Описывается он в файле docker-stack.yml. По файлу Swarm разворачивает инфраструктуру декларативно: текстом указывается, что должно быть, а Swarm сам решает, где и как это запустить.

Иерархия стека имеет следующий вид:

  • Стек — это описание всей системы.
  • Сервисы — это декларативное описание того, как должно работать приложение: сколько реплик, какие порты, переменные окружения, ограничения по ресурсам, сетевые правила. Сервис — это еще не контейнер, а «обещание».
  • Задачи — это атомарные единицы планирования оркестрации. Когда необходимо выполнить какое-нибудь действие, под него создается задача. При выполнении задачи у нее начинает меняться статус, который отслеживает Swarm.
  • Контейнеры — это фактическая реализация задачи: процесс, который реально работает на конкретном сервере.

На основе такой архитектуры реализована отказоустойчивость кластера Docker Swarm. Предположим, что на одном из узлов падает контейнер из-за нехватки памяти. Swarm постоянно сравнивает реальное состояние кластера с декларацией из docker-stack.yml. Если возникает расхождение, он автоматически создаёт новую задачу (новый слот) на подходящем узле. На основе этой задачи запускается новый контейнер, и система снова приходит в соответствие с декларацией.

Файл docker-stack.yml по своей структуре очень похож на файл docker-compose.yml, но имеет дополнительные параметры, используемые только в swarm. Давайте перенесем в него запуск нашего контейнера

services:
  test_tcp:
    image: oc/script/test-tcp:1.0.3
    command: [ "/test_tcp.sbsl", "server", "0.0.0.0", "${TCP_SERVER_PORT}" ]
    networks:
      - test_net
    ports:
      - target: ${TCP_SERVER_PORT}
        published: ${TCP_SERVER_PORT}
        mode: ingress
    tty: true
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure

networks:
  test_net:
    driver: overlay

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

export $(cat .env | xargs) && sudo -E docker stack deploy -c docker-stack.yml test_tcp
Creating network test_tcp_test_net
Creating service test_tcp_test_tcp

Здесь переменные из файла .env передаются иначе, чем для docker compose.

Проверим на каком узле запустился контейнер:

sudo docker service ps test_tcp_test_tcp
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
t143hgs39zmg test_tcp_test_tcp.1 oc/script/test-tcp:1.0.3 PC1 Running Running 5 seconds ago

Реестр образов

Для того, чтобы Docker Swarm мог запускать любые контейнеры на любых узлах, ему нужно централизованное хранилище образов Docker. Самым простым сервисом для организации такого хранилища является docker registry.

При этом стоит уточнить один нюанс, реестр образов нужен не контейнерам, а менеджеру Swarm, который будет получать оттуда образы, соответственно доступность реестра нужна не в виртуальной сети overlay, а в сети хостов. Если наши хосты будут располагаться в разных сетях, то им нужен централизованный доступ к реестру контейнеров, поэтому лучше всего его опубликовать по интернет-адресу.

Для нашего эксперимента и для описания возможностей Docker Swarm, разместим реестр образов через стек на менеджере, но в жизни так делать не стоит.

Создадим отдельный файл docker-stack-registry.yml:

services:
  registry:
    image: registry:latest
    networks:
      - registry
    ports:
      - target: 5000
        published: 5000
        mode: host
    volumes:
      - docker_registry:/var/lib/registry

    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
      placement:
        constraints:
          - node.role == manager

networks:
  registry:
    driver: overlay

volumes:
  docker_registry:

Мы жестко закрепили расположение сервиса за менеджером node.role == manager.

Обратите внимание, что режим проброса портов мы указали host, этот момент связан с контейнером ingress-endpoint, и о нем мы расскажем в отдельной главе.

Теперь развернем новый стек:

sudo docker stack deploy -c docker-stack-registry.yml registry

Можно посмотреть список стеков:

sudo docker stack ls
NAME SERVICES
registry 1
test_tcp 1
И список сервисов
sudo docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
tie8u9xwsq4l registry_registry replicated 1/1 registry:latest *:5000->5000/tcp
gx359kqsuxx2 test_tcp_test_tcp replicated 1/1 oc/script/test-tcp:1.0.3 *:50000->50000/tcp

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

sudo docker tag oc/script/test-tcp:1.0.3 192.168.0.101:5000/script/test-tcp:1.0.3

После чего загрузка образа в реестр выполняется командой:

sudo docker push 192.168.0.101:5000/script/test-tcp:1.0.3
The push refers to repository [192.168.0.101:5000/script/test-tcp]
Get "https://192.168.0.101:5000/v2/": http: server gave HTTP response to HTTPS client

Если вы сразу выполните команду, то у вас выйдет ошибка требования HTTPS. Это защита docker-клиента, так как образы содержат исполняемый код, поэтому они должны раздаваться только с доверенных адресов по зашифрованному протоколу.

В нашем примере мы можем поместить IP-адрес ПК1 в доверенные, выполнив три строчки кода настройки daemon.json:

sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<EOF
{
  "insecure-registries": ["192.168.0.101:5000"]
}
EOF

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

sudo docker push 192.168.0.101:5000/script/test-tcp:1.0.3
The push refers to repository [192.168.0.101:5000/script/test-tcp]
f6c7610d6fd8: Pushed
e263b0129948: Pushed
d21502bcdd5d: Pushed
9b35de789337: Pushed

Данные настройки daemon.json нужно выполнить на всех трех машинах: ПК1, ПК2, ПК3.

Откроем порт 5000 на ПК1:

New-NetFirewallRule -DisplayName "Docker Registry" -Direction Inbound -Protocol TCP -LocalPort 5000 -Action Allow

Теперь нам нужно обновить стек docker-stack.yml и переопубликовать его:

services:
  test_tcp:
    image: 192.168.0.101:5000/script/test-tcp:1.0.3
    command: [ "/test_tcp.sbsl", "server", "0.0.0.0", "${TCP_SERVER_PORT}" ]
    networks:
      - test_net
    ports:
      - target: ${TCP_SERVER_PORT}
        published: ${TCP_SERVER_PORT}
        mode: ingress
    tty: true

    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure

networks:
  test_net:
    driver: overlay
export $(cat .env | xargs) && sudo -E docker stack deploy -c docker-stack.yml test_tcp
Внимание Важно! Не забывайте менять IP-адрес ПК1 192.168.0.101 на реальный адрес вашей сети.

Оркестрация Swarm

Основная настройка инфраструктуры готова, теперь можно пощупать основные возможности Docker Swarm.

Репликация контейнеров

Сначала давайте посмотрим, как работает репликация контейнеров. В файле docker-compose.yml в секции deploy мы изначально указали 1 реплику. Чтобы изменить число реплик «на лету», не меняя конфигурационный файл, выполним команду:

sudo docker service scale test_tcp_test_tcp=2
overall progress: 2 out of 2 tasks
1/2: running [==================================================>]
2/2: running [==================================================>]
verify: Service test_tcp_test_tcp converged

Посмотрим какие задачи выполнил наш сервис:

sudo docker service ps test_tcp_test_tcp
y0h8ga7z6w33 test_tcp_test_tcp.1 192.168.0.101:5000/script/test-tcp:1.0.3 PC1 Running Running 4 hours ago
bvu0lmmyk04c test_tcp_test_tcp.2 192.168.0.101:5000/script/test-tcp:1.0.3 PC2 Running Running 2 minutes ago

Мы видим, что вторая копия контейнера (.2) успешно запустилась на узле PC2. Если мы укажем scale 3, то запустятся три контейнера на трех узлах. Если выбрать 4 и более, то контейнеры начнут циклически и равномерно заполнять доступные узлы в кластере.

В секции deploy файла конфигурации можно гибко регулировать, где именно будут запускаться контейнеры. Для этого используются ограничения (constraints):

  • По роли узла (например, запускать строго на node.role == worker или node.role == manager).
  • По имени узла (node.hostname == PC1).
  • По пользовательским меткам/тегам узлов (node.labels.disk == ssd).

Аварийное завершение сервера

В нашем примере запущено 2 контейнера: один на менеджере (ПК1), второй на рабочем узле (ПК2). Намеренно отключим узел ПК2 от сети (симулируем аварию). Подождав немного, проверим состояние нашего сервиса:

sudo docker service ps test_tcp_test_tcp
r8w3j88wgwk8 test_tcp_test_tcp.2 192.168.0.101:5000/script/test-tcp:1.0.3 PC3 Running Starting less than a second ago
bvu0lmmyk04c \_ test_tcp_test_tcp.2 192.168.0.101:5000/script/test-tcp:1.0.3 PC2 Shutdown Running 5 minutes ago

Мы видим, что на упавшем узле ПК2 старый контейнер перешел в состояние Shutdown (так как менеджер потерял с ним связь), но Swarm тут же автоматически запланировал и запустил новый контейнер на резервном узле ПК3.

В этом и состоит главный принцип оркестрации: Swarm постоянно следит за желаемым состоянием инфраструктуры (declarative state). В случае сбоя одного из узлов он мгновенно пытается восстановить работоспособность сервиса на оставшихся живых хостах.

Внимание Важно! Локальные тома контейнеров (Volumes) автоматически не реплицируются между узлами кластера. Данные физически остаются на том жестком диске, где контейнер был запущен изначально. Поэтому сервисы наподобие баз данных, использующие локальные папки хоста, обычно жестко привязывают к конкретному узлу с помощью ограничений (constraints). Чтобы добиться полной свободы перемещения контейнеров с данными в Swarm (как и в Kubernetes), необходимо подключать внешние распределенные сетевые хранилища (NFS, GlusterFS, Ceph) через специализированные плагины томов.

Сетевая связанность и входная балансировка. Сеть ingress

Итак, у нас запущено 2 одинаковых контейнера (реплики одного сервиса). Если мы отправим запрос с TCP-клиента, на какой из контейнеров он придет? Или придет сразу на оба?

Так как TCP — это протокол с установлением соединения, пакет придет только на один из контейнеров, который выберет встроенный балансировщик. На оба контейнера сразу пакет не продублируется.

Зайдем в любой контейнер и выполним команду:

ip ad

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
241: eth0@if242: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
    link/ether 02:42:0a:00:01:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.0.1.3/24 brd 10.0.1.255 scope global eth0
       valid_lft forever preferred_lft forever
243: eth1@if244: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether fe:b5:7b:94:a4:52 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet 172.18.0.3/16 brd 172.18.255.255 scope global eth1
       valid_lft forever preferred_lft forever
245: eth2@if246: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
    link/ether 02:42:0a:00:00:04 brd ff:ff:ff:ff:ff:ff link-netnsid 2
    inet 10.0.0.4/24 brd 10.0.0.255 scope global eth2

       valid_lft forever preferred_lft forever

Здесь мы видим сразу три сетевых адаптера:

  1. eth0 (сеть ingress) — overlay-сеть, которая создается в Docker Swarm по умолчанию. Через него балансируются входящие запросы с помощью механизма Routing mesh.
  2. eth1 (сеть docker_gwbridge) — специальный мост (bridge), через который контейнер получает доступ во внешнюю сеть (например, чтобы скачать обновления или достучаться до внешнего API).
  3. eth2 (пользовательская overlay-сеть) — сеть, которую мы создали сами для безопасной связи между контейнерами разных сервисов внутри Swarm.

Проще говоря, в зависимости от отправителя и получателя используются разные механизмы. Для связи контейнер <-> контейнер используется overlay-сеть. Для связи контейнер <-> внешняя сеть (то есть доступ изнутри) используется мост docker_gwbridge с включенной функцией NAT. Для связи внешняя сеть <-> контейнер (то есть при доступе извне) используется балансировка Routing mesh.

Механизмы связи в зависимости от направления трафика

Как работает балансировка Routing mesh:

  • Предположим, на один из хостов кластера (не важно, запущен ли на нем целевой контейнер или нет) на порт 50000 пришло сообщение от клиента «Привет, мир!».
  • Ядро хоста перенаправляет этот трафик в изолированное сетевое пространство балансировщика (ingress_sbox), где встроенный механизм IPVS (IP Virtual Server) решает, на какую именно реплику отправить пакет.
  • Пакет инкапсулируется в VXLAN и отправляется по overlay-сети ingress на хост, где запущен выбранный контейнер, и попадает в него через интерфейс eth0.
  • Контейнер, получив пакет, обрабатывает его и отправляет ответ обратно тем же путем — через интерфейс eth0 в сеть ingress и далее клиенту.

Таким образом, если поднято несколько реплик, мы не можем заранее сказать, какой именно контейнер обработает конкретный TCP-запрос — за распределение отвечает балансировщик Swarm. Главное, что пакет точно будет обработан одной из живых реплик.

Можно потренироваться: запустить TCP-клиент, отправить серию запросов на порты хостов и через команду docker logs <имя_контейнера> на разных репликах посмотреть, как именно Swarm распределяет входящие сообщения между ними.
Однако ingress-сети очень капризны: самой распространенной проблемой являются сбои в работе с IPVS ядра Linux (особенно под высокой нагрузкой). Поэтому в реальном продуктиве от стандартного ingress часто отказываются, используя альтернативные подходы:

  • Режим публикации портов host. Порты контейнера привязываются напрямую к сетевому интерфейсу хоста (как мы сделали для контейнера registry). Это решает проблемы с IPVS и сохраняет реальные IP-адреса клиентов, но при этом теряется встроенная сетевая балансировка Swarm, а на одном хосте нельзя запустить две реплики сервиса на одинаковом порту.
  • Использование внешних балансировщиков (Nginx или Traefik). Это самый гибкий и популярный подход. Балансировщик запускается в режиме host на портах (80 и 443), принимает весь входящий трафик и распределяет его между репликами приложения через изолированную внутреннюю overlay-сеть. Это возвращает кластеру балансировку, причем на более «умном» уровне.

Глава пятая, в которой мы поднимаем кластер из трех серверов 1С

Наконец-то мы дошли до последней главы! Попробуем развернуть кластер swarm приближенный к реальности. Для этого будем использовать готовый репозиторий github.com/agibalovsa/-1C_DevOps.git, где задана часть необходимых настроек.

Создание образов 1С, Postgres

  1. Подключимся к WSL на ПК1.
  2. Скопируем репозиторий 1C_DevOps:

    git clone https://github.com/agibalovsa/-1C_DevOps.git oc_devops
  3. Установим программу envsubst:

    sudo curl -L https://github.com/a8m/envsubst/releases/download/v1.2.0/envsubst-`uname -s`-`uname -m` -o envsubst;
    sudo chmod +x envsubst;
    sudo mv envsubst /usr/local/bin;
    sudo envsubst
  4. В VSCode откроем папку oc_devops через «Файл → Открыть папку».

  5. Запустим скрипт:

    sudo bash docker/init.sh
  6. Укажем имя debian:

    Ввод имени debian в скрипте init.sh

  7. Укажем раздел build:

    Выбор раздела build

  8. Выберем раздел OS System:

    Выбор раздела OS System

  9. В каталоге docker/users/builds/debian зададим значения в файле .arg:

    OS_EXT_TAG="debian:bookworm-slim"
    OS_TAG="os/debian:bookworm-slim"

    Здесь путь задан относительно головной папки.

  10. В каталоге docker/users/builds/common_context зададим значения в общем файле .arg:

    REGISTRY="192.168.0.101:5000/"
  11. Соберется образ базовой операционной системы:

    sudo bash docker/users/builds/debian/docker-build.sh
  12. Запустим снова init:

    sudo bash docker/init.sh
  13. Внесем имя «jre».

  14. Укажем «build».

  15. Выберем тип JDK:

    Выбор типа JDK

  16. В каталоге docker/users/builds/jre зададим только указанные значения в файле .arg (остальные значения нужно оставить без изменения):

    OS_TAG="os/debian:bookworm-slim" JDK_AXIOM_VERSION="8u513+5"
    JDK_PROD="Axiom"
    JDK_MAJOR_VERSION="8"

    В текущем примере устанавливается 8 версия JRE от Axiom.

  17. Запустим скрипт сборки:

    sudo bash ./docker/users/builds/jre/docker-build.sh
  18. Соберется образ с установленной платформой JRE.
  19. Запустим снова init:

    sudo bash docker/init.sh
  20. Укажем имя os-server.
  21. Укажем «build».
  22. Выберем тип «1C Platform»:

    Выбор типа 1C Platform

  23. В каталоге docker/users/builds/oc-server в файле .arg зададим только следующие значения (остальные значения нужно оставить без изменения):

    OS_TAG="axiom/jre-full:8u513.5"
    OC_VERSION="8.3.27.2342"
    OC_MODE="server"
    OC_MODE_SERVER="1"
    OC_MODE_WS="0"
    OC_MODE_CRS="0"
    OC_MODE_CLIENT="0"
    INSTALL_MODE="install_from_url"
  24. Также в каталоге docker/users/builds/oc-server зададим учетные данные в файле .secret:

    OC_URL_LOGIN="login"
    OC_URL_PASSWORD="password"
  25. Выполним команду:

    sudo bash docker/users/builds/oc-server/docker-build.sh "push"
  26. Соберется образ с установленной платформой 1С.
  27. После установки наш образ должден поместиться в docker-registry, настроенный нами раннее.
  28. Запустим снова init:

    sudo bash docker/init.sh
  29. Укажем имя «oс-postgres».
  30. Укажем «build».
  31. Выберем тип «Postgres»:

    Выбор типа Postgres

  32. В каталоге docker/users/builds/oc-postgres в файле .arg зададим только следующие значения (остальные значения нужно оставить без изменения):

    OS_TAG="os/debian:bookworm-slim"
    PG_MAJOR_VERSION="18"
    PG_VERSION="18.4"
    PG_MANUFACTURER="Postgres pro"
    PG_PATH="/var/lib/pgpro/1c-18"
    PG_BIN_PATH="/opt/pgpro/1c-18/bin"
    PG_SHARE_PATH="/opt/pgpro/1c-18/share"
    PG_RU=1
  33. Запустим сборку образа:

    sudo bash docker/users/builds/postgres/docker-build.sh "push"

    Соберется образ с установленным Postgres с патчем под 1С от компании Postgres Pro. Образ также будет помещен в docker-registry.

  34. В итоге мы получили 2 образа:

    1. Для 1С — 1c/server:8.3.27.2342.
    2. Для Postgres — 1c/postgres:18.4

    Обязательно проверьте, что они имеют тег для docker-registry, и помещены в репозиторий:

    • 192.168.0.101:5000/1c/server:8.3.27.2342.
    • 192.168.0.101:5000/1c/postgres:18.4.

Описание файла стека 1С, Postgres

  1. Теперь сгенерируем файл docker-compose для нашей архитектуры.
  2. Запустим снова init:

    sudo bash docker/init.sh
  3. Укажем имя «oc-postgres»:

    Ввод имени os-postgres

  4. Укажем «stack»:

    Выбор раздела compose

  5. Выберем 2 типа «1C Platform (server)», «Postgres»:

    Выбор типов 1C Platform (server) и Postgres

  6. Выбираем тип инициализации «Use .env file» в значение «No»

    Выбираем тип инициализации «Use .env file» в значение «No»

  7. В каталоге docker/users/stack/oc-postgres/oc-postgres будут созданы 4 файла:

    • common-compose-1c-server.yml,
    • common-compose-postgres.yml,
    • docker-stack-1c-server.yml,
    • docker-stack-postgres.yml.

    В каталоге docker/users/stack/oc-postgres/oc-postgres будут созданы 4 файла

    Нам необходимо кастомизировать эти файлы под нашу среду.

  8. Изменим в файле common-compose-1c-server.yml следующие пункты:

    • Зададим image: 192.168.0.101:5000/1c/server:8.3.27.2342;
    • Зададим hostname:PC1.

    Напомним, что 192.168.0.101 и PC1 необходимо поменять на IP и hostname вашего компьютера.

    • Зададим command: oc=server.
  9. В итоге секция oc-server должна принять вид:

       oc-server:

        image: 192.168.0.101:5000/1c/server:8.3.27.2342
        hostname: PC1
        command: oc=server
        environment:
          TZ:              
          MAIN_SERVER:    
          REQUIREMENT:    
          # 1С не любит проброс разноименных портов, используем проброс одноименных портов
          OC_RAGENT_PORT:  1540
          OC_RMNGR_PORT:   1541
          OC_RAS_PORT:     1545
          OC_DEBUG_PORT:   1549
          OC_RPHOST_PORTS: 1560-1591
          OC_DEBUG_TYPE:  
        networks:
          - oc_server_network
        ports:
          - "80:80"
          - "1540:1540"
          - "1541:1541"
          - "1545:1545"
          - "1549:1549"
          - "1560-1591:1560-1591"
        volumes:
          - "oc_srvinfo:/home/usr1cv8/.1cv8/1C/1cv8"
          - "oc_lic:/var/1C/licenses"
          - "oc_log:/var/log/1C"
          - "oc_conf:/opt/1cv8/conf"
        cap_add:
          - SYS_NICE
        tty: true

  10. Скопируем секцию oc-server 2 раза, и поменяем имена новых секций как:

    • oc-server-lic,
    • oc-server-work.
  11. В секции oc-server-lic зададим параметры:

    • hostname: oc-server-node-lic.

      Внимание Важно! В случае docker swarm hostname должен отличаться от наименования секции сервиса.

    • MAIN_SERVER: "PC1".
    • REQUIREMENT: "+LicenseService,-,-DataAcceleratorService".
    • Удалим секцию ports.

    Если в рамках стека описаны разные сервисы с одними портами, то они будут конфликтовать. Подразумевается, что мы будем общаться с основным менеджером, а он будет общаться со вспомогательными сервисамивнутри overlay сети.

    Эти настройки должны применить следующие требования назначения функциональности:

    Требования назначения функциональности

  12. В секции oc-server-work зададим параметры:
    • hostname: oc-server-node-work;
    • MAIN_SERVER: "PC1";
    • REQUIREMENT: "+FulltextSearchService,-";
    • Удалим секцию ports.
  13. Для автоматического создания томов и сетей уберем все строки external: true.
  14. Для секции networks зададим name: "oc_server_postgres_network".
  15. В итоге у нас должен получиться окончательный вариант файла common-compose-1c-server.yml:

    services:

      oc-server:

        image: 192.168.0.101:5000/1c/server:8.3.27.2342
        hostname: PC1
        command: oc=server
        environment:
          TZ:              
          MAIN_SERVER:    
          REQUIREMENT:    
          # 1С не любит проброс разноименных портов, используем проброс одноименных портов
          OC_RAGENT_PORT:  1540
          OC_RMNGR_PORT:   1541
          OC_RAS_PORT:     1545
          OC_DEBUG_PORT:   1549
          OC_RPHOST_PORTS: 1560-1591
          OC_DEBUG_TYPE:  
        networks:
          - oc_server_network
        ports:
          - "80:80"
          - "1540:1540"
          - "1541:1541"
          - "1545:1545"
          - "1549:1549"
          - "1560-1591:1560-1591"
        volumes:
          - "oc_srvinfo:/home/usr1cv8/.1cv8/1C/1cv8"
          - "oc_lic:/var/1C/licenses"
          - "oc_log:/var/log/1C"
          - "oc_conf:/opt/1cv8/conf"
        cap_add:
          - SYS_NICE
        tty: true

      oc-server-lic:

        image: 192.168.0.101:5000/1c/server:8.3.27.2342
        hostname: oc-server-node-lic
        command: oc=server
        environment:
          TZ:              
          MAIN_SERVER: "PC1"    
          REQUIREMENT: "+LicenseService,-,-DataAcceleratorService"    
          # 1С не любит проброс разноименных портов, используем проброс одноименных портов
          OC_RAGENT_PORT:  1540
          OC_RMNGR_PORT:   1541
          OC_RAS_PORT:     1545
          OC_DEBUG_PORT:   1549
          OC_RPHOST_PORTS: 1560-1591
          OC_DEBUG_TYPE:  
        networks:
          - oc_server_network
        volumes:
          - "oc_srvinfo:/home/usr1cv8/.1cv8/1C/1cv8"
          - "oc_lic:/var/1C/licenses"
          - "oc_log:/var/log/1C"
          - "oc_conf:/opt/1cv8/conf"
        cap_add:
          - SYS_NICE
        tty: true

      oc-server-work:

        image: 192.168.0.101:5000/1c/server:8.3.27.2342
        hostname: oc-server-node-work
        command: oc=server
        environment:
          TZ:              
          MAIN_SERVER: "PC1"  
          REQUIREMENT: "+FulltextSearchService,-"
          # 1С не любит проброс разноименных портов, используем проброс одноименных портов
          OC_RAGENT_PORT:  1540
          OC_RMNGR_PORT:   1541
          OC_RAS_PORT:     1545
          OC_DEBUG_PORT:   1549
          OC_RPHOST_PORTS: 1560-1591
          OC_DEBUG_TYPE:  
        networks:
          - oc_server_network
        volumes:
          - "oc_srvinfo:/home/usr1cv8/.1cv8/1C/1cv8"
          - "oc_lic:/var/1C/licenses"
          - "oc_log:/var/log/1C"
          - "oc_conf:/opt/1cv8/conf"
        cap_add:
          - SYS_NICE
        tty: true

    networks:
      oc_server_network:
        name: "oc_server_postgres_network"

    volumes:
      oc_srvinfo:
        name: oc_srvinfo
      oc_lic:
        name: oc_lic
      oc_log:
        name: oc_log
      oc_conf:
        name: oc_conf

  16. Изменим в файле common-compose-postgres.yml следующие пункты:
    • image: 192.168.0.101:5000/1c/postgres:18.4
    • POSTGRES_USER: "postgres"
    • "postgres_data:/var/lib/pgpro/1c-/data"
    • Также уберем все строки external: true
    • И для секции networks задаим name: "oc_server_postgres_network"
  17. В итоге получим следующий файл common-compose-postgres.yml:

    services:

      postgres:

        image: 192.168.0.101:5000/1c/postgres:18.4
        hostname: "postgres"
        command: postgres -c listen_addresses='*'
        environment:
          # https://hub.docker.com/_/postgres
          TZ: ""
          POSTGRES_USER: "postgres"
          POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
        networks:
          - postgres_network
        ports:
          - "5432:5432"
        volumes:
          - "postgres_data:/var/lib/pgpro/1c-18/data"
        secrets:
          - postgres_password

    networks:
      postgres_network:
        name: "oc_server_postgres_network"

    volumes:
      postgres_data:
        name: "postgres_data"

  18. В файле docker-stack-1c-server.yml зададим привязку к узлам для каждого сервиса:

    services:

      oc-server:

        deploy:
          replicas: 1
          restart_policy:
            condition: on-failure
          placement:
            constraints:
              - node.role == manager

      oc-server-lic:

        deploy:
          replicas: 1
          restart_policy:
            condition: on-failure
          placement:
            constraints:
              - node.labels.role == lic

      oc-server-work:

        deploy:
          replicas: 1
          restart_policy:
            condition: on-failure
          placement:
            constraints:
              - node.labels.role == work

  19. Также зададим привязку к менеджеру для postgres в файле docker-stack-postgres.yml:

    services:

      postgres:

        deploy:
          replicas: 1
          restart_policy:
            condition: on-failure
          placement:
            constraints:
              - node.role == manager

    secrets:
      postgres_password:
        external: true

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

Запуск среды

  1. Зададим метки для узлов PC2 и PC3.

    • Для этого узнаем ID узлов через команду:

      sudo docker node ls
      ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS VERSION
      6uyy8du8jgi913m2pkt4ql8xm * PC1 Ready Active Leader 29.8.0
      s5v9paqtrlvs9uh8h1r6xfnla PC2 Ready Active   29.8.0
      w2kbcpg8fcpkdryxkdsjrzb4e PC13 Ready Active   29.8.0
    • После чего зададим метку "role=lic" для PC2:

      sudo docker node update --label-add role=lic s5v9paqtrlvs9uh8h1r6xfnla
    • И зададим метку зададим метку "role=work" для PC3:

      sudo docker node update --label-add role=work w2kbcpg8fcpkdryxkdsjrzb4e
  2. Создадим секрет для пользователя postgres:

    • В папке docker/users/stack/oc-postgres создадим файл postgres.secret куда поместим наш пароль (чтобы не светить пароль в истории командной строки).
    • Выполним команду.

      sudo docker secret create postgres_password docker/users/stack/oc-postgres/postgres.secret
  3. Перейдем в каталог настройки среды и запустим ее:

    cd docker/users/stack/oc-postgres
    sudo bash ./docker-stack.sh
  4. Проверим, что все сервисы запущены:

    sudo docker stack services oc_postgres

    lbrgex40fl5v   oc_postgres_oc-server   replicated   1/1   192.168.0.101:5000/1c/server:8.3.27.2342   *:80->80/tcp, *:1540-1541->1540-1541/tcp, *:1545->1545/tcp, *:1549->1549/tcp, *:1560-1591->1560-1591/tcp

    23ebuw2348fa   oc_postgres_oc-server-lic   replicated   1/1   192.168.0.101:5000/1c/server:8.3.27.2342

    rfgs5ae5wmg8   oc_postgres_oc-server-work  replicated   1/1   192.168.0.101:5000/1c/server:8.3.27.2342

    o8brbp80lscr   oc_postgres_postgres   replicated   1/1   192.168.0.101:5000/1c/postgres:18.4    *:5432->5432/tcp

  5. Если сервис имеет статус replicated 0/1, значит он не запустился, для его анализа необходимо выполнить команду:

    docker service ps --no-trunc oc_postgres_oc-server-lic

    xt5yyvbznuuon07ztxswyttud   oc_postgres_oc-server-lic.1       192.168.0.101:5000/1c/server:8.3.27.2342   test-node-1   Ready           Rejected less than a second ago   "manifest for 192.168.0.101:5000/1c/server:8.3.27.2342 not found: manifest unknown: manifest unknown"

    63tje55ljlgcbz3onzxgjw63f    \_ oc_postgres_oc-server-lic.1   192.168.0.101:5000/1c/server:8.3.27.2342   test-node-1   Shutdown        Rejected 5 seconds ago            "manifest for 192.168.0.101:5000/1c/server:8.3.27.2342 not found: manifest unknown: manifest unknown" 
    В данном случае образ не был помещен в реестр.

  6. Теперь можно соединиться с 1С через консоль и проверить наши настройки.

Требования назначения функциональности

Заключение

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

Вы читаете статью из рубрики:
От экспертов «1С-Рарус»

Читайте первыми статьи от экспертов «1С‑Рарус»

Вы можете получать оповещения по электронной почте

Поле является обязательным

Или получайте уведомления в телеграм-боте

Рассылка «Новости компании»

Узнавайте первыми о новых статьях, мероприятиях и спецпредложениях.

Посмотреть все рассылки «1С‑Рарус»

Заинтересованы в сотрудничестве?
Нужна консультация?
Свяжитесь с нами!