nginx-upload-module模块实现文件上传(multipart/form-data和断点续传)

前言

有时候我们想简单实现文件上传的功能,又不想使用额外的语言(比如PHP、Java),或者想实现文件的断点续传。这个时候Nginx的一个模块nginx-upload-module就能满足我们的需求。

模块安装

下载模块:

  1. cd /tmp
  2. wget https://codeload.github.com/vkholodkov/nginx-upload-module/zip/2.2
  3. unzip 2.2

安装模块:

  1. .configure –add-module=/tmp/nginx-upload-module-2.2/

multipart/form-data表单上传示例

nginx.conf配置:

  1. server {
  2. […]
  3.         location /upload {
  4.                 upload_pass @uploadHandler;
  5.                 upload_store /usr/local/nginx/upload_temp 1;
  6.                 upload_set_form_field $upload_field_name.path "$upload_tmp_path";
  7.         }
  8.  
  9.         location @uploadHandler {
  10.                 proxy_pass http://backend-host;
  11.         }
  12. […]
  13. }

这里在server里定义了upload location,这个location是上传的接口,还有@uploadHandler location,是当文件上传完成后,nginx模块会对这个location发送一些必要的信息,如文件上传的路径。
这里涉及了几个指令:

  • upload_pass @uploadHandler: 上传完成后会发送必要的数据到@uploadHandler;
  • upload_store /usr/local/nginx/upload_temp 1: 文件上传的临时目录;
  • upload_set_form_field $upload_field_name.path “$upload_tmp_path”: 设置文件上传完成后,把文件临时路径发送给upload_pass指定的location。

断点续传示例

nginx.conf配置

  1. server {
  2. […]
  3.         location /resumable_upload {
  4.                 upload_resumable on;
  5.                 upload_state_store /usr/local/nginx/upload_temp ;
  6.                 upload_pass @drivers_upload_handler;
  7.                 upload_store /usr/local/nginx/upload_temp;
  8.                 upload_set_form_field $upload_field_name.path "$upload_tmp_path";
  9.         }
  10.  
  11.         location @resumable_upload_handler {
  12.                 proxy_pass http://localhost:8002;
  13.         }
  14. […]
  15. }

与上一步multipart/form-data表单上传示例配置不同的地方有:

  • upload_resumable on: 开启断点续传功能;
  • upload_state_store /usr/local/nginx/upload_temp: 设置断点续传状态文件存储的目录。

上传文件第一个片段

  1. POST /upload HTTP/1.1
  2. Host: example.com
  3. Content-Length: 51201
  4. Content-Type: application/octet-stream
  5. Content-Disposition: attachment; filename="big.TXT"
  6. X-Content-Range: bytes 0-51200/511920
  7. Session-ID: 1111215056
  8.  
  9. <0-51200的字节文件数据>

上传文件第一个片段服务器响应

  1. HTTP/1.1 201 Created
  2. Date: Thu, 02 Sep 2010 12:54:40 GMT
  3. Content-Length: 14
  4. Connection: close
  5. Range: 0-51200/511920
  6.  
  7. 0-51200/511920

上传文件最后一个片段

  1. POST /upload HTTP/1.1
  2. Host: example.com
  3. Content-Length: 51111
  4. Content-Type: application/octet-stream
  5. Content-Disposition: attachment; filename="big.TXT"
  6. X-Content-Range: bytes 460809-511919/511920
  7. Session-ID: 1111215056
  8.  
  9. <460809-511919字节文件数据>

上传文件最后一个片段服务器响应

  1. HTTP/1.1 200 OK
  2. Date: Thu, 02 Sep 2010 12:54:43 GMT
  3. Content-Type: text/html
  4. Connection: close
  5. Content-Length: 2270
  6.  
  7. <响应的内容>

请求头说明

请求头 说明
Content-Disposition attachment, filename=“上传的文件名”
Content-Type 待上传文件的mime type,如application/octet-stream(注:不能为multipart/form-data)
X-Content-Range 待上传文件字节范围,如第一片段bytes 0-51200/511920,最后一个片段bytes 460809-511919/511920(注:文件第一个字节标号为0,最后一个字节标号为n-1,其中n为文件字节大小)
X-Session-ID 上传文件的标识,由客户端随机指定.因为是断点续传,客户端必须确保同一个文件的所有片段上传标识一致
Content-Length 上传片段的大小

