Глава 5. За рамками прикладного приложения: добавляем Redis

Хорошо, признайтесь. Вы посмотрели на заголовок этой главы и подумали: "Redis ?! А как насчёт настройки базы данных?!" Раз это так, я обещаю, что я не сошёл с ума: есть очень веская причина для того чтобы сначала заняться Redis, как вы вскоре узнаете.

Прежде всего, давайте рассмотрим чего мы добились. Мы уже изучили как:

  • Применять Docker для генерации свежего роекта Rails не имея установленного Rails

  • Запускать сервер Ruby для исполнения нашего приложения

  • Обеспечивать факт установки наших gems и современности

  • Создавать лично наш образ Docker подогнанный под исполнение нашего прикладного приложения

  • Применять Docker Compose для управления всем нашим процессом

Всё это неплохо для начала, но в данный момент недостаточно для построения чего- то отличающегося от наиболее базовых вебсайтов или приложений. Мы упустили некий ключевой момент в нашей мозаике: как подключать наше приложение Rails к внешним службам таким как ... некая база данных. В этой главе вы не изучите в точности как это делать, ачав с Redis (если вам не не известен Redis, это некое хранимое в оперативной памяти хранилище ключ- значение, применяемое при обмене сообщениями pub/sub, в очередях, при кэшировании и так далее - все крутые парни применяют его, а после этой главы и вы тоже).

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

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

[Замечание]Реквизит для Аанда

Данное применяемое в этой главе демонстрационное приложение было вдохновлено демонстрацией Аанда Прасада, которая показала как подключать базовое приложение Python Flask к Redis с помощью Compose.

Аанд является создателем Fig - предшественника Docker Compose - и в недавнем прошлом сотрудника Docker.

Запуск сервера Redis

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

В конечном счёте мы желаем добавить Redis как некую новую службу с помощью Compose. Однако так как это самое первое что нам приходится добавлять, мы собираемся выполнить первые детские шаги. Мы начнём с рассмотрения того как запускать Redis в неком контейнере при помощи docker run прежде чем мы вернём цикл обратно с тем чтобы Compose делал это автоматически. По мере того как вы приобретёте дополнительный опыт и уверенность вы окажетесь в состоянии пропускать этот первый шаг и перепрыгивать сразу к настройке службы в Compose.

  Применение docker run

Для запуска сервера Redis при помощи docker run мы вызываем такую команду:


​​ 	​$ ​​docker​​ ​​run​​ ​​--name​​ ​​redis-container​​ ​​redis​
		

Эта команда в целом нам знакома: она просит Docker запустить некий контейнер на основе официального образа Docker redis. Тем не менее, здесь имеется пара параметров, которые нам ранее не встречались.

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

Теперь остановим свой сервер Redis при помощи Ctrl - C.

Окончательной нашей целью является файл docker-compose.yml для полного описания нашего приложения, включая все его зависимости. Посмотрев на запуск некого сервера Redis при помощи docker run мы готовы настроить Compose на управлением Redis под нас.

Давайте просмотрим свой файл docker-compose.yml:


​ 	version: ​'​​3'​
​ 	
​ 	services:
​ 	
​ 	  web:
​ 	    build: ​.​
​ 	    ports:
​ 	      - ​"​​3000:3000"​
​ 	    volumes:
​ 	      - ​.:/usr/src/app​
 	   

Давайте изменим его чтобы он содержал новую службу, которая вызывает redis:


​​ 	version: ​'​​3'​
​ 	
​ 	services:
​ 	
​ 	  web:
​ 	    build: ​.​
​ 	    ports:
​ 	      - ​"​​3000:3000"​
​ 	    volumes:
​ 	      - ​.:/usr/src/app​
​ 	
»	  redis:
»	    image: ​redis​
 	   

Данное определение для нашей новой службы redis совершенно отлично от нашей службы web. Для начала, оно намного проще; оно имеет всего лишь единственное свойство с названием image.

при определении некой службы у нас имеется два способа определения образа, который будет применён для создания контейнеров. Наша служба web применяет свойство build для указания Compose на необходимость сборки нашего персонального образа из некого Dockerfile. Однако для применения вместо этого предварительно имеющегося образа мы можем определить соответствующее название образа свойством image. Здесь мы определяем свой образ redis в точности как мы это делали в своей команде docker run.

Помимо этого основное отличие заключается в том, что мы не делаем определения.

