Бессерверная платформа Python Google Cloud: размещение Django в Cloud Run

Бессерверная платформа Python Google Cloud: размещение Django в Cloud Run

16 мая 2022 г.


Без сервера


Что такое без сервера?


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



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


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

  • Автоматически масштабирует эти ресурсы вверх или вниз в ответ на увеличение или уменьшение спроса.

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

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


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


Примеры бессерверных предложений:


  • [AWS Lambda] (https://aws.amazon.com/lambda/)


  • [Рабочие Cloudflare] (https://workers.cloudflare.com/)

  • [Функции Azure] (https://azure.microsoft.com/en-gb/services/functions/)


Преимущества бессерверного доступа


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

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

  • Упрощенный серверный код. С помощью FaaS разработчики могут создавать простые функции, которые независимо выполняют одну задачу, например вызов API.

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

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

Болевые точки бессерверных серверов


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

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

  • Может быть снижена производительность (холодный запуск)- Поскольку бессерверный код не работает постоянно, при его использовании может потребоваться «загрузка». Это время запуска может снизить производительность. Однако, если фрагмент кода используется регулярно, бессерверный провайдер будет держать его готовым к активации — запрос на этот готовый к работе код называется «теплым стартом». Запрос кода, который давно не использовался, называется «холодным стартом».

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

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

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

(Докер) Контейнеры


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


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


Что такое контейнеризация?


Контейнеры — это коробки, у которых нет основной операционной системы, поэтому они не зависят от устройства, на котором они работают.


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


К вашему сведению, вам не нужно быть экспертом по контейнерам, чтобы продуктивно работать с Cloud Run, но если вы им являетесь, Cloud Run не будет вам мешать.


Контейнеризация — это упаковка программного кода только с библиотеками операционной системы (ОС) и зависимостями, необходимыми для запуска кода для создания единого облегченного исполняемого файла, называемого контейнером, который последовательно работает в любой инфраструктуре. Более портативные и ресурсоэффективные, чем виртуальные машины (ВМ), контейнеры стали де-факто вычислительными единицами современных облачных приложений. Контейнеризация позволяет разработчикам создавать и развертывать приложения быстрее и безопаснее.



При использовании традиционных методов код разрабатывается в определенной вычислительной среде, которая при переносе на новое место часто приводит к ошибкам и ошибкам. Например, когда разработчик переносит код из операционной системы Linux в операционную систему Windows. Контейнеризация устраняет эту проблему, объединяя код приложения вместе с соответствующими файлами конфигурации, библиотеками и зависимостями, необходимыми для его запуска. Этот единый пакет программного обеспечения или «контейнер» абстрагируется от основной операционной системы, и, следовательно, он автономен и становится переносимым — способным без проблем работать на любой платформе или в облаке.


Докер



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


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


Быстрые определения:


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


Вы можете создать свой образ на основе инструкций, приведенных в Dockerfile — текстовом файле, который содержит инструкции о том, как будет создан образ Docker.


Когда образ создан, вы можете сохранить его локально или в каком-либо репозитории образов контейнеров, например hub.docker.com или Google Container Registry.


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


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


  • Dockerfile – это рецепт для создания образов Docker.

  • «Образ Docker» создается путем запуска команды Docker (которая использует текстовый файл, называемый «Dockerfile»).

  • «Контейнер Docker» — это работающий экземпляр образа Docker.

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


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

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

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

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

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

Облачный запуск: без сервера + контейнеры


Cloud Run – это вычислительная платформа от Google Cloud Platform, позволяющая запускать HTTP-контейнеры без сохранения состояния, не беспокоясь о подготовке. машины, кластеры или автомасштабирование. С Cloud Run вы переходите от «образа контейнера» к полностью управляемому веб-приложению, работающему на доменном имени с сертификатом TLS, которое автоматически масштабируется с запросами буквально двумя командами. Вы только платите во время обработки запроса. Это позволяет вам передать образ контейнера с веб-сервером внутри и указать некоторую комбинацию ресурсов памяти/ЦП и разрешенный параллелизм.


Обратите внимание, что Cloud Run не требует серверов: он абстрагирует все управление инфраструктурой, поэтому вы можете сосредоточиться на самом важном — создании отличных приложений.



Затем Cloud Run позаботится о создании конечной точки HTTP, получении запросов и их маршрутизации в контейнеры, а также обеспечении работы достаточного количества контейнеров для обработки объема запросов. Пока ваши контейнеры обрабатывают запросы, вы оплачиваете их с шагом в 100 мс.


Типичный вариант использования


Все рабочие нагрузки, работающие в режиме без сохранения состояния, со временем обработки менее 15 минут, могут запускаться одной или несколькими конечными точками HTTP, такими как CRUD REST API. Языки, библиотеки и бинарники значения не имеют, нужен только контейнер. Серверная часть веб-сайта, микропакетная обработка, вывод машинного обучения… возможные варианты использования.


Функции Cloud Run, которые нравятся разработчикам



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


Гибкость: Любой язык, любая библиотека, любой двоичный файл


Другие бессерверные предложения, такие как облачная функция, AWS Lambda или функции Azure, предоставляют ограниченный выбор языков и среды выполнения. Cloud Run обеспечивает гибкость для создания отличных приложений на вашем любимом языке с вашими любимыми зависимостями и инструментами и развертывания их за считанные секунды.


В облаке Google вы даже можете запустить Fortran или Pascal. -d7a16633db44), если хотите. 🍄 Пользователям удалось запустить веб-серверы, написанные на ассемблере x86, или 22-летний Python 1.3 в Cloud Run.


Цены


Cloud Run поставляется с щедрым бесплатным уровнем и платой за использование, что означает, что вы платите только за то, что запрос обрабатывается в вашем экземпляре контейнера. Если он простаивает без трафика, то вы ничего не платите.


Когда уровень бесплатного пользования превышает уровень бесплатного пользования, Cloud Run взимает плату только за те ресурсы, которые вы используете.


https://cdn.hackernoon.com/images/dsNDG3VITEYuADbhxQzydEdwJjo1-2022-05-09T09:12:50.578Z-cl2yia7wy004p0bs63dorfjl0


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


  • Экземпляр контейнера запускается, и

  • По крайней мере один запрос или событие обрабатывается экземпляром контейнера

В течение этого времени Cloud Run выставляет счет только за выделенный ЦП и память с округлением до ближайших 100 миллисекунд. Cloud Run также взимает плату за выход из сети и количество запросов.


По словам Себастьяна Морана, ведущего архитектора решений группы в Veolia и разработчика Cloud Run, это позволяет вам запускать любой контейнер без сохранения состояния с очень детализированной моделью ценообразования:


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


Узнайте больше о ценах на Cloud Run здесь.


Параллелизм > 1


Cloud Run автоматически масштабирует количество экземпляров контейнера, необходимое для обработки всех входящих запросов или событий. Однако, в отличие от других решений «Функции как услуга» (FaaS), таких как Облачные функции, эти экземпляры могут получать более одного запроса или события одновременно. .


Максимальное количество запросов, которые могут быть обработаны одновременно к данному экземпляру контейнера, называется параллелизмом. По умолчанию сервисы Cloud Run имеют максимальное количество параллелизма 80.


https://cdn.hackernoon.com/images/dsNDG3VITEYuADbhxQzydEdwJjo1-2022-05-09T09:12:50.577Z-cl2yia7wy004o0bs6etl0dibb


Использование параллелизма выше 1 имеет несколько преимуществ:


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

  1. Оптимизированное потребление ресурсов и, следовательно, более низкие затраты: если ваш код часто ожидает возврата сетевых операций (например, вызов стороннего API), выделенный ЦП и память могут тем временем использоваться для обработки других запросов.

Узнайте больше о концепции параллелизма здесь.


Возможность легко использовать CDN


Firebase Hosting отлично сочетается с Cloud Run: поместите собственный домен в любую службу Cloud Run в любом регионе, кэшируйте запросы и обслуживайте статические файлы в глобальный CDN бесплатно.


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


При разработке для Cloud Run вам необходимо создать контейнер. Этот контейнер можно развернуть где угодно: локально, в Kubernetes/GKE, в Cloud Run (без сервера и GKE), на виртуальной машине и т. д. где угодно. Кроме того, людям, которые сегодня используют контейнерные веб-серверы без сохранения состояния, будет очень и очень просто получать преимущества без сервера.


Также полезно знать, что Cloud Run основан на [Knative] (https://knative.dev/), проекте с открытым исходным кодом. Если вы когда-либо хотели покинуть управляемую среду Google Cloud, мы могли бы легко развернуть то же приложение и получить аналогичные функции с Knative везде, где работает Kubernetes.


Опыт разработчиков


Cloud Run Предоставляет простую командную строку и пользовательский интерфейс. Он быстро развертывает и управляет вашим сервисом.



Очень просто протестировать локально


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


Отличный мониторинг и ведение журнала


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




Полностью управляемый


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


Жизненный цикл контейнера в Cloud Run


Небольшой обзор того, как работает Cloud Run.


Обслуживание запросов


https://cdn.hackernoon.com/images/dsNDG3VITEYuADbhxQzydEdwJjo1-2022-05-09T09:12:50.575Z-cl2yia7wv004n0bs65fpycktp


Когда контейнер не обрабатывает запросы, он считается бездействующим. На традиционном сервере вы можете не задумываться об этом дважды. Но в Cloud Run это важное состояние:


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

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


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


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


Выключение


https://cdn.hackernoon.com/images/dsNDG3VITEYuADbhxQzydEdwJjo1-2022-05-09T09:12:50.572Z-cl2yia7wt004m0bs6e4uoca9y


Если ваш контейнер простаивает, Cloud Run может остановить его. По умолчанию контейнер просто исчезает при закрытии.


Развертывание



С Cloud Run вы переходите от «образа контейнера» к полностью управляемому веб-приложению, работающему на доменном имени с сертификатом TLS, который автоматически масштабируется с запросами в двух командах.


Образ контейнера — это автономный пакет с вашим приложением и всем, что ему нужно для запуска. Если вы запускаете образ контейнера, он называется контейнером. Cloud Run ожидает, что ваш контейнер будет прослушивать входящие запросы через порт 8080, на котором работает HTTP-сервер. Номер порта 8080 — это порт по умолчанию, который вы можете переопределить при развертывании контейнера.


Когда вы впервые развертываете образ контейнера в Cloud Run, он создает для вас службу. Служба автоматически получает уникальную конечную точку HTTPS (подробнее об этом позже).


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


Предпосылки:


  1. Создайте проект GCP и свяжите биллинг.

  1. Установите gcloud CLI.

  1. Включите службы проекта GCP.




Вы можете включить их через CLI:


Службы gcloud включают containerregistry.googleapis.com cloudbuild.googleapis.com run.googleapis.com


Шаг 1. Подготовьте Django


Часть 1: Добавьте файл requirements.txt (в большинстве проектов Python он уже есть), затем добавьте все ваши зависимости плюс gunicorn (веб-сервер, который мы будем использовать).


Пример:


требования.txt


Джанго==4.0.4


оружейный рог == 20.1.0


Часть 2: Отредактируйте ALLOWED_HOSTS в настройках Django, чтобы разрешить все хосты (не подходит для производственной среды и позже следует изменить на предпочитаемый хост(ы)):


settings.py


настройки.py


ALLOWED_HOSTS = ['*']


Шаг 2. Добавьте Dockerfile


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


Докерфайл


вытащить официальное базовое изображение


ОТ питона: 3.10


установить рабочий каталог


РАБОЧИЙКАТАЛОГ /приложение


Скопируйте локальный код в образ контейнера.


КОПИРОВАТЬ . .


Установить зависимости.


ЗАПУСК pip install --no-cache-dir -r requirements.txt


Запуск веб-службы при запуске контейнера. Здесь мы используем веб-сервер gunicorn с одним рабочим процессом и 8 потоками.


CMD exec gunicorn --bind 0.0.0.0:$PORT --workers 1 --threads 8 mysite.wsgi:application


Шаг 3. Сборка в облаке Сборка + развертывание в облаке


Не забудьте заменить $GC_PROJECT на ваш проект GCP.


Часть 1: Образ сборки (с помощью Cloud Build):


Создайте образ и сохраните его в реестре контейнеров Google. eu.gcr.io/$GC_PROJECT/my-dj-app — образ для сборки. Образ должен быть в формате реестр/проект/имя_изображения.


gcloud builds submit --tag eu.gcr.io/$GC_PROJECT/my-dj-app


Часть 2. Развертывание в Cloud Run


Разверните в Image Cloud Run. Не забудьте скопировать «URL-адрес службы» в выходные данные, которые будут использоваться на следующем шаге (Шаг 4).


gcloud запустите развертывание моего сервиса \


--image eu.gcr.io/$GC_PROJECT/my-dj-app \


--проект $GC_PROJECT \


--регион "европе-запад1" \


--allow-unauthenticated


Флаги:


  • --image: Образ для развертывания. Это тот же образ, который мы создали в Часть 1:.

  • --project: ваш проект Google Cloud.

  • --region: физический регион, в котором будет развернута служба.

  • --allow-unauthenticated: гарантирует, что вы можете получить доступ к URL-адресу службы без передачи заголовка аутентификации.

Вы также можете создать и запустить контейнер локально (необязательно):


Сделайте это только для того, чтобы проверить, работает ли это. Чтобы создать образ Docker из файла Dockerfile, который мы создали выше, выполните следующие команды:


Создаем образ


docker build -t my-dj-app .


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


docker run -e ПОРТ=8080 -p 8080:8080 -d --name мой-контейнер мое-dj-приложение


Сделать запрос


завиток локальный: 8080


Проверить логи


докер регистрирует мой-контейнер


Остановить контейнер


докер остановить мой-контейнер


Вы также можете выполнять сборку локально + развертывание в облаке (необязательно):


Сделайте это только для того, чтобы проверить, работает ли это. Замените $GC_PROJECT своим проектом GCP.


Собираем образ Docker


docker build -t eu.gcr.io/$GC_PROJECT/my-dj-app .


Отправка образа Docker в реестр контейнеров Google


docker push eu.gcr.io/$GC_PROJECT/my-dj-app


Развернуть образ из GCR в Cloud Run


gcloud запустите развертывание моего сервиса \


--image eu.gcr.io/$GC_PROJECT/my-dj-app \


--проект $GC_PROJECT \


--регион "европе-запад1" \


--платформа управляемая \


--allow-unauthenticated


Шаг 4. Посетите веб-приложение Cloud Run


Посетите «URL-адрес службы», который вы получили на предыдущем шаге, чтобы просмотреть свою службу. Вот мой:



Готово!


Вывод


Спасибо, что дошли до конца руководства!


Надеюсь, вы чему-то научились.


Источники и дополнительная литература
















Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE