Ruby application server thường dùng cùng web server như nginx. Khi user request trang từ Rails app, nginx chuyển request cho application server. Nhưng chính xác nó hoạt động như thế nào? Nginx giao tiếp với unicorn thế nào?
Một trong những tùy chọn hiệu quả nhất là dùng unix socket. Hãy xem chúng hoạt động như thế nào! Trong bài này chúng ta sẽ bắt đầu từ cơ bản về socket, và kết thúc bằng việc tạo application server đơn giản được proxy bởi nginx.
Socket cho phép chương trình giao tiếp như thể đang ghi/đọc file. Trong ví dụ này, Unicorn tạo socket và theo dõi kết nối. Nginx kết nối tới socket và nói chuyện với Unicorn.
Unix socket là gì?
Unix socket cho phép một chương trình nói chuyện với chương trình khác theo cách giống như làm việc với file. Chúng là loại IPC (inter-process communication).
Để có thể truy cập qua socket, chương trình của bạn trước tiên tạo socket và "lưu" nó vào disk, như file. Nó theo dõi socket cho kết nối đến. Khi nhận được kết nối, nó dùng phương thức IO tiêu chuẩn để đọc/ghi dữ liệu.
Ruby cung cấp mọi thứ cần thiết để làm việc với unix socket qua hai class:
UNIXServer - Tạo socket, lưu vào disk, cho phép theo dõi kết nối mới.
UNIXSocket - Mở socket hiện có cho IO.
LƯU Ý: Các loại socket khác tồn tại. Đặc biệt TCP socket. Nhưng bài này chỉ nói về unix socket. Làm sao phân biệt? Unix socket có tên file.
Socket đơn giản nhất
Chúng ta sẽ xem hai chương trình nhỏ.
Thứ nhất là "server". Nó chỉ tạo instance của class UnixServer, rồi dùng server.accept để chờ kết nối. Khi nhận kết nối, nó trao đổi chào hỏi.
Lưu ý cả accept và readline đều block thực thi chương trình cho đến khi nhận được thứ chúng chờ.
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
Vậy là có server. Bây giờ chỉ cần client.
Trong ví dụ dưới, chúng ta mở socket do server tạo. Sau đó dùng phương thức IO bình thường để gửi/nhận chào hỏi.
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
Để demo, trước tiên chạy server. Sau đó chạy client. Kết quả như dưới:
Ví dụ tương tác client/server UNIX socket đơn giản. Client bên trái. Server bên phải.
Giao tiếp với nginx
Bây giờ chúng ta biết cách tạo unix socket "server", có thể dễ dàng giao tiếp với nginx.
Không tin? Hãy làm proof-of-concept nhanh. Tôi sẽ adapt code trên để in ra mọi thứ nhận được từ socket.
require "socket" # Tạo socket và "lưu" vào file system server = UNIXServer.new('/tmp/socktest.sock') # Chờ kết nối (bởi nginx) socket = server.accept # Đọc mọi thứ từ socket while line = socket.readline puts line.inspect end socket.close
Nếu tôi cấu hình nginx forward request tới socket tại /tmp/socktest.sock tôi có thể thấy nginx gửi dữ liệu gì. (Đừng lo, sẽ thảo luận cấu hình ngay)
Khi tôi làm web request, nginx gửi dữ liệu sau tới server nhỏ của tôi:

Khá cool! Chỉ là HTTP request bình thường với vài header thêm. Bây giờ sẵn sàng xây app server thật. Nhưng trước, hãy nói về cấu hình nginx.
Cài đặt và cấu hình Nginx
Nếu chưa có nginx trên máy dev, hãy cài ngay. Rất dễ trên OSX qua homebrew:
brew install nginx
Bây giờ cần cấu hình nginx forward request trên localhost:2048 tới upstream server qua socket tên /tmp/socktest.sock. Tên đó không đặc biệt. Chỉ cần khớp với tên socket dùng bởi web server.
Bạn có thể lưu cấu hình vào /tmp/nginx.conf và chạy nginx với lệnh nginx -c /tmp/nginx.conf để load.
# Chạy nginx như chương trình console bình thường, không phải daemon daemon off; # Log lỗi ra stdout error_log /dev/stdout info; events {} # Boilerplate http { # In access log ra stdout access_log /dev/stdout; # Nói cho nginx biết có external server @app sống tại socket upstream app { server unix:/tmp/socktest.sock fail_timeout=0; } server { # Chấp nhận kết nối trên localhost:2048 listen 2048; server_name localhost; # Application root root /tmp; # Nếu path không tồn tại trên disk, forward request tới @app try_files $uri/index.html $uri @app; # Set config cho request forward tới @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 https://app; } } }
Cấu hình này khiến nginx chạy như app terminal bình thường, không phải daemon. Nó cũng ghi tất cả log ra stdout. Khi chạy nginx trông như sau:
Nginx chạy ở chế độ non-daemon.
DIY Application Server
Bây giờ đã thấy cách kết nối nginx với chương trình, xây simple application server là việc đơn giản. Khi nginx forward request tới socket của chúng ta, đó là HTTP request tiêu chuẩn. Sau chút thử nghiệm tôi xác định nếu socket trả về HTTP response hợp lệ, nó sẽ hiển thị trên browser.
Application dưới nhận bất kỳ request nào và hiển thị timestamp.
require "socket" # Connection tạo socket và chấp nhận kết nối mới 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 = server.accept yield(socket) socket.close end end # AppServer log request đến và render view phản hồi class AppServer attr_reader :connection attr_reader :view def initialize(connection:, view:) @connection = connection @view = view 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 chỉ cung cấp HTTP response class TimeView def render %[HTTP/1.1 200 OK The current timestamp is: #{ Time.now.to_i } ] end end AppServer.new(connection: Connection.new(path: '/tmp/socktest.sock'), view: TimeView.new).run
Bây giờ nếu tôi khởi động nginx cũng như script, tôi vào localhost:2048. Request được gửi tới app. Và response được render bởi browser. Pretty Cool!
HTTP request được log ra STDOUT bởi simple app server
Và đây là thành quả lao động. Ngắm xem! Một Timestamp!
Server trả về timestamp hiển thị trên browser