用Docker+Nginx+Redis搭建个文档服务器(1)

习惯上只是简单把 Nginx 用作 WebServer,最近看了看它的插件,发现其实有很多玩法,特别是很多工作可以不再需要写代码了,通过安装配置插件就可以实现。本文以搭建一个文档服务器为例,演示一下怎样使用 Nginx 插件。

项目概述

本项目尝试用Nginx的多个模块搭建一个文档服务器,实现文件的上传和浏览。纯粹用Nginx实现是想讲文件上传作为一个独立的基础模块,如果其他业务模块需要文件管理功能,可以直接使用。

image.png

项目地址:https://github.com/jasony62/tms-nginx-finder

安装模块

名称 功能 指令
echo-nginx-module 快速响应内容 echo
nginx-upload-module 上传文件 upload_xxx
njs 用 Javascript 处理业务逻辑 js_xxx
set-misc-nginx-module 在 nginx.conf 文件中处理变量 set_unescape_uri
redis2-nginx-module redis 客户端 redis2_xxx

配置 Nginx

环境参数

环境变量 参数名 说明
REDIS_KEY_UPLOAD_START_TIME $redis_key_upload_start_time Redis 中用于保存服务启动时间的 key
REDIS_KEY_UPLOAD_COUNTER $redis_key_upload_counter Redis 中用于保存上传文件次数的 key
REDIS_CHANNEL_UPLOAD $redis_channel_upload Redis 中接收上传文件信息的 channel
LOCAL_UPLOAD_LOG 本地保存上传文件日志,指定了记,不指定不记

需要在nginx.conf文件中添加如下设置,用env指令说明需要使用的环境变量,用js_set指令创建变量并赋值(没有找到直接在配置文件中使用环境变量的方法)。

env REDIS_KEY_UPLOAD_START_TIME;
env REDIS_KEY_UPLOAD_COUNTER;
env REDIS_CHANNEL_UPLOAD;
js_include /usr/local/nginx/njs/upload.js;
js_set $redis_key_upload_start_time var_redis_key_upload_start_time;
js_set $redis_key_upload_counter var_redis_key_upload_counter;
js_set $redis_channel_upload var_redis_channel_upload;

访问目录

我们希望可以通过 Nginx 直接访问某个目录下的文件,例如:nginx 工作目录下的 files 目录,在nginx.conf中添加如下内容:

location = /files/ {
  root .;
  autoindex on;
}

上传文件

我们用nginx-upload-module插件实现文件上传功能。安装完成后,在nginx.conf中添加如下配置:

location /upload/ {
  # 指定上传文件的存放位置
  upload_store /usr/local/nginx/files;

  # 设置转发的信息
  upload_set_form_field $upload_field_name.name "$upload_file_name";
  upload_set_form_field $upload_field_name.content_type "$upload_content_type";
  upload_set_form_field $upload_field_name.path "$upload_tmp_path";
  upload_aggregate_form_field "$upload_field_name.md5" "$upload_file_md5";
  upload_aggregate_form_field "$upload_field_name.size" "$upload_file_size";

  # 文件上传后转发请求
  upload_pass @upload_response;
  # add_header "Content-Type" "text/html; charset=UTF-8";
  # echo "echo: Upload done";
}

指令upload_pass是文件上传后将文件的基本信息转发到指定地址。接收上传文件信息的地址用的是命名地址(named location),它不是按照正则匹配的,用于内部转发(后面 njs 模块会说明如何实现)。

上传的文件会放在指令upload_store指定的位置,文件是由数字组成的递增的字符,例如:0000000001,0000000002。如果不需要后续处理,可以不用转发上传文件数据,直接通过echo指令返回结果。

处理文件

因为无法指定上传文件的命名(没有扩展名),而且每次重启 Nginx 都会从头开始给文件命名,可能会覆盖已有的文件,这样使用起来不方便,所以希望能够修改上传文件的名字。这里我们使用了njs模块,在nginx.conf中添加如下配置:

js_include /usr/local/nginx/njs/upload.js;
location @upload_response {
  js_content handle;
}

文件的命名由 3 个部分组成:服务启动时间+启动以来上传的文件数加 1+扩展名,例如:20191224_124808_8.mp4。为了实现这个要求,需要记录服务启动时间和上传文件次数。最简单的方法考虑是做成全局变量,但是在 Nginx 中没有找到实现方法。本来想可以把数据写到本地文件中,但是这样应该会有并发读取的问题,所以决定用 Redis 来保存数据。

