基于 images.weserv.nl 和 minio 搭建图片处理服务

在使用阿里云 OSS 对象存储服务时,OSS 提供了图片处理的服务,可以通过在请求图片对象的 url 上携带各种图片处理的参数,如:x-oss-process=image/resize,w_100 等来实现图片的缩放、裁剪、水印、内切圆等等图片处理方式。

阿里云 OSS 图片处理服务指南

类似阿里云的这个图片处理服务,我们也可以通过 MinIO 和一个 开源的图片处理服务项目 images.weserv.nl 来自建一套同时支持图片处理(和部分兼容阿里云图片处理样式)的对象存储系统。

images.weserv.nl 这个开源项目的最新版本 5.x 在 4.x 版本上完全使用 C++ 重写了,大幅度提升了性能,而 4.x 主要是基于 openresty + lua 来实现的,具体的重写原因,主要也是因为 images.serv.nl 的流量请求越来越多,对图片处理的性能要求也越来越高,所以团队从 5.x 之后完全重写了。不过,我们这里的演示仍然是基于 images.weserv.nl 4.x 分支的 🙂

images.weserv.nl支持的图片处理能力比 OSS 的图片处理服务要少一些,不过一些基本的如缩放、裁剪、内切圆等等图片处理操作都还是支持的,可以至 官方参考文档中查看具体支持的图片处理参数

关于 images.weserv.nl 从 5.x 开始重写的原因可以从官方的 API 5 Reference 中看到。

示例部署架构

在正式下手搭建之前,先简单看下总体的结构:

这里的示例是通过 docker 容器来安装部署 images.weserv.nl 和 MinIO,最前端的 Nginx 负责将外部的对象访问请求根据对象类型来转发:

  • 如果为 非图片类型 对象,则默认转发至 MinIO 服务,直接由 MinIO 根据策略返回对戏那个;
  • 如果为 图片对象 ,则转发至 imagesweserv 服务,由 imagesweserv 先从 MinIO 中获取图片并在本地进行图片处理,处理完成后再通过 Nginx 返回给外部请求端;

这个示例架构基本还是很清晰明了的,下面开始搭建示例环境,本文档就忽略了 MinIO 的 docker 容器化搭建步骤,主要记录下 imagesweserv 容器服务和 Nginx 的配置。

搭建imagesweserv服务容器

首先把 images.weserv.nl 4.x 分支的代码 clone 到本地,然后打开 app/config.lua 源码文件,更改如下:

return {
    -- Template options
    template = {
        name = "API 4 - GitHub, DEMO",
        url = "images.weserv.nl",
        args = "",
        example_image = "ory.weserv.nl/lichtenstein.jpg",
        example_transparent_image = "ory.weserv.nl/transparency_demo.png",
        example_smartcrop_image = "ory.weserv.nl/zebra.jpg"
    },

    -- Client options
    client = {
        -- User agent for this client
        user_agent = "Mozilla/5.0 (compatible; ImageFetcher/8.0; +http://images.weserv.nl/)",
        -- Sets the connect timeout thresold, send timeout threshold, and read timeout threshold,
        -- respetively, in milliseconds.
        timeouts = {
            connect = 5000,
            send = 5000,
            read = 15000,
        },
        -- Number describing the max image size to receive (in bytes). Use 0 for no limits.
        max_image_size = 104857600, -- 100 MB
        -- Number describing the maximum number of allowed redirects.
        max_redirects = 10,
        -- Allowed mime types. Use empty table to allow all mime types
        allowed_mime_types = {
            ["image/jpeg"] = "jpg",
            ["image/png"] = "png",
            ["image/gif"] = "gif",
            ["image/bmp"] = "bmp",
            ["image/tiff"] = "tiff",
            ["image/webp"] = "webp",
            ["image/x-icon"] = "ico",
            ["image/vnd.microsoft.icon"] = "ico",
        }
    },

    -- Throttler options
    throttler = {
        -- Redis driver
        redis = {
            scheme = "tcp",
            host = "127.0.0.1",
            port = 6379,
            timeout = 1000, -- 1 sec
            -- The max idle timeout (in ms) when the connection is in the pool
            max_idle_timeout = 10000,
            -- The maximal size of the pool for every nginx worker process
            pool_size = 100
        },
        allowed_requests = 700, -- 700 allowed requests
        minutes = 3, --  In 3 minutes
        prefix = "c", -- Cache key prefix
        whitelist = {
            ["192.168.1.77"] = true,    -- Local IP
            ["127.0.0.1"] = true,       -- Local IP
            ["192.168.1.168"] = true,
        },
        policy = {
            ban_time = 60, -- If exceed, ban for 60 minutes
            cloudflare = {
                enabled = false, -- Is CloudFlare enabled?
                email = "",
                auth_key = "",
                zone_id = "",
                mode = "block" -- The action to apply if the IP get's banned
            }
        }
    }
}

