您的位置:首页 > 运维架构 > Nginx

OpenResty(Nginx)+Lua+GraphicsMagick实现缩略图功能

2017-09-05 00:00 1201 查看
一、背景说明
大多数网站基本都涉及到图片缩略图的处理,比如新闻配图、电商商品图等,特别是电商类网站,每个商品图对应多个不同尺寸的缩略图,用于不同的页面。

初期访问量少时,处理流程一般由web程序在上传成功后,同时生成相应缩略图。这种方式在访问量小,单机部署时没有问题。当访问量逐渐加大,服务器由单台变为多台时,这种方式扩展性较差。

以下有几种方案可以解决这个问题:
1、使用七牛又拍云提供的云存储及数据处理服务,解决图片的处理、存储、多节点访问速度的问题,这种方式优点是方案成熟,相应的有一定费用和开发工作,另外有一些小概率的风险,比如云服务挂掉影响本站访问。
2、使用第三方的图片处理程序,比如zimg,点击查看使用手册@招牌疯子开发。zimg的性能和扩展性不错,文档也很完善,会继续保持关注。
3、自己造轮子,根据自身业务,将生成缩略图功能独立出来,与web程序解耦。

我们采用的是第三种方案,参考了网友的基础代码,利用OpenResty(Nginx)+Lua+GraphicsMagick实现缩略图功能,图片上传及删除还是由web程序处理,缩略图由单独模块完成。目前可实现配置路径及缩略尺寸,无图片时显示默认图片,支持多种缩放方式等,后续可基于GraphicsMagick实现更多功能

