1. 概述
在 Nginx 中每次 HTTP 请求时执行 Shell 脚本,是一个看似简单但实现起来颇具挑战的任务。因为 Nginx 本身并不直接支持执行外部命令,但我们可以通过一些变通方法来实现,具体取决于实际需求。
常见的使用场景包括触发嵌入式设备上的 LED 指示灯、记录自定义指标、清除缓存或发送通知等。不同实现方式在性能和安全性方面各有优劣。本文将介绍三种常用方式:
- 使用 Lua 模块直接执行
- 通过 FastCGI(如 fcgiwrap)执行脚本
- 利用 mirror 模块异步执行
2. 面临的挑战
Nginx 是一个事件驱动、非阻塞的 Web 服务器,它的设计初衷是避免在核心配置中直接创建进程或执行外部命令,以保证性能和稳定性。
在每次请求中执行 Shell 脚本,会带来以下几个问题:
✅ 性能开销大:每个请求都创建一个新进程会显著影响性能。
✅ 安全风险高:执行任意 Shell 命令可能引入安全漏洞。
✅ 错误处理复杂:脚本出错或超时可能导致请求阻塞甚至服务崩溃。
尽管如此,仍有一些实际场景需要这种能力。接下来我们来看几种可行的实现方式。
3. 使用 HttpLuaModule 执行脚本
Lua 是一种轻量级脚本语言,通过 HttpLuaModule
(也称 lua-nginx-module
),我们可以在 Nginx 中嵌入 Lua 代码,从而实现执行 Shell 脚本的能力。
3.1. 安装 HttpLuaModule
在 Ubuntu/Debian 上安装:
sudo apt-get install libnginx-mod-http-lua
或者安装包含 Lua 模块的 nginx-extras:
sudo apt-get install nginx-extras
在 CentOS/RHEL 上:
sudo yum install nginx-mod-http-lua
如果系统不支持包管理安装,可以考虑使用 OpenResty,它内置了 Lua 支持。
安装完成后,在 nginx.conf
中加载模块:
load_module modules/ngx_http_lua_module.so;
验证配置是否正确:
nginx -t
3.2. Lua 执行 Shell 脚本示例
以下是一个简单的 Lua 脚本执行配置:
location /trigger {
content_by_lua_block {
os.execute("/path/to/our/script.sh")
ngx.say("Script executed")
}
}
访问 /trigger
接口时,Nginx 会同步执行指定脚本并返回结果:
Script executed
如果需要获取脚本输出,可以使用 io.popen()
:
location /run-script {
content_by_lua_block {
local handle = io.popen("date +'%Y-%m-%d %H:%M:%S'")
local result = handle:read("*a")
handle:close()
ngx.header.content_type = "text/plain"
ngx.say("Current time: ", result)
}
}
该配置执行 date
命令并返回当前时间:
Current time: 2025-06-15 14:30:45
也可以传递请求参数:
location /process {
content_by_lua_block {
local args = ngx.var.arg_param or "default"
local cmd = string.format("echo 'Processing: %s'", args)
local handle = io.popen(cmd)
local result = handle:read("*a")
handle:close()
ngx.say(result)
}
}
访问 /process?param=test
返回:
Processing: test
3.3. 注意事项
os.execute()
的标准输出会被丢弃,标准错误会写入 Nginx 错误日志。- Lua 执行是同步的,脚本执行时间过长会影响请求响应。
- 可以通过
lua_socket_connect_timeout
等参数控制超时。 - 需要对脚本执行进行异常处理,避免 Nginx 工作进程崩溃。
4. 使用 FastCGI 和 fcgiwrap
FastCGI 是一种传统的脚本执行方式,通过 fcgiwrap
我们可以将 Shell 脚本作为 CGI 程序运行。
4.1. 安装 fcgiwrap
Ubuntu/Debian:
sudo apt-get install fcgiwrap
安装完成后,fcgiwrap
通常会以 systemd 服务形式运行,并创建 Unix 套接字 /var/run/fcgiwrap.socket
。
检查服务状态:
sudo systemctl status fcgiwrap
输出示例:
● fcgiwrap.service - Simple CGI Server
Loaded: loaded (/usr/lib/systemd/system/fcgiwrap.service; indirect; preset: enabled)
Active: active (running) since Sat 2025-06-14 12:09:14 UTC; 4s ago
TriggeredBy: ● fcgiwrap.socket
Main PID: 4914 (fcgiwrap)
Tasks: 1 (limit: 9366)
Memory: 320.0K (peak: 584.0K)
CPU: 2ms
CGroup: /system.slice/fcgiwrap.service
└─4914 /usr/sbin/fcgiwrap -f
确认服务正常运行。
4.2. 配置 Nginx 支持 FastCGI
Nginx 配置示例:
location ~ \.(sh|pl|py)$ {
gzip off;
root /var/www/scripts;
fastcgi_pass unix:/var/run/fcgiwrap.socket;
include /etc/nginx/fastcgi_params;
fastcgi_param DOCUMENT_ROOT /var/www/scripts;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
创建一个 Shell 脚本 /var/www/scripts/info.sh
:
#!/bin/bash
echo "Content-type: text/html"
echo ""
echo "<html><body>"
echo "<h1>Request received at $(date)</h1>"
echo "<p>Client IP: $REMOTE_ADDR</p>"
echo "<p>Request URI: $REQUEST_URI</p>"
echo "</body></html>"
设置可执行权限:
sudo chmod 755 /var/www/scripts/info.sh
还可以创建 Python 脚本处理 POST 请求(/var/www/scripts/process.py
):
#!/usr/bin/env python3
import sys
import os
print("Content-type: text/plain\n")
print(f"Method: {os.environ.get('REQUEST_METHOD', 'Unknown')}")
print(f"Query: {os.environ.get('QUERY_STRING', 'None')}")
if os.environ.get('REQUEST_METHOD') == 'POST':
content_length = int(os.environ.get('CONTENT_LENGTH', 0))
post_data = sys.stdin.read(content_length)
print(f"POST data: {post_data}")
同样设置可执行权限:
sudo chmod 755 /var/www/scripts/process.py
该方法适用于多语言脚本支持,也适合迁移旧的 CGI 应用。
5. 使用 Nginx Mirror 模块异步执行
与前面两种方式不同,mirror
模块用于创建异步的后台请求。这种方式适用于不需要返回脚本执行结果的场景,例如记录日志、采集指标或触发后台任务。
5.1. Mirror 模块原理
Mirror 模块会为指定 URI 创建后台子请求,这些请求的响应会被忽略,主请求不会等待它们完成。这非常适合“触发即忘”(fire-and-forget)的场景。
5.2. 配置 Mirror 模块执行脚本
Nginx 配置示例:
location / {
mirror /mirror;
mirror_request_body off;
proxy_pass http://backend;
}
location = /mirror {
internal;
proxy_pass http://localhost:8888/execute-script;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
}
上述配置为所有访问 /
的请求创建一个后台子请求到 /mirror
,该路径设置为 internal
,不能直接访问。
创建一个简单的 Python 服务监听 8888 端口,用于执行脚本:
#!/usr/bin/env python3
from http.server import HTTPServer, BaseHTTPRequestHandler
import subprocess
import threading
class ScriptHandler(BaseHTTPRequestHandler):
def do_GET(self):
self._handle_request()
def do_POST(self):
self._handle_request()
def _handle_request(self):
original_uri = self.headers.get('X-Original-URI', '')
def run_script():
subprocess.run(['/opt/scripts/log-request.sh', original_uri])
thread = threading.Thread(target=run_script)
thread.daemon = True
thread.start()
self.send_response(200)
self.end_headers()
self.wfile.write(b'OK')
if __name__ == '__main__':
server = HTTPServer(('localhost', 8888), ScriptHandler)
print("Script server listening on port 8888")
server.serve_forever()
对应的 Shell 脚本 /opt/scripts/log-request.sh
:
#!/bin/bash
echo "$(date): Request to $1" >> /var/log/nginx-requests.log
设置可执行权限:
sudo chmod 755 /opt/scripts/log-request.sh
这种方式确保脚本不会阻塞主请求,适用于高并发场景。
6. 总结
本文介绍了三种在 Nginx 中每次请求时执行 Shell 脚本的方法:
方法 | 优点 | 缺点 |
---|---|---|
Lua 模块 | 可直接执行脚本并获取输出 | 同步执行可能影响性能 |
FastCGI / fcgiwrap | 支持多种语言、兼容 CGI | 配置稍复杂,性能不如 Lua |
Mirror 模块 | 异步执行,不影响主请求 | 无法获取脚本输出 |
选择哪种方式取决于具体场景:
- ✅ 需要脚本输出:使用 Lua
- ✅ 多语言支持或迁移 CGI:使用 FastCGI
- ✅ 后台日志记录或指标采集:使用 Mirror 模块
⚠️ 注意:每次请求执行脚本都会带来性能和安全风险,务必做好限流、超时控制和输入校验。