我们根据自己的运行环境来更改其中的配置项:

  • client :配置 imagesweserv 作为客户端向后向的 MinIO 发起获取对象文件的参数;
  • throttler :配置限流控制的策略等相关参数;

更改之后,可以开始构建 imagesweserv 镜像,之后再启动相应的容器:

$ docker build . -t imagesweserv
$ docker run --shm-size=1gb -p 18080:80 -d --name=imagesweserv imagesweserv

配置和启动前端的 Nginx

因为需要编写 lua 脚本来实现对阿里云 oss 样式的解析,因此这里示例基于 Openresty 来搭建前端 Nginx+Lua 环境,具体安装参考 Openresty 官网。这里重点看下 Nginx 的配置以及解析图片处理请求参数的部分代码。

下面为 nginx/conf/conf.d/minio.conf

upstream minio_cluster {
    server 192.168.1.77:19000;
}

server {
    listen 80;
    server_name 192.168.1.77 localhost;

    # To allow special characters in headers
    ignore_invalid_headers off;

    # Allow any size file to be uploaded.
    # Set to a value such as 1000m; to restrict file size to a specific value
    client_max_body_size 100m;

    # To disable buffering
    proxy_buffering off;

    location ~* /kyc/(.*)/(.*).(jpg|jpeg|png|gif|webp)$ {

        set $upstream "";
        rewrite_by_lua_file /usr/local/openresty/nginx/lua/image.lua;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;
        proxy_connect_timeout 30;
        proxy_send_timeout 30;
        proxy_read_timeout 30;

        # Default is HTTP/1, keepalive is only enabled in HTTP/1.1
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        chunked_transfer_encoding off;

        proxy_pass http://$upstream;
    }

    location /kyc {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;
        proxy_connect_timeout 30;
        proxy_send_timeout 30;
        proxy_read_timeout 30;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        chunked_transfer_encoding off;
        proxy_pass http://minio_cluster;
    }
}

该配置中将指定匹配路径的图片请求通过第一个 location section 来处理,而剩余的其他对象请求,均通过后面的 location /kyc 来处理,也就是直接转发给了后面的 minio server cluster。

这里重点看下图片处理的逻辑,从上面的配置中可以看到遇到匹配的图片请求,在 rewrite 阶段由 nginx/lua/image.lua 来处理,下面为示例中的 image.lua 处理脚本:

-- get style, eg: x-oss-process=style/preview1
local img_style = nil
local uri_args = ngx.req.get_uri_args()
for key, val in pairs(uri_args) do
    if key == "x-oss-process" and type(val) == "string" then
        local i, j = string.find(val, "style/")
        if i and j and i == 1 and j == 6 then
            img_style = string.sub(val, 7, -1)
        end
    end
end

if img_style == "preview1" then
    ngx.var.upstream = "192.168.1.77:18080/?url=172.17.0.2:9000" .. ngx.var.uri .. "?q=90&w=600"
    ngx.log(ngx.ERR, "==> upstream of : " .. ngx.var.upstream)
    return ngx.exit(ngx.OK)
end

ngx.var.upstream = "192.168.1.77:18080/?url=172.17.0.2:9000" .. ngx.var.request_uri
ngx.log(ngx.ERR, "==> upstream of : " .. ngx.var.upstream)
return ngx.exit(ngx.OK)

我们这里的示例是演示了一个 oss style 为 preview1 的图片样式,这里是假设该样式要的是图片质量( q )为 90% 和图片宽度( w )最大为 600 像素的图片,在代码的开始处对 x-oss-process=style/xxxx 进行解析,并在后面根据解析出来的样式进行处理,如果为需要处理的样式,则根据样式规则进行转换(为 imagesweserv 支持的图片处理参数);如果不是样式或无法解析的,则直接将请求url转发至 imagesweserv 进行处理。

简单测试验证

首先通过 minio 控制台上传一张图片到 bucket kyc 的根目录 public 中,其中 public 需要设置为公共访问权限。

设置访问策略请参考前一篇文章 《为 MinIO 设置 Nginx 代理以及访问策略的设置》

  • 下面是通过携带 oss style 的 x-oss-process 参数的示例截图:

  • 下图为携带的 weserv.images 的图片处理参数示例:

如果查看 nginx 日志也可以看到通过 image.lua 直接转发给了 imagesweserv