1 安裝和配置 nginx-gridfs
我們存取圖片的請求首先經過 nginx, 然後再通過 nginx-gridfs 轉到 mongodb, 所以配置 nginx-gridfs 是很重要的一步。
如果伺服器上沒有安裝 nginx, 我們參考 https://github.com/mdirolf/nginx-gridfs#installation 的步驟安裝即可,如果已經安裝好了 nginx, 但是沒有安裝 nginx-gridfs 模組,那麼我們可以只安裝 nginx-gridfs 模組。
1.1 安裝 nginx 和 nginx-gridfs
為簡單起見,我們假設是第一次安裝 nginx, 並且是通過原始碼安裝。
1, 下載 nginx 源碼, 並且解壓縮
wget http://nginx.org/download/nginx-1.10.0.tar.gz
tar -xvf nginx-1.10.0.tar.gz
2, 將 nginx-gridfs 複製到本地
git clone git@github.com:mdirolf/nginx-gridfs.git
cd nginx-gridfs
git checkout v0.8
git submodule init
git submodule update
3, build
cd /path/to/nginx-source
./configure --add-module=/path/to/nginx-gridfs/source/
make
make install
1.2 配置 nginx-gridfs
這一步其實就是在 nginx 的 conf 檔案增加 gridfs 相關的內容。
nginx-gridfs 上的官方配置文檔過於簡單,不適合生產環境。我們接下來的配置會在 nginx conf 檔案裡 增加兩個 server, 第 1 個 server 將監聽 4444 連接埠,這個 server 將直接和 mongodb 互動,第 2 個 server 將監聽 5555 連接埠,這個 server 上面會添加圖片緩衝等配置,並且在緩衝到期後會將請求轉寄 到第 1 個 server, 具體配置內容如下:
$ cat nginx.conf
worker_processes 1;
events {
worker_connections 1024;
}
http {
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
include mime.types;
proxy_temp_path /data/nginx_cache/nginx_temp;
proxy_cache_path /data/nginx_cache/nginx_cache levels=1:2 keys_zone=cache_one:4000m inactive=2d max_size=10g;
server{
listen 4444;
location / {
gridfs imgdb root_collection=fs field=filename type=string;
mongo 127.0.0.1:27017;
}
}
upstream my_server_pool {
server 127.0.0.1:4444 weight=1;
}
server {
listen 5555;
location /upload/ {
proxy_cache cache_one;
proxy_cache_valid 200 304 2d;
proxy_cache_valid 301 302 1m;
proxy_cache_valid any 1s;
proxy_cache_key $host$uri$is_args$args;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://my_server_pool/;
add_header X-Cache HIT-LT;
expires max;
}
}
}
這樣我們就完成了 nginx-gridfs 的配置。
這裡要注意 /data/nginx_cache/nginx_temp 的存取權限問題,如果 nginx 的 error log 中出現類似 failed (13: Permission denied) while reading upstream 的錯誤,可以使用 chown 命令將 /data/nginx_cache/nginx_temp 的 owner 修改下。
我們分析下 4444 連接埠也就是和 Mongo GridFS 互動的配置的細節:
server{
listen 4444;
location / {
gridfs imgdb root_collection=fs field=filename type=string;
mongo 127.0.0.1:27017;
}
}
gridfs: nginx識別外掛程式的名字;
imgdb: 資料庫名稱;
root_collection: 選擇collection,如root_collection=images, mongod就會去找images.files,預設是fs;
field: 查詢欄位,支援_id, filename, 可省略, 預設是_id, 在這裡我們使用 filename 即圖片的名字去尋找圖片;
type: 解釋field的資料類型,支援objectid, int, string, 可省略, 預設是int, 如果我們 field 用的是 filename, 那麼此處需要設定為 string;
user: 使用者名稱, 可省略;
pass: 密碼, 可省略;
mongo: mongodb 的 host 和 port;
2 啟動 nginx 和 mongodb, 並測試是否能夠正常存取圖片
啟動 nginx:
/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
mongodb 的設定檔如下:
port=27017
dbpath=/data/db
logpath=/var/mongodb/log/mongodb.log
logappend=true
fork=true
啟動 mongodb:
mongod -f /path/to/mongodb.conf
在啟動 nginx 和 mongodb 時會出現一些許可權和目錄不存在的問題,可以使用 chown 和 mkdir -p 解決。上面的配置步驟都是在 mac os 上操作的。
測試上傳圖片:
mongofiles --host localhost --port 27017 --db imgdb --local=/path/to/dog.jpg put my_dog.jpg
通過瀏覽器可以訪問已經上傳的圖片:
3 與 Rails 項目整合
3.1 demo 項目簡介
在寫這個 demo 時,我原本打算參照我所在公司的項目的代碼來完成圖片上傳的功能,但是由於公司使用的 rails 版本比較低,相關配套的 gem 在適配比較新的 rails 版本(這個 demo 用的 rails 版本是 4.2.6)時出現了很多問題, 並且相關的配置過於繁瑣複雜,我決定只使用 mongodb 的官方 ruby driver 來實現圖片上傳的功能。
公司項目中為了實現圖片上傳使用了如下的 gem:
# Gemfile
gem 'carrierwave'
gem 'carrierwave-mongoid'
gem 'mongoid'
只使用 mongodb 的官方 ruby driver: mongo,
# Gemfile
gem 'mongo', '~> 2.2'
在此 demo 中, 圖片上傳的商務程序可以簡單到用一句話說清楚:
應用為圖片產生唯一的名稱,然後將圖片提交給圖片伺服器,如果圖片伺服器儲存圖片成功,則將圖片名稱儲存到 資料庫以供應用將來從圖片伺服器拿圖片。
3.2 demo 設計
當然在實際的應用中還涉及到圖片的壓縮,裁減,變換,加浮水印等商務邏輯, 但是這些商務邏輯應該是和圖片上傳隔離的不應該耦合在一起,從這種角度考慮,我比較討厭 carrierwave 之類的圖片上傳庫把圖片的上傳,裁減,變換等工作都放到一個 Uploader 裡去實現。
作為一名程式員,將自己的想法轉化成直觀的圖形有兩點好處:
方便自己寫代碼
方便別人寫代碼
所以我們仍然對圖片上傳這一看似簡單的功能畫一張圖:
通過分析上面的圖, 我們可以分4個步驟實現圖片上傳的功能:
應用方面,我們需要產生 image params, 比如 image name, image 實體等參數提交給圖片伺服器用戶端;
圖片伺服器用戶端方面, 我們需要寫伺服器的配置資訊, 並串連伺服器;
圖片伺服器用戶端方面, 我們需要將圖片參數提交給伺服器;
應用方面, 我們需要處理圖片伺服器的響應, 如果響應成功我們需要將圖片的名稱等參數記錄下來;
3.3 demo 實現
3.3.1 圖片伺服器用戶端的配置和初始化
先寫設定檔:
# config/mongo.yml
development:
host: localhost
port: 27017
database: imgdb
test:
host: localhost
port: 27017
database: imgdb
staging:
host: localhost
port: 27017
database: imgdb
production:
host: localhost
port: 27017
database: imgdb
初始化用戶端:
# config/initializers/mongo.rb
mc = YAML.load_file(Rails.root.join('config', 'mongo.yml'))[Rails.env]
db_url = "mongodb://#{mc['host']}/#{mc['database']}"
$mongo = Mongo::Client.new(db_url)
這樣我們就擁有了一個可以全域訪問的用戶端: $mongo
3.3.2 編寫圖片上傳服務
圖片上傳的服務邏輯非常簡單: 接受一個 file 參數, 返回 filename 和 content_type
# app/services/upload_file_service.rb
class UploadFileService
def initialize(file)
@file = file
end
def call
grid = $mongo.database.fs
grid.upload_from_stream(filename, @file)
res = {
filename: filename,
content_type: @file.content_type
}
end
private
def filename
return @filename if @filename.present?
ext = File.extname(@file.original_filename)
@filename = "#{SecureRandom.uuid}#{ext}"
end
end
使用 UploadFileService 也非常簡單,可以參考相關控制器的代碼:
# app/controllers/avatars_controller.rb
class AvatarsController < ApplicationController
def create
@avatar = Avatar.new
file = params[:avatar][:attachment_file_name]
# 調用圖片上傳服務將圖片上傳到圖片伺服器
# 並將圖片名字存入到資料庫
res = UploadFileService.new(file).call
@avatar.attachment_file_name = res[:filename]
@avatar.attachment_content_type = res[:content_type]
if @avatar.save
redirect_to action: 'index'
else
render :new
end
end
end