Как стать автором
Обновить

Как Unicorn и Puma взаимодействуют с nginx. Введение в UNIX сокеты с помощью Ruby

Время на прочтение5 мин
Количество просмотров3.7K
Автор оригинала: Starr Horne

Мы начнём с основ unix сокетов и закончим созданием простого Ruby приложения, которое может быть проксировано через nginx.

Оригинал этого поста впервые опубликован в блоге HoneyBadger в 2015, за авторством Старр Хорн. Несмотря на давний срок публикации, статья не теряет актуальности. Речь идёт о фундаментальных идеях и подходах к разработке с помощью Ruby — одного из не многих языков читаемых как естественный — Plain English.

Ruby приложения обычно используются вместе с веб сервером типа nginx. Когда пользователь запрашивает страницу вашего Rails приложения, nginx делегирует запрос серверу приложения. Но как именно это работает? Как nginx общается с Unicorn?

Одним из наиболее эффективных способов будут Unix сокеты. Давайте посмотрим как они работают! В этом посте мы начнём с основ Unix сокетов и закончим созданием своего простого Ruby приложения, которое может быть проксировано nginx.

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

Что такое UNIX сокет?

UNIX сокеты позволяют процессам коммуницировать между собой методами работы с файлами. Сокеты — один из типов IPC (inter-process communication).

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

Руби предоставляет всё что нужно для работы с unix сокетами в паре классов:

  • UNIXServer — создаёт сокеты, сохраняет их на диск, и позволяет мониторить их на предмет соединений.

  • UNIXSocket — позволяет открывать существующие сокеты.

NB: Существуют и другие типы сокетов. В частности TCP сокеты. Однако этот пост посвящён только unix сокетам. Как отличить один от другого? У unix сокета должно быть имя файла.

Простейший сокет

Рассмотрим две небольшие программы,

Первая это сервер. Она создаёт экземпляр UNIXServer и использует его метод #accept, чтобы прослушивать соединения. Когда кто-то подключается, программа шлёт привет.

Стоит отметить, что оба метода #accept и #readline блокируют программу, до того момента как метод получит то что ждёт.

require "socket"
server = UNIXServer.new '/tmp/simple.sock'
puts "==== Waiting for connection"
socket = server.accept
puts "==== Got request:"
puts socket.readline
puts "==== Sending Response"
socket.write "I read you loud and clear, good buddy!"
socket.close

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

В примере ниже мы будем работать с сокетом созданным нашим сервером. Затем воспользуемся обычными IO методами для обмена приветами.

require "socket"
socket = UNIXSocket.new "/tmp/simple.sock"
puts "==== Sending"
socket.write "Hello server, can you hear me?\n"
puts "==== Getting Response"
puts socket.readline
socket.close

Для демонстрации запустим сначала клиент, а потом сервер. Результат не должен отличаться от следующего:

$ ruby server.rb
==== Waiting for connection
==== Got request:
Hello server, can you hear me?
==== Sending Response
$ ruby client.rb
==== Sending
==== Getting Response
I read you loud and clear, good buddy!


Это простейший пример взаимодействия с помощью unix сокетов. Клиент снизу и сервер сверху в сниппетах выше.

Взаимодействие с nginx

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

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

require "socket"
# Создадим сокет и сохраним его в файловой системе
server = UNIXServer.new "/tmp/socktest.sock"
# Ждём входящие подключения от nginx
socket = server.accept
# Читаем всё что приходит на сокет
while line = socket.readline
  puts line.inspect
end
# Закрываем сокет (так же как и при окончании работы с файлом)
socket.close

Теперь если мы настроим nginx так, чтобы он перенаправлял запросы на сокет по пути /tmp/socktest.sock, мы сможем получать все данные отправляемые nginx. (Конфиг будет приведён ниже.)

Когда мы делаем запрос, nginx отправляет нашему серверу следующие данные:

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

Установка и настройка nginx

Если у вас нет nginx — установите любым удобным способом, например с помощью homebrew, если вы на маке:

brew install nginx

Теперь нам нужно настроить nginx, так чтобы он перенаправлял запросы на localhost:2047 к upsream-серверу через сокет лежащий по пути /tmp/sockettest.sock. Путь и имя могут быть любыми, главное чтобы они совпадали с теми, что мы используем в нашем приложении.

Давайте сохраним следующий конфиг по пути /tmp/nginx.conf и запустим nginx указав с параметром указывающим на конфиг nginx -c /tmp/nginx.conf.

# Запускаем nginx как обычно, не в режиме демона
daemon off;

# Ошибки в stdout
error_log /dev/stdout info;

events {} # Боейлерплейт

http {

  # Логи обращений к серверу тоже в stdout
  access_log /dev/stdout;

  # Сообщаем nginx о существовании внешнего сервера @app, работающего на нашем сокете
  upstream app {
    server unix:/tmp/socktest.sock fail_timeout=0;
  }

  server {

    # Слушаем соединения на localhost:2048
    listen 2048;
    server_name localhost;

    # Наша коренева директория
    root /tmp;

    # Если на диске нет такого файла, запросы идут к @app
    try_files $uri/index.html $uri @app;

    # Настроим форвардинг заголовков
    location @app {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_redirect off;
      proxy_pass http://app;
    }
  }
}

Этот конфиг заставляет nginx работать в обычном режиме, не в виде демона. А также писать много логов в stdout. При запуске в терминале должно появиться что-то вроде этого:

$ nginx -c /tmp/nginx.conf
2022/12/10 11:51:40 [notice] 14497#0: using the "kqueue" event method
2022/12/10 11:51:40 [warn] 14497#0: 512 worker_connections exceed open file resource limit: 256
nginx: [warn] 512 worker_connections exceed open file resource limit: 256
2022/12/10 11:51:40 [notice] 14497#0: nginx/1.23.2
2022/12/10 11:51:40 [notice] 14497#0: built by clang 14.0.0 (clang-1400.0.29.202)
2022/12/10 11:51:40 [notice] 14497#0: OS: Darwin 22.1.0
2022/12/10 11:51:40 [notice] 14497#0: hw.ncpu: 10
2022/12/10 11:51:40 [notice] 14497#0: net.inet.tcp.sendspace: 131072
2022/12/10 11:51:40 [notice] 14497#0: kern.ipc.somaxconn: 128
2022/12/10 11:51:40 [notice] 14497#0: getrlimit(RLIMIT_NOFILE): 256:9223372036854775807
2022/12/10 11:51:40 [notice] 14497#0: start worker processes
2022/12/10 11:51:40 [notice] 14497#0: start worker process 14498

Такой вывод означает, что nginx работает в обычном, а не daemon, режиме.

Сервер приложения своими руками

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

Программа ниже принимает запросы по HTTP и возвращает текущее время.

require "socket"

# Connection - создаёт сокет и прослушивает подключения
class Connection
  attr_accessor :path

  def initialize path:
    @path = path
    File.unlink(path) if File.exists?(path)
  end

  def server
    @server ||= UNIXServer.new @path
  end

  def on_request
    socket = self.server.accept
    yield socket
    socket.close
  end

end

# AppServer - записывает входящие запросы и возвращает вьюху
class AppServer
  attr_reader :connection, :view

  def initialize connection:, view:
    @view = view
    @connection = connection
  end

  def run
    while true
      connection.on_request do |socket|
        while (line = socket.readline) != "\r\n"
          puts line
        end
        socket.write view.render
      end
    end
  end
end

# TimeView - просто возвращает HTTP ответ
class TimeView
  def render
    %[HTTP/1.1 200 OKThe Current timestamp is: #{Time.now.to_i}]
  end
end

AppServer.new(connection: Connection.new(path: "/tmp/socktest.sock"), view: TimeView.new).run

Теперь, если мы запустим nginx и наш скрипт вместе, то сможем увидеть текущее время по адресу http://localhost:2048 прямо в браузере. Невероятно круто!

$ ruby app.rb
GET / HTTP/1.0
X-Forwarded-For: 127.0.0.1
Host: localhost:2048
Connection: close
User-Agent: curl/7.84.0
Accept: */*

HTTP запросы логируются в stdout.

А вот и результат наших трудов. Встречайте незаменимое приложение — часы!

$ curl localhost:2048
The Current timestamp is: 1670652851

Теги:
Хабы:
Всего голосов 4: ↑3 и ↓1+2
Комментарии4

Публикации

Истории

Работа

Ruby on Rails
6 вакансий
Программист Ruby
7 вакансий

Ближайшие события

19 сентября
CDI Conf 2024
Москва
24 сентября
Конференция Fin.Bot 2024
МоскваОнлайн
30 сентября – 1 октября
Конференция фронтенд-разработчиков FrontendConf 2024
МоскваОнлайн