访问 Redis

Redis 中存放两个数据:1、启动时间;2、计数器。每次上传一个文件就通过启动时间和计数器组合出一个文件名。获取启动时间用get命令,获取计数器用incr命令。利用 redis2-nginx-module 模块可以把 nginx 变成一个 redis 客户端,我们在nginx.conf中添加如下配置:

location = /redis/counter {
  redis2_query get $redis_key_upload_start_time;
  redis2_query incr $redis_key_upload_counter;
  redis2_pass redis:6379;
}

Redis 除了存储共享信息外,我们还用它进行事件通知,每次完成一个文件的处理后,利用 Redis 的发布订阅机制发送一条上传文件的信息,这样如果其他的系统需要进行扩展可以接收这个事件。在nginx.conf中添加如下配置:

location = /redis/publish {
  set_unescape_uri $message $arg_message;
  redis2_query publish $redis_channel_upload $message;
  redis2_pass redis:6379;
}

容器化

为了便于使用,我们将整个项目做成 docker 容器。容器有 3 个:Nginx 容器,Redis 容器和 Redis-cli 容器,其中 Redis-cli 是为了在 Redis 中设置初始值。

设置时区

alpine镜像默认的时区是UTC,和我们差了 8 个小时。前面提到过需要将服务启动时间作为上传文件名的一部分,因此需要把失去调整为Asia/Shanghai。Dockerfile 中添加如下内容:

RUN sed -i 's?http://dl-cdn.alpinelinux.org/?https://mirrors.aliyun.com/?' /etc/apk/repositories && \
    apk add -U tzdata && \
    cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    apk del tzdata

Redis 初始值

我们需要每次服务启动时在 Redis 中记录启动时间,想到最简单的方法 Redis 服务启动后,用redis-cli执行如下命令。

date +'%Y%m%d_%H%M%S' | xargs redis-cli -h redis set uploadBootAt

环境变量

docker-compose.yml文件中指定环境变量的值,如果需要可以通过docker-compose.override.yml文件覆盖。

environment:
  - REDIS_KEY_UPLOAD_START_TIME=upload:start_time
  - REDIS_KEY_UPLOAD_COUNTER=upload:counter
  - REDIS_CHANNEL_UPLOAD=upload:event

其他

volumes:
  - ./nginx/nginx.conf:/usr/local/nginx/conf/nginx.conf:ro
  - ./nginx/html:/usr/local/nginx/html
  - ./nginx/njs:/usr/local/nginx/njs
  - ./upload:/usr/local/nginx/files

这里分享个经验。本来想让文件上传到容器中的tmp目录下,然后再改名到volumes指定的目录下。但是,这样需要在两个device之间移动文件,是不允许的,所以改成了文件直接上传到指定的目录,就地改名。


nginx.conf中设置连接 Redis。

location = /redis/counter {
  redis2_query get $redis_key_upload_start_time;
  redis2_query incr $redis_key_upload_counter;
  redis2_pass redis:6379;
}

这需要两个容器之间进行连接,但事先我们并不知道地址是什么,解决的方法是在docker-compose.yml中进行指定。

Containers for the linked service are reachable at a hostname identical to the alias, or the service name if no alias was specified.

nginx:
  ...
  links:
      - redis

JS 代码说明

nginx.conf文件

env REDIS_KEY_UPLOAD_START_TIME;
js_set $redis_key_upload_start_time var_redis_key_upload_start_time;

njs/upload.js

//
function var_redis_key_upload_start_time(r) {
  return process.env.REDIS_KEY_UPLOAD_START_TIME;
}

通过js_set指令可以解决在nginx.conf文件中无法引用环境变量给变量赋值的问题。


r.subrequest("/redis/counter", { method: "GET" }, function(res) {
  ......
});

subrequest方法可以发起调用,但是不能对命名地址(named location)进行调用。


var fs = require("fs");
fs.renameSync(oFileData.path, newpath);

使用fs模块可以进行简单的文件操作。但是不能用require调用自己编写的模块。

遗留问题

访问控制问题。

多文件上传问题。

大文件续传问题。

参考

https://github.com/nginx/njs

http://nginx.org/en/docs/

https://github.com/openresty

一篇讲解Nginx变量的文章,推荐阅读:https://blog.csdn.net/ok449a6x1i6qq0g660fv/article/details/80276506

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341