Python上传demo

  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*- 
  3.  
  4.  
  5. import os.path
  6. import requests
  7. import hashlib
  8.  
  9. # 待上传文件路径
  10. FILE_UPLOAD = "/tmp/testfile"
  11. # 上传接口地址
  12. UPLOAD_URL = "http://host/drivers_upload"
  13. # 单个片段上传的字节数
  14. SEGMENT_SIZE = 1048576
  15.  
  16. def upload(fp, file_pos, size, file_size):
  17.     session_id = get_session_id()
  18.     fp.seek(file_pos)
  19.     payload = fp.read(size)
  20.     content_range = "bytes {file_pos}-{pos_end}/{file_size}".format(file_pos=file_pos,
  21.                     pos_end=file_pos+size-1,file_size=file_size)
  22.     headers = {‘Content-Disposition’: ‘attachment; filename="big.TXT"’,’Content-Type’: ‘application/octet-stream’,
  23.                 ‘X-Content-Range’:content_range,’Session-ID’: session_id,’Content-Length’: size}
  24.     res = requests.post(UPLOAD_URL, data=payload, headers=headers)
  25.     print(res.text)
  26.  
  27.  
  28. # 根据文件名hash获得session id
  29. def get_session_id():
  30.     m = hashlib.md5()
  31.     file_name = os.path.basename(FILE_UPLOAD)
  32.     m.update(file_name)
  33.     return m.hexdigest()
  34.  
  35. def main():
  36.     file_pos = 0
  37.     file_size = os.path.getsize(FILE_UPLOAD)
  38.     fp = open(FILE_UPLOAD,"r")
  39.  
  40.     while True:
  41.         if file_pos + SEGMENT_SIZE >= file_size:
  42.             upload(fp, file_pos, file_size – file_pos, file_size)
  43.             fp.close()
  44.             break
  45.         else:
  46.             upload(fp, file_pos, SEGMENT_SIZE, file_size)
  47.             file_pos = file_pos + SEGMENT_SIZE
  48.  
  49. if __name__ == "__main__":
  50.     main()

参考

https://www.nginx.com/resources/wiki/modules/upload/
http://www.grid.net.ru/nginx/upload.en.html

openresty(nginx lua)统计域名状态码、平均响应时间和流量

背景

 

之前我们统计域名状态码、平均响应时间和流量的方法是:在每台机器添加一个定时脚本,来获取每个域名最近一分钟的访问日志到临时文件。然后zabbix再对这个一分钟日志临时文件作相关统计。一直运行良好,最近发现某台服务器突然负载增高。使用iotop查看发现获取最近一分钟日志的脚本占用的IO特别高。停止这个定时任务之后恢复正常。于是就打算使用nginx lua来替换目前的方法。新的方法具有统计时占用资源少,实时的特点。

 

方法介绍

 

使用nginx lua统计网站相关数据的方法为(我们以统计devops.webres.wang 404状态码为例):
记录过程:

  • 1、定义了一个共享词典access,获取当前时间戳,获取当前域名,如devops.webres.wang;
  • 2、我们定义用来存储状态码的词典key为,devops.webres.wang-404-当前时间戳;
  • 3、自增1 key(devops.webres.wang-404-当前时间戳)的值;
  • 4、循环2,3步。

 

查询过程:
提供一个接口,来累加key为devops.webres.wang-404-(前60秒时间戳-当前时间戳)的值,返回结果。

 

方法实现

 

nginx.conf设置

  1. http {
  2. […]
  3. lua_shared_dict access 10m;
  4. log_by_lua_file conf/log_acesss.lua;
  5. server {
  6. […]
  7.    location /domain_status {
  8.         default_type text/plain;
  9.         content_by_lua_file "conf/domain_status.lua";
  10.     }
  11. […]
  12. }
  13. […]
  14. }

 

log_access.lua

  1. local access = ngx.shared.access
  2. local host = ngx.var.host
  3. local status = ngx.var.status
  4. local body_bytes_sent = ngx.var.body_bytes_sent
  5. local request_time = ngx.var.request_time
  6. local timestamp = os.date("%s")
  7. local expire_time = 70
  8.  
  9. local status_key = table.concat({host,"-",status,"-",timestamp})
  10. local flow_key = table.concat({host,"-flow-",timestamp})
  11. local req_time_key = table.concat({host,"-reqt-",timestamp})
  12. local total_req_key = table.concat({host,"-total_req-",timestamp})
  13.  
  14. — count total req
  15. local total_req_sum = access:get(total_req_key) or 0
  16. total_req_sum = total_req_sum + 1
  17. access:set(total_req_key, total_req_sum, expire_time)
  18.  
  19. — count status
  20. local status_sum = access:get(status_key) or 0
  21. status_sum = status_sum + 1
  22. access:set(status_key, status_sum, expire_time)
  23.  
  24. — count flow
  25. local flow_sum = access:get(flow_key) or 0
  26. flow_sum = flow_sum + body_bytes_sent
  27. access:set(flow_key, flow_sum, expire_time)
  28.  
  29. — count request time
  30. local req_sum = access:get(req_time_key) or 0
  31. req_sum = req_sum + request_time
  32. access:set(req_time_key, req_sum, expire_time)

domain_status.lua

 

  1. local access = ngx.shared.access
  2. local args = ngx.req.get_uri_args()
  3. local count = args["count"]
  4. local host = args["host"]
  5. local status = args["status"]
  6. local one_minute_ago = tonumber(os.date("%s")) – 60
  7. local now = tonumber(os.date("%s"))
  8.  
  9. local status_total = 0
  10. local flow_total = 0
  11. local reqt_total = 0
  12. local req_total = 0
  13.  
  14. if not host then
  15.         ngx.print("host arg not found.")
  16.         ngx.exit(ngx.HTTP_OK)
  17. end
  18.  
  19. if count == "status" and not status then
  20.         ngx.print("status arg not found.")
  21.         ngx.exit(ngx.HTTP_OK)
  22. end
  23.  
  24. if not (count == "status" or count == "flow" or count == "reqt") then
  25.         ngx.print("count arg invalid.")
  26.         ngx.exit(ngx.HTTP_OK)
  27. end
  28.  
  29. for second_num=one_minute_ago,now do
  30.         local flow_key = table.concat({host,"-flow-",second_num})
  31.         local req_time_key = table.concat({host,"-reqt-",second_num})
  32.         local total_req_key = table.concat({host,"-total_req-",second_num})
  33.  
  34.         if count == "status" then
  35.                 local status_key = table.concat({host,"-",status,"-",second_num})
  36.                 local status_sum = access:get(status_key) or 0
  37.                 status_total = status_total + status_sum
  38.         elseif count == "flow" then
  39.                 local flow_sum = access:get(flow_key) or 0
  40.                 flow_total = flow_total + flow_sum
  41.         elseif count == "reqt" then
  42.                 local req_sum = access:get(total_req_key) or 0
  43.                 local req_time_sum = access:get(req_time_key) or 0
  44.                 reqt_total = reqt_total + req_time_sum
  45.                 req_total = req_total + req_sum
  46.         end
  47. end
  48.  
  49. if count == "status" then
  50.         ngx.print(status_total)
  51. elseif count == "flow" then
  52.         ngx.print(flow_total)
  53. elseif count == "reqt" then
  54.         if req_total == 0 then
  55.                 reqt_avg = 0
  56.         else
  57.                 reqt_avg = reqt_total/req_total
  58.         end
  59.         ngx.print(reqt_avg)
  60. end

使用说明

1、获取域名状态码
如请求devops.webres.wang一分钟内404状态码数量
请求接口http://$host/domain_status?count=status&host=devops.webres.wang&status=404
2、获取域名流量
请求接口http://$host/domain_status?count=flow&host=devops.webres.wang
3、获取域名一分钟内平均响应时间
请求接口http://$host/domain_status?count=reqt&host=devops.webres.wang