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 模块

⚠️ 注意:每次请求执行脚本都会带来性能和安全风险,务必做好限流、超时控制和输入校验。


原始标题:Run a Shell Script on Every Request in Nginx