Это пример взаимодействия между несколькими контейнерами Docker. Используется наиболее распространённая стек технологий LNMP, то есть Nginx
+ PHP
+ MySQL
.
В этом примере я использую Docker Compose, что делает его более компактным. Конечно, можно достичь того же результата с помощью команд docker
, но процесс будет сложнее.
В файле docker-compose.yml
определены три сервиса: nginx
, php
и mysql
.
services:
nginx:
image: "${DOCKER_USER}/lnmp-nginx:v1.2"
build:
context: .
dockerfile: Dockerfile.nginx
...
php:
image: "${DOCKER_USER}/lnmp-php:v1.2"
build:
context: .
dockerfile: Dockerfile.php
...
mysql:
image: mysql:5.7
...
В сервисе mysql
параметр image: mysql:5.7
указывает на использование образа mysql:5.7
. В то же время, параметр image
для сервисов nginx
и php
имеет более сложное значение. С одной стороны, это указывает на использование образа с указанным именем, а с другой — если такой образ отсутствует, то он будет построен с помощью команд, указанных в параметре build
. В одиночной среде этот параметр image
не обязателен, достаточно указать только build
. Однако в среде Swarm требуется, чтобы все узлы использовали один и тот же образ, поэтому самостоятельное построение образа на каждом узле не подходит. Указание параметра image
позволяет построить и отправить образ на registry, а затем автоматически скачать его при выполнении команды up
в Swarm-среде.Имена образов также выглядят немного по-другому:
image: "${DOCKER_USER}/lnmp-nginx:v1.2"
Здесь ${DOCKER_USER}
— это переменная окружения, которая будет заменена значением переменной окружения DOCKER_USER
, если таковая существует. А откуда берутся эти переменные окружения? Помимо экспорта их в Shell, Docker Compose поддерживает файл с переменными окружения по умолчанию, то есть файл .env
. Вы можете заметить, что в той же директории, где находится docker-compose.yml
, есть файл .env
, который определяет переменные окружения.
DOCKER_USER=twang2218
Каждый раз, когда выполняется команда docker-compose
, этот файл .env
автоматически загружается, что делает его удобным местом для настройки файла compose. Здесь я определил только одну переменную окружения DOCKER_USER
, но вы можете добавить больше переменных окружения, каждую на новой строке. В дополнение к этому, можно явно указать файлы переменных окружения. Для более подробных настроек обратитесь к официальной документации по docker-compose
.
Для сервиса mysql
используются официальные изображения Docker. Использование официальных изображений не означает, что они не могут быть настроены; обычно у официальных изображений Docker есть определенные возможности для кастомизации.```yml
mysql:
image: mysql:5.7
...
environment:
TZ: 'Asia/Shanghai'
MYSQL_ROOT_PASSWORD: Пароль1
command: ['mysqld', '--character-set-server=utf8']
...
В этом примере для сервиса `mysql` задается начальное пароль для базы данных MySQL через переменную окружения `MYSQL_ROOT_PASSWORD` со значением `Пароль1`, а также задается локальное время через переменную окружения `TZ`.
Кроме того, я переопределяю команду запуска контейнера, добавляя дополнительные параметры в `command`. Параметр `--character-set-server=utf8` задает начальный набор символов.
### Изображение для сервиса nginx
Официальное изображение для сервиса `nginx` удовлетворяет большинство требований, но для него необходимо добавить файл конфигурации по умолчанию и директорию с веб-страницами.
```Dockerfile
FROM nginx:1.11
ENV TZ=Asia/Shanghai
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
COPY ./site /usr/share/nginx/html
Кастомизация изображения проста: задается локальное время, а затем копируются файлы конфигурации и директория с веб-страницами в соответствующие папки.
Изображение для сервиса php
является специальным, так как официальное изображение php
не содержит плагинов для подключения к mysql
. Поэтому для сервиса php
необходимо использовать официальное изображение как основу и установить необходимые плагины.
Соответствующий файл Dockerfile.php
:
FROM php:7-fpm
ENV TZ=Asia/Shanghai
COPY sources.list /etc/apt/sources.list
```RUN set -xe \
&& echo "Сборка зависимостей" \
&& buildDeps=" \
build-essential \
php5-dev \
libfreetype6-dev \
libjpeg62-turbo-dev \
libmcrypt-dev \
libpng12-dev \
" \
&& echo "Зависимости для выполнения" \
&& runtimeDeps=" \
libfreetype6 \
libjpeg62-turbo \
libmcrypt4 \
libpng12-0 \
" \
&& echo "Установка PHP и компонентов для компиляции и сборки" \
&& apt-get update \
&& apt-get install -y ${runtimeDeps} ${buildDeps} --no-install-recommends \
&& echo "Компиляция и установка компонентов PHP" \
&& docker-php-ext-install iconv mcrypt mysqli pdo pdo_mysql zip \
&& docker-php-ext-configure gd \
--with-freetype-dir=/usr/include/ \
--with-jpeg-dir=/usr/include/ \
&& docker-php-ext-install gd \
&& echo "Очистка" \
&& apt-get purge -y --auto-remove \
-o APT::AutoRemove::RecommendsImportant=false \
-o APT::AutoRemove::SuggestsImportant=false \
$buildDeps \
&& rm -rf /var/cache/apt/* \
&& rm -rf /var/lib/apt/lists/* \
COPY ./php.conf /usr/local/etc/php/conf.d/php.conf
COPY ./site /usr/share/nginx/html
```Новички Docker часто ошибочно считают, что `Dockerfile` эквивалентен shell-скриптам и используют множество команд `RUN`, каждая из которых соответствует одному команде. Это неправильное использование, что приводит к чрезмерно объемным конечным образом. `Dockerfile` — это файл для настройки образа, где каждая команда определяет, как должна измениться эта слой. Поэтому следует [следовать лучшим практикам](https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/), объединяя одинаковые элементы в один слой и очищая все ненужные файлы в конце.Цель этого слоя — установка и сборка PHP-плагинов. Таким образом, действительно необходимы только собранные плагины и зависимости, необходимые для их работы. Любые дополнительные файлы не должны присутствовать. В этом блоке зависимости разделены на "построительные зависимости" и "зависимости для выполнения". После установки ненужные "построительные зависимости" удаляются, чтобы избежать ненужных файлов, оставленных после сборки.
Здесь используются официальные скрипты `docker-php-ext-install` и `docker-php-ext-configure` из официального образа `php` для установки плагинов PHP и настройки параметров сборки соответственно. Эти скрипты предоставлены официальными образами для помощи в настройке образов, и многие официальные образы имеют подобные скрипты или программы для помощи в настройке.
Дополнительная информация о том, как настраивать образы, может быть найдена в документации официальных образов на Docker Hub: <https://hub.docker.com/_/php/>
В процессе очистки в конце можно увидеть, что, помимо удаления "построительных зависимостей" и ненужных программ, полностью очищается кэш `apt`. Любые ненужные элементы должны быть удалены, чтобы убедиться, что слой содержит только необходимые файлы после завершения сборки.
В конце `Dockerfile` копируются конфигурационные файлы и директория веб-сайта на соответствующие позиции.## СетьВ этом примере показано, как использовать пользовательскую сеть и взаимодействовать с сервисами по их именам.
Во-первых, в конце файла `docker-compose.yml` в глобальной секции `networks` определены два пользовательских сетевых подключения, названных `frontend` и `backend`.
```yml
networks:
frontend:
backend:
```
Каждое пользовательское сеть-подключение может быть настроено на множество параметров, включая используемый драйвер сети и диапазон адресов. Однако, вы можете заметить, что `frontend` и `backend` определены без дополнительных параметров, что означает использование значений по умолчанию. В одиночном контейнере это будет означать использование драйвера `bridge`, а в контексте Swarm — драйвера `overlay`, и диапазон адресов будет определен Docker.
Затем, в секции `services`, каждая служба имеет свою секцию `networks`, которая используется для определения сетей, к которым подключена служба.
```yml
services:
nginx:
...
networks:
- frontend
php:
...
networks:
- frontend
- backend
mysql:
...
networks:
- backend
```
В этом примере:
* `nginx` подключен к сети `frontend`;
* `mysql` подключен к сети `backend`;
* `php`, как промежуточная служба, подключен к обоим сетям `frontend` и `backend`.
Контейнеры, подключенные к одной и той же сети, могут взаимодействовать друг с другом, в то время как контейнеры, подключенные к разным сетям, будут изолированы.Таким образом, в этом примере `nginx` может взаимодействовать с `php`, а `php` — с `mysql`, так как они подключены к одной сети. `nginx` и `mysql` не подключены к одной сети, поэтому они не могут взаимодействовать друг с другом, что обеспечивает изоляцию.
Контейнеры, подключенные к одной сети, могут использовать **имена сервисов** для доступа к другим сервисам. Например, в этом примере в файле `./site/index.php` используется имя сервиса `mysql` для подключения к серверу базы данных.
```php
<?php
// Установка соединения
$conn = mysqli_connect("mysql", "root", $_ENV["MYSQL_PASSWORD"]);
...
?>
```
В этом коде подключения к базе данных можно заметить, что пароль для базы данных считывается из переменной окружения `$_ENV["MYSQL_PASSWORD"]`. Это означает, что пароль не хранится в коде, а передается в контейнер через переменную окружения. В этом примере переменная окружения задается в файле `docker-compose.yml`.```yml
version: '2'
services:
...
php:
...
environment:
MYSQL_PASSWORD: Passw0rd
...
```
Чтобы узнать больше о пользовательских сетях Docker, можно обратиться к официальной документации:
<https://docs.docker.com/engine/userguide/networking/dockernetworks/#/user-defined-networks>
Чтобы узнать больше о использовании пользовательских сетей в Docker Compose, можно посмотреть соответствующую часть документации:
<https://docs.docker.com/compose/networking/>
## ХранилищеВ этих трех службах `nginx` и `php` являются безсостоятельными службами, которые не требуют локального хранилища. Однако `mysql` является базой данных, которая требует хранения динамических данных. Мы знаем, что Docker требует, чтобы слои хранения контейнеров не содержали состояния, а все состояние (то есть динамические данные) должно быть сохранено с помощью томов. В данном случае используется именованный том для сохранения данных.```yaml
volumes:
mysql-data:
```
В конфигурационном файле `docker-compose.yml` есть глобальная секция `volumes`, которая используется для определения именованных томов. Здесь мы определяем именованный том `mysql-data`. В определении тома можно указать дополнительные параметры, такие как драйвер тома и некоторые его настройки. В данном случае все параметры используются по умолчанию, то есть используется простой локальный драйвер тома `local`, и созданный именованный том может находиться в `/var/lib/docker/volumes`, но прямой доступ к содержимому этого пути не требуется и не рекомендуется.
В секции конфигурации службы `mysql` также есть секция `volumes`, которая определяет тома, которые должны быть смонтированы в контейнере или привязаны к каталогам на хосте. В данном случае используется ранее определенный именованный том `mysql-data`, который монтируется в `/var/lib/mysql` контейнера.
```yaml
mysql:
image: mysql:5.7
volumes:
- mysql-data:/var/lib/mysql
...
```
## Зависимости
Порядок запуска служб иногда имеет значение, и Compose позволяет контролировать этот процесс. В данном примере используется зависимость `depends_on` для определения порядка запуска.
```yaml
services:
nginx:
...
depends_on:
- php
php:
...
depends_on:
- mysql
mysql:
...
```
Здесь `nginx` зависит от службы `php`, поэтому в конфигурации зависимости указана `php`. А `php` служба зависит от `mysql`, поэтому она указывает зависимость на `mysql`.
При запуске `docker-compose up -d` Compose запускает службы в соответствии с указанными зависимостями. В данном примере службы запускаются в порядке `mysql` → `php` → `nginx`.
```Необходимо учитывать, что контроль последовательности запуска сервисов здесь ограничен, и не означает, что каждый последующий сервис будет запущен только после полной готовности всех зависимых сервисов. Вместо этого, после запуска контейнера начинается запуск следующего сервиса. Поэтому, такой контроль последовательности может привести к ситуации, когда сервис запускается, но необходимые для него сервисы еще не готовы. Например, после запуска `php`, может возникнуть ситуация, когда база данных `mysql` еще не была полностью инициализирована. Для некоторых приложений, это может привести к ошибкам, связанным с невозможностью подключения к необходимым сервисам.
Если требуется контроль зависимостей на уровне приложений, необходимо добавить в скрипты запуска, такие как `entrypoint.sh`, проверку готовности сервисов. Также можно использовать настройку `restart: always`, чтобы приложение имело возможность повторной попытки запуска, если оно было вынуждено остановиться из-за не готовых зависимостей.
# Операции на одном узле
## Запуск
```bash
docker-compose up -d
```*Если во время сборки обнаруживается, что загрузка образов происходит очень медленно или вообще не удается, это может быть связано с "Большой стеной". Вам потребуется настроить ускоритель, подробнее можно почитать в моей статье [Docker FAQ](http://blog.lab99.org/post/docker-2016-07-14-faq.html#docker-pull-cha-man-a-zen-me-ban).*Если вы изменили файл конфигурации, возможно, потребуется явное пересоздание образа, что можно сделать с помощью команды `docker-compose build`.
## Просмотр состояния сервисов
```bash
docker-compose ps
```
## Просмотр логов сервисов
```bash
docker-compose logs
```
## Доступ к сервисам
`nginx` будет слушать порт `80`,
* Если вы используете Linux или `Docker for Mac`, вы можете напрямую обращаться к <http://localhost>
* Если вы используете `Docker Toolbox`, вам следует использовать адрес виртуальной машины, например <http://192.168.99.100>, для получения конкретного адреса виртуальной машины используйте команду `docker-machine ip default`.
* Если вы используете собственную установку Ubuntu, CentOS или подобного, вы можете напрямую получить адрес виртуальной машины.
Если после обращения вы видите сообщение "Успешное подключение к серверу MySQL", это означает, что подключение к базе данных работает корректно.
## Остановка сервисов
```bash
docker-compose down
```
# Управление кластером SwarmВ одиночном узле часто используются монтированные директории хоста, что очень удобно для разработки. Однако, при развертывании приложений в кластере, монтирование директорий хоста становится неудобным. В кластерной среде Swarm может распределить контейнеры для выполнения на любой из узлов. Если один из узлов выйдет из строя, контейнеры могут быть перенаправлены на другие узлы, чтобы обеспечить непрерывность обслуживания. В этом случае, если использовать привязку к каталогу узла, необходимо будет предварительно подготовить содержимое в соответствующих местах на всех узлах и поддерживать его синхронизированным. Это не является оптимальным решением. Поэтому для удобства развертывания в кластерной среде лучше всего поместить код приложения, конфигурационные файлы и т.д. непосредственно в образ. Как в этом примере мы видим образы для `nginx` и `php` сервисов, в процессе кастомизации `Dockerfile` конфигурационные файлы и код приложения добавляются в образ.`Dockerfile` для образа `nginx` сервиса
```Dockerfile
...
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
COPY ./site /usr/share/nginx/html
```
`Dockerfile` для образа `php` сервиса
```Dockerfile
...
COPY ./php.conf /usr/local/etc/php/conf.d/php.conf
COPY ./site /usr/share/nginx/html
```
Docker Swarm в настоящее время имеет две версии. Первая версия запускается в виде контейнера и называется Docker Swarm; вторая версия, начиная с `1.12`, интегрирована в `docker` на основе `SwarmKit` и называется Docker Swarm Mode.
## Первая версия Swarm
[Первая версия Swarm](https://docs.docker.com/swarm/) является первым попыткой Docker команды по организации кластера, запускается в виде контейнера, требует внешнего ключевого хранилища (например, etcd, consul, zookeeper), а также ручной настройки `overlay` сети. Её конфигурация проще, чем у `kubernetes`, но всё же сложнее, чем у второй версии.
В этом разделе представлен скрипт `run1.sh`, который используется для создания первой версии Swarm, запуска сервисов и горизонтального масштабирования.
### Создание кластера Swarm
На виртуальной машине с установленным `docker-machine` и VirtualBox (например, на Mac/Windows с Docker Toolbox), можно использовать скрипт `run1.sh` для создания кластера:
```bash
./run1.sh create
```
### Запуск
```bash
./run1.sh up
```
### Горизонтальное масштабирование
```bash
./run1.sh scale 3 5
```
Здесь первый параметр указывает количество контейнеров `nginx`, а второй параметр — количество контейнеров `php`.
### Доступ к сервисам`nginx` будет слушать порт 80. Используйте `docker ps`, чтобы увидеть, на каком узле кластера запущен `nginx` и какой у него IP-адрес.
```bash
$ eval $(./run1.sh env)
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d85a2c26dd7d twang2218/lnmp-php:v1.2 "php-fpm" 9 минут назад Up 9 минут назад 9000/tcp node1/dockerlnmp_php_5
c81e169c164d twang2218/lnmp-php:v1.2 "php-fpm" 9 минут назад Up 9 минут назад 9000/tcp node1/dockerlnmp_php_2
b43de77c9340 twang2218/lnmp-php:v1.2 "php-fpm" 9 минут назад Up 9 минут назад 9000/tcp master/dockerlnmp_php_4
fdcb718b6183 twang2218/lnmp-php:v1.2 "php-fpm" 9 минут назад Up 9 минут назад 9000/tcp node3/dockerlnmp_php_3
764b10b17dc4 twang2218/lnmp-nginx:v1.2 "nginx -g 'daemon off'" 9 минут назад Up 9 минут назад 192.168.99.104:80->80/tcp, 443/tcp master/dockerlnmp_nginx_3
e92b34f998bf twang2218/lnmp-nginx:v1.2 "nginx -g 'daemon off'" 9 минут назад Up 9 минут назад 192.168.99.106:80->80/tcp, 443/tcp node2/dockerlnmp_nginx_2
077ee73c8148 twang2218/lnmp-nginx:v1.2 "nginx -g 'daemon off'" 22 минут назад Up 22 минут назад 192.168.99.105:80->80/tcp, 443/tcp node3/dockerlnmp_nginx_1
1931249a66c1 e8920543aee8 "php-fpm" 22 минут назад Up 22 минут назад 9000/tcp node2/dockerlnmp_php_1
cf71bca309dd mysql:5.7 "docker-entrypoint.sh" 22 минут назад Up 22 минут назад 3306/tcp node1/dockerlnmp_mysql_1
```
В такой ситуации можно использовать <http://192.168.99.104>, <http://192.168.99.105>, <http://192.168.99.106> для доступа к сервису.### Остановка сервиса
```bash
./run1.sh down
```
### Уничтожение кластера
```bash
./run1.sh remove
```
## Второе поколение Swarm (Swarm Mode)
[Второе поколение Swarm](https://docs.docker.com/engine/swarm/) (Docker Swarm Mode) — это нативная система управления кластерами Docker, введенная с версии 1.12. Она значительно улучшила архитектуру, устраняя проблемы первого поколения Swarm, и существенно упростила процесс создания кластеров. Встроенный распределенный базовый слой данных позволяет избежать необходимости настройки внешнего ключевого хранилища; встроенное ядро-уровневое балансирование нагрузки; встроенное балансирование нагрузки на границе.
Так же как и в примере с первым поколением Swarm, для удобства объяснения здесь предоставлен скрипт `run2.sh` для создания кластера и запуска сервиса.
### Создание Swarm кластера
На виртуальной машине с установленным `docker-machine` и VirtualBox (например, на Mac/Windows с Docker Toolbox), можно использовать скрипт `run2.sh` для создания кластера:
```bash
./run2.sh create
```
*Если вы используете облачные сервисы, такие как Digital Ocean или AWS, вам не потребуется использовать VirtualBox локально, но вам потребуется предварительно настроить соответствующие переменные окружения для `docker-machine`.*
### Запуск
```bash
./run2.sh up
```
### Горизонтальное масштабирование
```bash
./run2.sh scale 10 5
```
Первый параметр указывает количество контейнеров nginx, второй — количество контейнеров php.### Просмотр состояния сервисов
Можно использовать стандартные команды для просмотра всех сервисов и их состояний:
```bash
$ docker service ls
ID NAME REPLICAS IMAGE COMMAND
2lnqjas6rov4 mysql 1/1 mysql:5.7 mysqld --character-set-server=utf8
ahqktnscjlkl php 5/5 twang2218/lnmp-php:v1.2
bhoodda99ebt nginx 10/10 twang2218/lnmp-nginx:v1.2
```Также можно использовать следующую команду для просмотра состояния каждого контейнера для каждого сервиса.
```bash
$ . /run2.sh ps
+ docker service ps -f desired-state=running nginx
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR
87xr5oa577hl9amelznpy7s7z nginx.1 twang2218/lnmp-nginx:v1.2 node2 Running Running 3 часа назад
7dwmc22qaftz0xrvijij9dnuw nginx.2 twang2218/lnmp-nginx:v1.2 node3 Running Running 22 минуты назад
00rus0xed3y851pcwkbybop80 nginx.3 twang2218/lnmp-nginx:v1.2 manager Running Running 22 минуты назад
5ypct2dnfu6ducnokdlk82dne nginx.4 twang2218/lnmp-nginx:v1.2 manager Running Running 22 минуты назад
7qshykjq8cqju0zt6yb9dkktq nginx.5 twang2218/lnmp-nginx:v1.2 node2 Running Running 22 минуты назад
e2cux4vj2femrb3wc33cvm70n nginx.6 twang2218/lnmp-nginx:v1.2 node1 Running Running 22 минуты назад
9uwbn5tm49k7vxesucym4plct nginx.7 twang2218/lnmp-nginx:v1.2 node1 Running Running 22 минуты назад
6d8v5asrqwnz03hvm2jh96rq3 nginx.8 twang2218/lnmp-nginx:v1.2 node1 Running Running 22 минуты назад
eh44qdsiv7wq8jbwh2sr30ada nginx.9 twang2218/lnmp-nginx:v1.2 node3 Running Running 22 минуты назад
51l7nirwtv4gxnzbhkx6juvko nginx.10 twang2218/lnmp-nginx:v1.2 node2 Running Running 22 минуты назад
+ docker service ps -f desired-state=running php
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR
4o3pqdva92vjdbfygdn0agp32 php.1 twang2218/lnmp-php:v1.2 manager Running Running 3 часа назад
bf3d6g4rr8cax4wucu9lixgmh php.2 twang2218/lnmp-php:v1.2 node3 Running Running 22 минуты назад
9xq9ozbpea7evllttvyxk7qtf php.3 twang2218/lnmp-php:v1.2 manager Running Running 22 минуты назад
8umths3p8rqib0max6b6wiszv php.4 twang2218/lnmp-php:v1.2 node2 Running Running 22 минуты назад
0fxe0i1n2sp9nlvfgu4xlc0fx php.5 twang2218/lnmp-php:v1.2 node1 Running Running 22 минуты назад
+ docker service ps -f desired-state=running mysql
```ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR
3ozjwfgwfcq89mu7tqzi1hqeu mysql.1 mysql:5.7 node3 Running Running 3 часа назад
```### Доступ к сервису````nginx` будет слушать порт 80. В связи с тем, что второе поколение Swarm имеет сетевое балансирование нагрузки (Routing Mesh, Ingress Load balance), все узлы в кластере будут слушать порт 80, независимо от того, являются ли они Manager или Worker, а также независимо от того, запущен ли контейнер `nginx` на этих узлах. Когда какой-либо узел получает запрос на порт 80, он автоматически перенаправляет запрос на соответствующий контейнер с использованием overlay сети. Поэтому, доступ к порту 80 любого узла должен показывать сервис.
С помощью следующей команды можно вывести список всех узлов, и доступ к любому из этих адресов должен показывать страницу приложения:
```bash
$ ./run2.sh nodes
manager http://192.168.99.101
node1 http://192.168.99.103
node2 http://192.168.99.102
node3 http://192.168.99.104
```
### Остановка сервиса
```bash
./run2.sh down
```
### Уничтожение кластера
```bash
./run2.sh remove
```
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Комментарии ( 0 )