Viết microservice với RabbitMQ
Microservice là chủ đề mới và ngày càng nóng hổi trong cộng đồng lập trình viên. Bài viết này sẽ giới thiệu sơ lược về cách xây dựng một microservice trên Ruby on Rails. Bài toán ở đây là, viết một microservice dùng để gửi mail. Microservice này sẽ nhận một message có dạng: { "provider" : ...
Microservice là chủ đề mới và ngày càng nóng hổi trong cộng đồng lập trình viên. Bài viết này sẽ giới thiệu sơ lược về cách xây dựng một microservice trên Ruby on Rails.
Bài toán ở đây là, viết một microservice dùng để gửi mail. Microservice này sẽ nhận một message có dạng:
{ "provider": "framgia", "template": "thanks", "from": "support@framgia.com", "to": "user@example.com", "replacements": { "salutation": "VietNH", "year": "2016" } }
Và tự động gửi một thanks mail đến địa chỉ user@example.com và sử dụng một số biến số được truyền vào replacements. Đây là một ví dụ hoàn hảo cho một microservice bởi vì service sẽ rất nhỏ, tập chung hoàn thành một chức năng với một giao diện rõ ràng.
Với một microservice, ta cần một cách nào đó để gửi các thông điệp đến nó. Để tránh mất mát dữ liệu, ta sử dụng một hàng đợi các thông điệp. Ta có thể tự viết một hàng đợi như thế, hoặc sử dụng một gem có sẵn để thực hiện điều này. Ở đây, ta chọn sử dụng RabbitMQ, bởi vì những lý do sau:
- RabbitMQ rất phổ biến với mã hóa chuẩn AMQP.
- RabbitMQ hỗ trợ nhiều ngôn ngữ lập trình, có thể sử dụng trong các ứng dụng đa ngôn ngữ.
- RabbitMQ phù hợp với nhiều hoàn cảnh, từ những workflow đơn giản như các hàng đợi có tên đến những hàng đợi có logic phức tạp.
- RabbitMQ hỗ trợ giao diện đồ họa với một trang admin riêng trên web browser.
- RabbitMQ có thể được host trên một server khác với ứng dụng.
Việc gửi một thông điệp đến hàng đợi của RabbitMQ có thể thực hiện dễ dàng:
require 'bunny' require 'json' connection = Bunny.new connection.start channel = connection.create_channel queue = channel.queue 'mails', durable: true json = { ... }.to_json queue.publish json connection.close
bunny là gem chính thức của RabbitMQ trên Rails. Nếu ta không truyền thêm tham số vào Bunny.new, RabbitMQ sẽ tự động chạy trên cổng localhost:5672. Ta có thể truy cập và sử dụng hàng đợi có tên "mails". Nếu không có hàng đợi có tên tương ứng, một hàng đợi mới sẽ được tạo. Các thông điệp gửi đến microservice sẽ được đẩy trực tiếp vào hàng đợi này. Ở đây, thông điệp được truyền dưới dạng json, nhưng ta hoàn toàn có thể sử dụng bất cứ format nào nếu muốn.
Bây giờ ta bắt đầu xây dựng microservice, một ứng dụng lấy các message về, phân tích và thực hiện gửi mail. Với mục đích đó, ta dùng sneakers, gem hỗ trợ của RabbitMQ. Sử dụng sneakers, ta có thể định nghĩa một worker như sau:
require 'sneakers' require 'json' require 'mandrill_api/provider' class Mailer include Sneakers::Worker from_queue 'mails' def work(message) puts "RECEIVED: #{message}" option = JSON.parse(message) MandrillApi::Provider.new.deliver(options) ack! end end
Như đã định nghĩa ở trên, ta đã có một hàng đợi có tên "mails" để nhận các thông điệp. Worker này có nhiệm vụ lần lượt lấy các thông điệp về, parse dưới dạng json (cũng đã được định nghĩa ở trên) và chuyển đến tác vụ gửi mail.
Việc tiếp theo để hoàn thiện microservice là setup các biến môi trường. Giả sử ta có thư mục rails project có dạng như sau:
. ├── Gemfile ├── Gemfile.lock ├── Procfile ├── README.md ├── bin │ └── mailer ├── config │ ├── deploy/... │ ├── deploy.rb │ ├── settings.yml │ └── setup.rb ├── examples │ └── mail.rb ├── lib │ ├── mailer.rb │ └── mandrill_api/... └── spec ├── acceptance/... ├── acceptance_helper.rb ├── lib/... └── spec_helper.rb
Ta thực hiện các setting trong file bin/mailer.rb:
#!/usr/bin/env ruby require_relative '../config/setup' require 'sneakers/runner' require 'logger' require 'mailer' require 'httplog' Sneakers.configure( amqp: SETTINGS['amqp_url'], daemonize: false, log: STDOUT ) Sneakers.logger.level = Logger::INFO Httplog.options[:log_headers] = true Sneakers::Runner.new([Mailer]).run
Từ bây giờ mỗi khi chạy file bin/mailer ta sẽ thấy log có dạng:
... WARN: Loading runner configuration... ... INFO: New configuration: #<Sneakers::Configuration:0x007f96229f5f28 ...> ... INFO: Heartbeat interval used (in seconds): 2
Từ đây, mỗi khi gửi một message, ta sẽ nhận được log có dạng:
... RECEIVED: {"provider":"framgia","template":"thanks", ...} D, ... [httplog] Sending: POST https://mandrillapp.com:443/api/1.0/messages/send-template.json D, ... [httplog] Data: {"template_name":"invoice", ...} D, ... [httplog] Connecting: mandrillapp.com:443 D, ... [httplog] Status: 200 D, ... [httplog] Response: [{"email":"user@example.com","status":"sent", ...}] D, ... [httplog] Benchmark: 1.698229061003076 seconds
Việc xây dựng một microservice không tốn qúa nhiều thời gian và công sức. Nhưng phần khó nhất ở đây là việc deploy. Deploy một microservice thường phải thỏa mãn một số yêu cầu:
- Microservice được chạy ở một process riêng và chạy ngầm so với rails server.
- Microservice phải được log riêng biệt với log của server
- Microservice sẽ được restart mỗi khi server được restart.
- Microservice phải có các lệnh start/stop/restart để sử dụng khi cần.
Tất cả những việc này đều có thể làm được với Ruby, nhưng ta có thể sử dụng các ứng dụng của hệ điều hành (ở đây là Linux) để làm điều này. Ở đây, ta có thể sử dụng một tool tên là foreman. Với foreman, ta có thể xác định các process cần phải chạy trong một Procfile. Ta sử dụng foreman để export các biến môi trường để có thể cài đặt ở nhiều server. Ví dụ, một file của foreman export có dạng:
[Unit] PartOf=-.target [Service] User=mailer_user WorkingDirectory=/var/www/mailer_production/releases/16 Environment=PORT=5000 Environment=PATH= /home/deploy/.rvm/gems/ruby-2.2.3/gems/bundler-1.11.2:... Environment=ENVIRONMENT=production ExecStart=/bin/bash -lc 'bin/mailer' Restart=always StandardInput=null StandardOutput=syslog StandardError=syslog SyslogIdentifier=%n KillMode=process
Với các biến môi trường này, ta có thể nhìn rõ các service cần dùng, lệnh để chạy các service này, và có thể cài đặt chúng thành các lệnh theo ý thích. Ta có thể cài đặt để chạy mỗi khi khởi động hệ thống với lệnh sudo systemctl enable mailer.target. Và với log output, ta chỉ quy định standard output cho service.
Để ra lệnh cho foreman export các biến môi trường, trước tiên cần cài đặt các process trên môi trường lập trình. Ta có thể ghi chúng vào file .env:
$ echo "PATH=$(bundle show bundler):$PATH" >> .env $ echo "ENVIRONMENT=production" >> .env
Sau đó ta dùng foreman để export systemd trong khi đọc các biến môi trường trong file .env:
$ sudo -E env "PATH=$PATH" bundle exec foreman export systemd /etc/systemd/system -a mailer -u mailer_user -e .env
Sau đó reload lại để nhận các biến môi trường mới:
$ sudo systemctl daemon-reload $ sudo systemctl reload-or-restart mailer.target
Sau đó, ta bắt đầu khởi động service:
$ sudo systemctl enable mailer.target
Bây giờ, service của chúng ta đã có thể chạy trên server, sẵn sàng nhận các message.