Мы не публикуем никакие порты. Нашей службе web требуется публикация порта с тем чтобы те запросы, которые делаются в нашей локальной машине достигали имеющийся сервер Rails, запущенный внутри контейнера. Redis, однако, не нуждается в доступе извне; на самом деле, с целью безопасности, мы предпочитаем чтобы его не было вовсе. Не выставляя некий порт он скрыт и не имеет доступа из внешнего мира.

Мы также не определяем никаких томов подлежащих монтированию. Наша служба web применяет тома для монтирования нашего локального каталога, содержащего код нашего проекта Rails внутри своего контейнера. Мы делаем это для того, чтобы мы были способны изменять эти файлы локально, причём все изменения автоматически также автоматически подхватываются внутри нашего контейнера. Для Redis нам не требуется такое поведение - мы не изменяем никакие файлы.

Теперь давайте запустим свой сервер Redis:


​ 	​$ ​​docker-compose​​ ​​up​​ ​​-d​​ ​​redis​
		

Мы можем видеть запуск Redis просмотрев регистрационные записи:


​ 	​$ ​​docker-compose​​ ​​logs​​ ​​redis​
​ 	Attaching to myapp_redis_1
​ 	redis_1  | 1:C 15 Jan 2019 10:03:52.794 ​# oO0OoO0OoO0Oo Redis is starting oO​0OoO0OoO0Oo
​ 	redis_1  | 1:C 15 Jan 2019 10:03:52.794 ​# Redis version=5.0.3, bits=64,​ commit=00000000, modified=0, pid=1, just started
​ 	...
​ 	redis_1  | 1:M 15 Jan 2019 10:03:52.796 * Running mode=standalone, port=6379
​ 	...
​ 	redis_1  | 1:M 15 Jan 2019 10:03:52.796 ​# Server initialized​
​ 	...
​ 	redis_1  | 1:M 15 Jan 2019 10:03:52.796 * Ready to accept connections
		

Отлично! Мы успешно настроили Redis в качестве некой новой службы для своего приложения.

Подключение к серверу Redis вручную

Мы только что запустили Redis при помощи Compose и увидели из его вывода что он исполняется. Однако поскольку мы всё ещё знакомимся с Docker, давайте вручную подключимся к нашему серверу Redis и вступим с ним во взаимодействие чтобы проверить для себя что он на самом деле работает.

Некий быстрый способ для этого состоит в применении интерфейса командной строки Redis (redis-cli). Мы можем воспользоваться тем же самым образом redis который уже обладает установленным redis-cli. Что удобно.

Вместо того чтобы настраивать некую новую обособленную службу в Compose мы можем подхватить прицепом свою уже имеющуюся службу redis, так как она использует нужный нам образ redis. Применив тот приём, который мы изучили в Исполнение одноразовых команд, мы можем исполнить redis-cli и подключиться к своему серверу Redis при помощи такой команды:


​ 	​$ ​​docker-compose​​ ​​run​​ ​​--rm​​ ​​redis​​ ​​redis-cli​​ ​​-h​​ ​​redis​
		

Эта команда говорит: "В неком одноразовом контейнере (--rm) для нашей службы redis выполнить команду redis-cli -h redis". Выполнив её вы должны обнаружить стандартное приглашение Redis отображающее название самого хоста и порт исполнения:


​ 	​redis:6379> 
		

Не стесняйтесь потренироваться здесь. Например, попробуйте запустить команду ping, которая должна выдать вам соответствующий отклик "PONG". Когда вы закончите, выполните выход при помощи команды quit - это завершит ваш клиент Redis и, как результат и сам контейнер.{Прим. пер.: подробнее с работой redis-cli можете ознакомиться в нашем переводе Книги рецептов Redis 4.x Пеньчень Хуань, Зуофей Вань}

Итак, вы его получили. Наш сервер Redis поднят и исполняется и мы можем подключаться к нему из некого обособленного контейнера. Обратите внимание, что мы применяли docker-compose run вместо exec - специально для того чтобы наш redis-cli исполнялся в неком новом, обособленном контейнере, хотя и на основе того же самого образа redis. Это показывает что мы имеем возможность выполнять доступ к своему серверу Redis из некого другого контейнера.

Но позвольте, секундочку! Разве контейнеры не предполагают изолированности? Как мы стали способны подключаться из контейнера исполняющего redis-cli к имеющемуся контейнеру с запущенным сервером redis?