二、相关规范
1、文件夹规划

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
img.hopesoft.org
|-- avatars
| `-- 001
| `-- 001.jpg
|-- default
| `-- notfound.jpg
|-- photos
| `-- 001
| `-- 001.jpg
`-- thumbnail
`-- photos
`-- 001
|-- 001_100x100.jpg
|-- 001_140x140.jpg
|-- 001_250x250.jpg
|-- 001_300x300.jpg
|-- 001_350x350.jpg
|-- 001_50x50.jpg
`-- abc_50x50.jpg
其中img.hopesoft.org为图片站点根目录,avatars和photos目录是原图目录,可根据目录设置不同的缩略图尺寸,default文件夹的notfound.jpg文件是在未找到原图时的默认图片,thumbnail文件夹用来存放缩略图,可定时清理。

2、链接地址
原图访问地址:/Uploads/Images/Content/201510/4369bb4932007e029bfe5c5c0432bd31.jpg
缩略图访问地址:/Uploads/Images/Content/201510/94ba883c120f13ba47bf8ef74c8b9f5f.jpg,(请勿加thumbnail,这个是因为我们原来的原图和缩略图在同一个目录,后来将缩略图单独放了一个文件夹)
不同目录可设置不同的缩略图规则,如:
原图访问地址:http://img.xxx.com/mall/001/001.jpg
缩略图访问地址:http://img.xxx.com/mall/001/001.jpg_100x100.jpg (请勿加thumbnail)

3、访问流程
首先判断缩略图是否存在,如存在则直接显示缩略图;
如不存在则按以下流程处理:

判断缩略图链接与规则是否匹配,如不匹配,则404退出;如匹配跳至2

判断原图是否存在,如原图存在则跳至5,如不存在则进入下一步;

判断是否显示默认图片,如不显示则404退出;如显示则进入下一步

判断是否存在默认图片,如不存在则404退出;如存在则将默认图片代替原始图片,进入下一步;

拼接graphicsmagick命令,生成并显示缩略图

三、安装OpenResty
1、关于OpenResty(http://openresty.org/cn/

OpenResty (也称为 ngx_openresty)是一个全功能的 Web 应用服务器,它打包了标准的 Nginx 核心,很多的常用的第三方模块,以及它们的大多数依赖项。
OpenResty 通过汇聚各种设计精良的 Nginx 模块,
从而将 Nginx 有效的变成一个强大的 Web 应用服务器,
这样, Web 开发人员可以使用 Lua 脚本语言调动 Nginx 支持的各种C以及Lua 模块,
快速构造出足以胜任 10K+ 并发连接响应的超高性能Web 应用系统。更多

2、安装OpenResty
注:drizzle-nginx-module模块和nginx-http-concat模块非必选项,各位可按需安装

#创建安装文件目录

mkdir -p /usr/local/src/nginx

cd /usr/local/src/nginx

#安装drizzle模块(访问mysql数据库模块,非必需,建议安装)

wget http://openresty.org/misc/nginx/drizzle7-2011.07.21.tar.gz 
tar zxvf drizzle7-2011.07.21.tar.gz

cd drizzle7-2011.07.21

./configure --without-server

make libdrizzle-1.0

make install-libdrizzle-1.0

cd ..

#下载openresty

wget https://openresty.org/download/ngx_openresty-1.7.10.2.tar.gz 
tar zxvf ngx_openresty-1.7.10.2.tar.gz

#下载nginx-http-concat(合并静态文件请求模块,非必需,建议安装)

wget https://github.com/alibaba/nginx-http-concat/archive/master.zip 
unzip master

mv nginx-http-concat-master/ ngx_openresty-1.7.10.2/bundle/nginx-http-concat

#安装openresty

cd ngx_openresty-1.7.10.2

./configure --with-luajit --with-http_drizzle_module --with-http_iconv_module --with-ld-opt="-Wl,-rpath,/usr/local/lib" --with-http_stub_status_module --with-http_ssl_module --with-http_sub_module --add-module=./bundle/nginx-http-concat/

gmake

gmake install

3、将OpenResty(Nginx)加入自启动

vi /etc/rc.d/init.d/nginx

文件内容如下

#! /bin/sh

# chkconfig: 2345 55 25

# Description: Startup script for nginx webserver on Debian. Place in /etc/init.d and

# run 'update-rc.d -f nginx defaults', or use the appropriate command on your

# distro. For CentOS/Redhat run: 'chkconfig --add nginx'

### BEGIN INIT INFO

# Provides:          nginx

# Required-Start:    $all

# Required-Stop:     $all

# Default-Start:     2 3 4 5

# Default-Stop:      0 1 6

# Short-Description: starts the nginx web server

# Description:       starts nginx using start-stop-daemon

### END INIT INFO

PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

DESC="nginx daemon"

NAME=nginx

DAEMON=/usr/local/openresty/nginx/sbin/$NAME

CONFIGFILE=/usr/local/openresty/nginx/conf/$NAME.conf

PIDFILE=/usr/local/openresty/nginx/logs/$NAME.pid

SCRIPTNAME=/etc/init.d/$NAME

set -e

[ -x "$DAEMON" ] || exit 0

do_start() {

$DAEMON -c $CONFIGFILE || echo -n "nginx already running"

}

do_stop() {

kill -INT `cat $PIDFILE` || echo -n "nginx not running"

}

do_reload() {

kill -HUP `cat $PIDFILE` || echo -n "nginx can't reload"

}

case "$1" in

start)

echo -n "Starting $DESC: $NAME"

do_start

echo "."

;;

stop)

echo -n "Stopping $DESC: $NAME"

do_stop

echo "."

;;

reload|graceful)

echo -n "Reloading $DESC configuration..."

do_reload

echo "."

;;

restart)

echo -n "Restarting $DESC: $NAME"

do_stop

do_start

echo "."

;;

*)

echo "Usage: $SCRIPTNAME {start|stop|reload|restart}" >&2

exit 3

;;

esac

exit 0


把nginx加入chkconfig,并设置开机启动

chkconfig --add nginx

chkconfig nginx on

启动、停止、查看状态

service nginx start
service nginx stop
service nginx status
四、安装GraphicsMagick
1、安装

#创建安装目录

mkdir -p /usr/local/src/graphicsmagick

cd /usr/local/src/graphicsmagick

#下载graphicsmagick

wget http://sourceforge.net/projects/graphicsmagick/files/graphicsmagick/1.3.18/GraphicsMagick-1.3.18.tar.gz/download 
tar zxvf GraphicsMagick-1.3.18.tar.gz

#下载libjpeg

wget  ftp://ftp.pl.freebsd.org/vol/rzm1/GraphicsMagick/delegates/libjpeg-6b.tar.gz 
rpm -qa | grep libjpeg

rpm -qa | grep libjpeg | xargs rpm -e --nodeps --allmatches

tar zxvf libjpeg-6b.tar.gz

cd libjpeg-6b

./configure

make

make install

ln -s /usr/local/lib/libjpeg* /lib/

ln -s /usr/local/lib/libjpeg* /lib64/

cd ..

#安装libpng

rpm -qa | grep libpng

rpm -qa | grep libpng | xargs rpm -e --nodeps --allmatches

wget ftp://ftp.pl.freebsd.org/vol/rzm1/GraphicsMagick/delegates/libpng-1.2.49.tar.gz 
tar zxvf libpng-1.2.49.tar.gz

cd libpng-1.2.49

./configure

make

make install

ln -s /usr/local/lib/libpng* /lib/

ln -s /usr/local/lib/libpng* /lib64/

cd ../

#安装freetype

wget ftp://ftp.pl.freebsd.org/vol/rzm1/GraphicsMagick/delegates/freetype-2.4.10.tar.gz 
tar zxvf freetype-2.4.10.tar.gz

cd freetype-2.4.10

./configure

make install

ln -s /usr/local/lib/freetype* /lib/

ln -s /usr/local/lib/freetype* /lib64/

cd ..

#安装GraphicsMagick

cd GraphicsMagick-1.3.18

./configure --prefix=/usr/local/graphicsmagick-1.3.18

make

make install

#编辑profile文件

vi /etc/profile

#末尾增加以下内容

export GMAGICK_HOME="/usr/local/graphicsmagick-1.3.18"

export PATH="$GMAGICK_HOME/bin:$PATH"

LD_LIBRARY_PATH=$GMAGICK_HOME/lib:$LD_LIBRARY_PATH

export LD_LIBRARY_PATH

#保存退出,运行以下命令立即生效配置

source /etc/profile


2、用法下载libjpeg、libpng、freetype链接如失效可访问:http://www.filewatcher.com/m/libjpeg-6b.tar.gz.961981-0.html

原始图片是input.jpg,尺寸:160×120

1)只缩小不放大

gm convert input.jpg -resize "500x500>" output_1.jpg

加了>,表示只有当图片的宽与高,大于给定的宽与高时,才进行“缩小”操作。

生成的图片大小是:160×120,未进行操作
如果不加>,会导致图片被比等放大。

2)等比缩图 (缺点:产生白边)

gm convert input.jpg -thumbnail "100x100" output_1.jpg
生成的图片大小是:100×75

3)非等比缩图,按给定的参数缩图(缺点:长宽比会变化)

gm convert input.jpg -thumbnail "100x100!" output_2.jpg
生成的图片大小是:100×100

4)裁剪后保证等比缩图 (缺点:裁剪了图片的一部分)

gm convert input.jpg -thumbnail "100x100^" -gravity center -extent 100x100 output_3.jpg
生成的图片大小是:100×100,还保证了比例。不过图片经过了裁剪,剪了图片左右两边才达到1:1

5)填充后保证等比缩图 (缺点:要填充颜色,和第一种方法基本一样)

gm convert input.jpg -thumbnail "100x100" -background gray -gravity center -extent 100x100 output_4.jpg
生成的图片大小是:100×100,还保证了比例,同时没有对图片进行任何裁剪,缺失的部分按指定颜色进行填充。

6)裁剪、填充相结合 (缺点:最差的方法)

1
gm convert input.jpg -thumbnail "10000@ -background gray -gravity center -extent 100x100 output_5.jpg
生成的图片大小是:100×100,这次保证了大小和比例,其中的10000就是100×100的乘积,同时在填充和裁剪之间做了一个平衡。

7)位深度32 转为24
IE6,7,8不支持显示“位深度32”的图片,但IE9、火狐、谷歌浏览器就可以显示。
使用GM,把“位深度32”的图片转换为“位深度24”的图片
输入图片zzz.jpg就是“位深度32”的图片,输出图片 zzz_out.jpg就是“位深度24”的图片

1
gm convert -resize 100x100 -colorspace RGB zzz.jpg zzz_out.jpg
转完后,图片的颜色会有轻微变化。

更多请参考:http://elf8848.iteye.com/blog/382528

五、相关配置及脚本
1、Nginx配置文件

user  www www;

worker_processes 1;

# 日志级别调试时可设为notice,生产环境请设为error

error_log  /usr/local/openresty/nginx/logs/error.log notice;

events

{

use epoll;

worker_connections 51200;

}

http

{

lua_package_path '/usr/local/openresty/nginx/lua/?.lua;;';

server {

listen       80;

server_name  img.hopesoft.org;

root  /home/wwwroot/img.hopesoft.org;

#/thumbnail目录下的图片请求不经过缩略图模块

location ^~ /thumbnail/ {

}

#对类似_100x100.gif/jpg/png/jpeg进行缩略图处理

location ~* _([0-9]+)x([0-9]+)\.(gif|jpg|png|jpeg)$ {                   #匹配文件名规则

root  /home/wwwroot/img.hopesoft.org;                             #站点根目录

set $image_root /home/wwwroot/img.hopesoft.org;                   #图片目录

set $thumbnail_root /home/wwwroot/img.hopesoft.org/thumbnail;     #缩略图存放目录

#如果缩略图文件存在,直接返回

set $file $thumbnail_root$uri;

if (-f $file) {

rewrite ^/(.*)$ /thumbnail/$1 last;

}

#如果缩略图文件不存在,则应用缩略图模块处理

if (!-f $file) {

rewrite_by_lua_file lua/thumbnail.lua;

}

}

}

include vhost/*.conf;

}


2、Lua脚本
/usr/local/openresty/nginx/lua/thumbnail.lua

-- nginx thumbnail module

-- last update : 2014/8/21

-- version     : 0.4.1

local c  = require 'config'

--[[

uri               :链接地址,如/goods/0007/541/001_328x328.jpg

ngx_img_root      :图片根目录

ngx_thumbnail_root:缩略图根目录

img_width         :缩略图宽度

img_width         :缩略图高度

img_size          :缩略图宽x高

img_crop_type     :缩略图裁剪类型

cur_uri_reg_model :缩略图uri正则规则

]]

local uri = ngx.var.uri

local ngx_img_root = ngx.var.image_root

local ngx_thumbnail_root = ngx.var.thumbnail_root

local img_width,img_height,img_size,img_crop_type = 0

local cur_uri_reg = c.default_uri_reg

--[[

日志函数

log_level: 默认为ngx.NOTICE

取值范围:ngx.STDERR , ngx.EMERG , ngx.ALERT , ngx.CRIT , ngx.ERR , ngx.WARN , ngx.NOTICE , ngx.INFO , ngx.DEBUG

请配合nginx.conf中error_log的日志级别使用

]]

function lua_log(msg,log_level)

log_level = log_level or c.lua_log_level

if(c.enabled_log) then

ngx.log(log_level,msg)

end

end

-- 匹配链接对应缩略图规则

function table.contains(table,element)

local i = 1

img_crop_type = 0

for _, value in pairs(c.cfg) do

local dir = value['dir']

local sizes = value['sizes']

local uri_reg = value['uri_reg']

_,_,img_width,img_height = string.find(uri,''..dir..'+.*_([0-9]+)x([0-9]+)')

if(img_width and img_height and img_crop_type==0) then

img_size = img_width..'x'..img_height

for _, value in pairs(sizes) do

if(uri_reg) then

lua_log('value[uri_reg]==='..uri_reg)

else

lua_log('value[uri_reg]===nil,dir='..dir..',cur_uri_reg='..cur_uri_reg)

end

cur_uri_reg = uri_reg or cur_uri_reg

if (img_size == value) then

img_crop_type=1

return true

elseif (img_size..'_' == value) then

img_crop_type=2

return true

elseif (img_size..'!' == value) then

img_crop_type=3

return true

elseif (img_size..'^' == value) then

img_crop_type=4

return true

elseif (img_size..'>' == value) then

img_crop_type=5

return true

elseif (img_size..'$' == value) then

img_crop_type=6

img_size = img_width..'x'

return true

end

end

end

i=i+1

end

return false

end

-- 拼接gm命令

local function generate_gm_command(img_crop_type,img_original_path,img_size,img_thumbnail_path)

local cmd = c.gm_path .. ' convert ' .. img_original_path

if (img_crop_type == 1) then

cmd = cmd .. ' -thumbnail '  .. img_size .. ' -background ' .. c.img_background_color .. ' -gravity center -extent ' .. img_size

elseif (img_crop_type == 2) then

cmd = cmd .. ' -thumbnail '  .. img_size

elseif (img_crop_type == 3) then

cmd = cmd .. ' -thumbnail "'  .. img_size .. '!" -extent ' .. img_size

elseif (img_crop_type == 4) then

cmd = cmd .. ' -thumbnail "'  .. img_size .. '^" -extent ' .. img_size

elseif (img_crop_type == 5 or img_crop_type == 6) then

cmd = cmd .. ' -resize "'  .. img_size .. '>"'

else

lua_log('img_crop_type error:'..img_crop_type,ngx.ERR)

ngx.exit(404)

end

cmd = cmd .. ' ' .. img_thumbnail_path

return cmd

end

lua_log("ngx_thumbnail_root======="..ngx_thumbnail_root)

if not table.contains(c.cfg, uri) then

lua_log(uri..' is not match!',ngx.ERR)

ngx.exit(404)

else

lua_log(uri..' is match!')

local img_original_uri = string.gsub(uri, cur_uri_reg, '')

lua_log('img_original_uri_old===' .. uri)

lua_log('cur_uri_reg===' .. cur_uri_reg)

lua_log('img_original_uri_new===' .. img_original_uri)

local img_exist=io.open(ngx_img_root .. img_original_uri)

if not img_exist then

if not c.enabled_default_img then

lua_log(img_original_uri..' is not exist!',ngx.ERR)

ngx.exit(404)

else

img_exist=io.open(ngx_img_root ..  c.default_img_uri)

if img_exist then

lua_log(img_original_uri .. ' is not exist! crop image with default image')

img_original_uri = c.default_img_uri

else

lua_log(img_original_uri..' is not exist!',ngx.ERR)

ngx.exit(404)

end

end

end

local img_original_path  = ngx_img_root .. img_original_uri

local img_thumbnail_path = ngx_thumbnail_root .. uri

local gm_command         = generate_gm_command(img_crop_type,img_original_path,img_size,img_thumbnail_path)

if (gm_command) then

lua_log('gm_command======'..gm_command)

_,_,img_thumbnail_dir,img__thumbnail_filename=string.find(img_thumbnail_path,'(.-)([^/]*)$')

os.execute('mkdir -p '..img_thumbnail_dir)

os.execute(gm_command)

end

ngx.req.set_uri('/thumbnail'..uri)

end


/usr/local/openresty/nginx/lua/config.lua

-- nginx thumbnail module

-- last update : 2014/8/21

-- version     : 0.4.1

module(...,package.seeall)

--[[

enabled_log: 是否打开日志

lua_log_level: 日志记录级别

gm_path: graphicsmagick安装目录

img_background_color: 填充背景色

enabled_default_img: 是否显示默认图片

default_img_uri: 默认图片链接

default_uri_reg: 缩略图正则匹配模式,可自定义

_[0-9]+x[0-9] 对应:001_100x100.jpg

_[0-9]+x[0-9]+[.jpg|.png|.gif]+ 对应:001.jpg_100x100.jpg

]]

enabled_log = true

lua_log_level        = ngx.NOTICE

gm_path = '/usr/local/graphicsmagick-1.3.18/bin/gm'

img_background_color = 'white'

enabled_default_img  = true

default_img_uri = '/default/notfound.jpg'

default_uri_reg      = '_[0-9]+x[0-9]+'

--[[

配置项,对目录、缩略图尺寸、裁剪类型进行配置,匹配后才进行缩略图处理

1.sizes={'350x350'} 填充后保证等比缩图

2.sizes={'300x300_'}等比缩图

3.sizes={'250x250!'}非等比缩图,按给定的参数缩图(缺点:长宽比会变化)

4.sizes={'50x50^'}裁剪后保证等比缩图 (缺点:裁剪了图片的一部分)

5.sizes={'100x100>'}只缩小不放大

6.sizes={'140x140$'}限制宽度,只缩小不放大(比如网页版图片用于手机版时)

dir="/"       对应根目录,请放在default之前

dir="default" 对应默认图片尺寸,当原图不存在时,请求该尺寸会以默认图片生成缩略图

]]

cfg = {

{

dir   = 'photos',

sizes = {'50x50^','100x100>','140x140$','250x250!','300x300_','350x350'},

},

{ dir   = 'avatars',

sizes = {'50x50^','80x80'},

},

{

dir      = 'mall',

sizes    = {'130x130!','228x228!','420x420!'},

uri_reg  = '_[0-9]+x[0-9]+[.jpg|.png|.gif]+',

},

{ dir   = 'default',

sizes = {'50x50^','100x100>','140x140$','250x250!','300x300_','350x350','80x80'},

}

}


我们的文件上传和删除由web程序来完成,当web程序删除原始文件时,需要同时删除缩略图,我们使用Linux的inotify来监控原始文件的变化,当有文件修改或删除时,删除相应的缩略图(注:inotify需要linux的内核版本大于等于2.6.13)。六、监控脚本

脚本文件/home/shell/monitor_thumbnail.sh内容如下:

#!/bin/bash

basedir=/home/wwwroot/img.hopesoft.org/

hostname=img.hopesoft.org

thumbnaildir=thumbnail

excludedir=^${basedir}${thumbnaildir}

/usr/local/bin/inotifywait --exclude $excludedir -mrq --timefmt '%d/%m/%y %H:%M' --format '%T %w%f %e' --event delete,modify  ${basedir} | while read  date time file event

do

if [ "${file##*.}" = "jpg" -o "${file##*.}" = "jpeg"  -o "${file##*.}" = "gif" -o "${file##*.}" = "png" ];then

case $event in(DELETE|MODIFY)

tmpfile=${file/$hostname/$hostname\/$thumbnaildir};

filelist=${tmpfile%.*}_*.${tmpfile##*.};

for File in $filelist; do

#echo "rm -rf "$File;

rm -rf $File

done

;;

esac

fi

done


#加入启动

vi /etc/rc.d/rc.local

#增加下行

nohup /home/script/delete_thumbnail.sh &


七、参考文档关于inotify的更多资料,请参考:http://www.1987.name/637.html

1、OpenResty官网
http://openresty.com/cn/index.html
2、HttpLuaModule模块介绍
http://wiki.nginx.org/HttpLuaModule
3、[老王]Nginx与Lua
http://huoding.com/2012/08/31/156
4、[老王]尝试使用GraphicsMagick的缩略图功能
http://hi.baidu.com/thinkinginlamp/item/753d86383545d10fcfb9fe14
5、GraphicsMagick安装及使用
http://www.cnblogs.com/javapro/archive/2013/04/28/3048393.html
6、Lua程序设计
http://book.luaer.cn/
7、灵活自定义缩略图片尺寸大小方案分享(nginx,lua_nginx,GraphicsMagick)
http://www.iteye.com/topic/1125126
8、揭​秘​淘​宝​2​8​6​亿​海​量​图​片​存​储​与​处​理​架​构
http://wenku.baidu.com/view/7dc77b2e7375a417866f8ff0.html
9、django-nginx-image
https://github.com/adw0rd/django-nginx-image
10、Lua中的正则表达式
http://blog.sina.com.cn/s/blog_512f462201016u3b.html

八、示例图片(图片来自老王博客)

原始图片(160×120)



1、填充后保证等比缩图(sizes={‘350×350’} )



2、等比缩图(sizes={‘300x300_’})



3、非等比缩图,按给定的参数缩图,长宽比会变化(sizes={‘250×250!’})



4、裁剪后保证等比缩图,裁剪了图片的一部分(sizes={’50×50^’})



5、只缩小不放大(sizes={‘100×100>’})



6、限制宽度,只缩小不放大(比如网页版图片用于手机版时)(sizes={‘140×140$’})



原图不存在时
1、当配置显示默认图片,且路径及缩略图尺寸符合时,用默认图片填充



2、当配置不显示默认图片或路径及缩略图尺寸不符合时,返回404




九、Github
项目地址:https://github.com/hopesoft/nginx-lua-image-module
问题反馈:https://github.com/hopesoft/nginx-lua-image-module/issues
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息