Хороший вопрос. Давайте изучим это в своём следующем разделе.

Как контейнеры общаются друг с другом

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

Если вы вспомните наш раздел Запуск нашего прикладного приложения, мы говорили, что docker-compose up создаёт некую новую сетевую среду для имеющегося прикладного приложения. По умолчанию все контейнеры для нашего приложения подключены к данной сетевой среде приложения и имеют возможность взаимодействовать друг с другом. Это означает что наши контейнеры, в точности как и некий физический или виртуальный сервер, способны взаимодействовать вне своих пределов при помощи сетевой среды TCP/IP {Прим. пер.: а также, как это показано в Дополнении D поверх сетевой среды RDMA.}

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


​ 	​$ ​​docker​​ ​​network​​ ​​ls​
		

Вы должны получить некий вывод, похожий на такой:


​ 	NETWORK ID          NAME                 DRIVER        SCOPE
​ 	128925dfad81        bridge               bridge        local
​ 	5bd7167263e8        host                 host          local
​ 	e2af02026928        myapp_default        bridge        local
​ 	d1145155d62a        none                 null          local
		

Самая первая сетевая среда с названием bridge является наследуемой сетевой средой для предоставления обратной совместимости с некоторыми более ранними свойствами Docker - мы не будем ею пользоваться сейчас, когда мы переключились на Compose. Аналогично, сетевые среды host и none являются особыми сетями, которые Docker устанавливает и нам не требуется о них заботиться.

Та сетевая среда о которой нам следует заботиться, именуется как myapp_default - именно она является выделенной сетью нашего приложения, которую Compose создал для нас (Compose применяет соглашение об именах <appname>_default). Та причина, по которой Compose создал эту сетевую среду для нас проста: он знает что все те службы, что мы определяем связаны с одним и тем же приложением, поэтому неминуемо им потребуется общаться друг с другом.

Но как контейнеры в этой сетевой среде обнаруживают друг друга?

Все сетевые среды Docker(за исключением наследуемой сети bridge) имеют встроенное разрешение имён DNS (Domain Name System). Это означает, что мы можем взаимодействовать с прочими контейнерами, запущенными в той же самой сетевой среде по имени. Compose применяет соответствующее название службы (как оно определено в docker-compose.yml) как некую запись DNS. Поэтому если мы желаем достичь своей службы web, она становится доступной через её название хоста web. Это предоставляется некой базовой формой службы обнаружения (service discovery) - согласованного способа поиска служб на основе контейнеров, даже по мере перезапусков контейнеров.

Это поясняет как мы способны подключаться из узкоспециализированного контейнера исполняющего конкретный redis-cli к нашему серверу Redes в качестве службы redis. Вот та команда которую мы применяли:


​ 	​$ ​​docker-compose​​ ​​run​​ ​​--rm​​ ​​redis​​ ​​redis-cli​​ ​​-h​​ ​​redis​
		

Наш параметр -h redis сообщает, "Подключиться к хосту с названием redis". Это работает исключительно потому что Compose уже создал нашу сетевую среду приложения и установил записи DNS для каждой службы. В частности, на нашу службу redis можно ссылаться по имени её хоста redis.

Наше прикладное приложение Rails общается с Redis

Хотя это и прекрасно, что мы запустили некий сервер Redis при помощи Compose, это само по себе не слишком полезно. Весь смысл работы сервера Redis состоит в том, чтобы наше приложение Rails могло общаться с ним и применять его в качестве хранилища ключ- значение. Итак, давайте подключим своё приложение Rails к Redis и реально чем= нибудь воспользуемся. Звучит весело?

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

Готовы? давайте приступим.

  Установка Gem Redis

Самый первый момент, которого нам стоит добиться чтобы заставить наше приложение Rails общаться с Redis, так это установить необходимый gem redis. Вы можете помнить, что для обновления своих gem нам требуется Повторное построение наших образов, которое мы уже рассмотрели.

Поэтому в своём Gemfile удалите комментарий с gem Redis в самом Gemfile следующим образом:


​ 	gem ​'redis'​, ​'~> 4.0'​
		

Затем остановите наш сервер Rails:


​ 	​$​ docker-compose stop web
		

и выполните повторную сборку нашего персонального образа Rails:


​ 	​$ ​​docker-compose​​ ​​build​​ ​​web​
		

Помимо прочих моментов, это запускает bundle instal, который устанавливает необходимые gem Redis:


​ 	Building web
​ 	Step 1/8 : FROM ruby:2.6
​ 	...
​ 	Step 6/8 : RUN bundle install
​ 	...
​ 	Installing redis 4.1.0
​ 	...
​ 	Bundle complete! 16 Gemfile dependencies, 69 gems now installed.
​ 	Bundled gems are installed into `/usr/local/bundle`
​ 	...
​ 	Removing intermediate container 3831c10d2cb5
​ 	​ --->​​ ​​1ca01125bd35​
​ 	Step 7/8 : COPY . /usr/src/app/
​ 	​ --->​​ ​​852dc1f2b419​
​ 	Step 8/8 : CMD ["bin/rails", "s", "-b", "0.0.0.0"]
​ 	​ --->​​ ​​Running​​ ​​in​​ ​​280c7e2eb556​
​ 	Removing intermediate container 280c7e2eb556
​ 	​ --->​​ ​​d9b3e5325308​
​ 	Successfully built d9b3e5325308
​ 	Successfully tagged myapp_web:latest
		

Полезно свыкнуться с применяемым образом повторной сборки нашего образа для bundle install обновляя наш Gemfile. Тем не менее, мы узнаем о Главе 9, Расширенном управлении Gem, который помимо того что намного быстрее, позволяет нам придерживаться нашего привычного рабочего процесса bundle install.

Давайте снова запустим свой вновь собранный сервер Rails:


​ 	​$ ​​docker-compose​​ ​​up​​ ​​-d​​ ​​web​
		

  Обновление нашего приложения Rails для использования Redis

Далее мы намерены реально применить Redis из нашего приложения Rails. Как мы уже говорили ранее, мы всего лишь желаем выполнить базовую демонстрацию того как мы можем подключаться к серверу Redis и извлекать значения. Поэтому давайте начнём с выработки контроллера welcome в нашем приложении Rails с одним единственным действием index:

[Замечание]Пользователи Linux: владение файлами

Убедитесь что вы выполнили chown для данных файлов запустив:


​ 	​$ ​​sudo​​ ​​chown​​ ​​<your_user>:<your_group> ​​-R​​ ​​.​
		

За дополнительными подробностями отсылаем к Дополнению A. Владельцы файлов и полномочия.


​ 	​$ ​​docker-compose​​ ​​exec​​ ​​web​​ ​​bin/rails​​ ​​g​​ ​​controller​​ ​​welcome​​ ​​index​
​ 	      create  app/controllers/welcome_controller.rb
​ 	       route  get 'welcome/index'
​ 	      invoke  erb
​ 	      create    app/views/welcome
​ 	      create    app/views/welcome/index.html.erb
​ 	      invoke  helper
​ 	      create    app/helpers/welcome_helper.rb
​ 	      invoke  assets
​ 	      invoke    coffee
​ 	      create      app/assets/javascripts/welcome.coffee
​ 	      invoke    scss
​ 	      create      app/assets/stylesheets/welcome.scss
		

Давайте изменим своё действие welcome#indexapp/controllers/welcome_controller.rb) чтобы оно выглядело так:


​1: 	​class​ WelcomeController < ApplicationController
​2: 	  ​def​ ​index​
​3: 	    redis = Redis.​new​(​host: ​​"redis"​, ​port: ​6379)  
​4: 	    redis.​incr​ ​"page hits"​                        
​5: 	
​6: 	    @page_hits = redis.​get​ ​"page hits"​            
​7: 	  ​end​
​8: 	​end​
		

В нашем действии index в строке 3 мы применяем gem клиента Redis для подключения к серверу Redis по имени и номеру порта, с которыми, как мы ожидаем, он должен быть запущен. Затем в строке 4 мы увеличиваем значение пары ключ- значение Redis с названием "page hits". Если вам интересно что происходит при самом первом исполнении данного кода, не волнуйтесь: Redis инициализирует его нулём если этот ключ не найден, поэтому наш код будет работать как мы и ожидаем от него. Наконец, в строке 6 мы опрашиваем число попаданий на страницу из Redis, сохраняем его в неком экземпляре переменной, готовой к отображению в нашем просмотре.

Теперь изменим свой файл просмотра (app/views/welcome/index.html.erb) для отображения значения числа заходов на страницу:


​ 	<h1>This page has been viewed <​%= pluralize(@page_hits, 'time') %>!</h1>​
		

Наконец, в config/routes.rb, давайте изменим свой автоматически вырабатываемый маршрут с тем чтобы мы могли получить доступ к новому действию index в WelcomeController из /welcome (вместо /welcome/index):


​ 	Rails.​application​.​routes​.​draw​ ​do​
​ 	  get ​'welcome'​, ​to: ​​'welcome#index'​
​ 	​end​
		

Теперь давайте посетим своё приложение Rails в нашем браузере по http://localhost:3000/welcome. Вы должны обнаружить некую страницу с нашим файлом отрисовки приветствия index.html.erb, как это показано на следующем рисунке:

 

Рисунок 5-1



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

Что это означает? Это означает что наше приложение Rails подключилось к имеющемуся серверу Redis, увеличило значение заходов на страницу с 0 до 1 и, наконец, отобразило наше сообщение приветствия с числом заходов на эту страницу. Говоря в общем, мы успешно получили два общающихся между собой контейнера. Это возможно благодаря созданной Compose сетевой среде для нашего приложения и автоматическому подключению к ней контейнеров.

Запуск всего прикладного приложения целиком при помощи Docker Compose

Мы только что добавили Redis в качестве некой новой службы в свой файл Compose и настроили своё приложение Rails для общения с ним. Пока мы это делали, наш сервер Rails уже исполнялся, поэтому мы запустили сам сервер Redis при помощи docker-compose run redis. Тем не менее, одно из преимуществ Compose состоит в том, что вне зависимости от того насколько много служб мы добавляем в своё приложение, мы можем управлять им целиком при помощи одной единственной команды, заменяя потребность в gem словно foreman.

Мы можем остановить и сервер Rails, и сервер Redis за один шаг с помощью:


​ 	​$ ​​docker-compose​​ ​​stop​
		

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


​ 	​$ ​​docker-compose​​ ​​ps​
		

Вы должны обнаружить нечто подобное:


​ 	Name                   Command               State    Ports
​ 	---------------------------------------------------------------
​ 	myapp_redis_1   docker-entrypoint.sh redis ...   Exit 0
​ 	myapp_web_1     bin/rails s -b 0.0.0.0           Exit 1
		

Это показывает что и Redis и наша служба web были остановлены; значение столбца State сообщает Exit наряду с кодом останова ту команду, которой он был осуществлён (ваше сотсояние выхода может отличаться). Если по какой- то причине хотя бы один продолжает исполнение, остановите его при помощи команды docker-compose stop (или kill).

Теперь давайте запустим все приложение целиком снова - оба сервера, Rails и Redis:


​ 	​$ ​​docker-compose​​ ​​up​​ ​​-d​
		

Теперь если мы исполним:


​ 	​$ ​​docker-compose​​ ​​ps​
		

мы можем обнаружить запущенными обе службы:


​ 	Name                  Command                State         Ports
​ 	----------------------------------------------------------------------------
​ 	myapp_redis_1  docker-entrypoint.sh redis…   Up      6379/tcp
​ 	myapp_web_1    bin/rails s -b 0.0.0.0        Up      0.0.0.0:3000->3000/tcp
		

Теперь наступает момент истины. Соединяется ли наше действие welcome#index всё ещё с имеющимся сервером Redis? Просмотрите вновь http://localhost:3000/welcome (или перезапустите эту страницу, если она всё ещё открыта) и вы должны увидеть следующий знакомый нам экран (но с увеличившимся числом посещений):

 

Рисунок 5-2



Беглый обзор

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

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

Давайте повторим основные моменты:

  1. Мы запустили некий сервер Redis в контейнере при помощи docker run. Мы рассмотрели два новых параметра: --name для придания конетйнеру какого- то дружественного нам названия и -d для запуска контейнера в отключённом (detached) режиме.

  2. Мы добавили отдельную службу в Compose для запуска такого сервера Redis.

  3. Мы убедились что этот сервер Redis исполняется (и что мы имеем возможность подключаться к нему из обособленного контейнера) запустив некий новый контейнер для выполнения redis-cli.

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

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

  6. Наконец, мы увидели что наш верный docker-compose up просто тработает и запустит сразу Rails и Redis за один проход.

Далее мы возьмём то что узнали о Compose и применим это для добавления базы данных Postgres. Мы продвинемся на шаг далее и посмотрим как обеспечить сохранность своих данных даже если исполняемый контейнер с нашей базой данных был удалён.