实战:将 Nginx 日志实时解析并写入 MySQL,不再依赖 ELK

最近在做系统监控和日志分析时,遇到一个很现实的问题:我们不想引入 ELK(Elasticsearch + Logstash + Kibana)这么重的架构,但又需要把 Nginx 的访问日志结构化存入数据库,用于后续的业务分析、异常追踪或安全审计。

于是,我决定用一个轻量级方案:用 Bash 脚本实时解析 Nginx 日志,并直接写入 MySQL。整个过程踩了一些坑,也积累了一些经验,今天就来分享一下这个"小而美"的实现思路。


一、Nginx 日志格式定制

首先,得确保 Nginx 输出的日志格式是我们可控的。默认的 combined 格式虽然通用,但缺少一些关键字段,比如后端响应时间、服务端口等。

我在 nginx.conf 中自定义了一个 main 格式:

nginx 复制代码
log_format main '$remote_addr - $remote_user $server_port [$time_local] "$request" '
                '$status $request_time $body_bytes_sent "$http_referer" '
                '"$http_user_agent" "$http_x_forwarded_for"';

access_log /data/log/access.log main;
error_log /data/log/error.log error;

注意这里我用了 $request_time 而不是 $upstream_response_time,因为业务场景中更关心整个请求的耗时(包括网络、排队等),而不仅仅是后端处理时间。


二、日志样例与字段拆解

来看一条真实的日志:

复制代码
172.33.45.11 - - 443 [21/Oct/2025:10:13:59 +0800] "POST /gatewayproxy/api HTTP/1.1" 200 0.253 489 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36 Hutool" "-"

对应字段如下:

字段
$remote_addr 172.33.45.11
$remote_user -(未认证)
$server_port 443
$time_local 21/Oct/2025:10:13:59 +0800
$request POST /gatewayproxy/api HTTP/1.1
$status 200
$request_time 0.253(秒)
$body_bytes_sent 489
$http_referer -
$http_user_agent Mozilla/5.0 ... Hutool
$http_x_forwarded_for -

这里有个细节:$request 是一个复合字段,包含方法、URL 和协议,后续需要拆解。


三、Bash 脚本:实时解析 + 写入 MySQL

1. 数据库表结构

先建好表(MySQL 8.0+):

sql 复制代码
CREATE TABLE nginx_log (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    remote_addr VARCHAR(45),
    remote_user VARCHAR(100),
    server_port INT,
    time_local DATETIME,
    request TEXT,
    request_method VARCHAR(10),
    request_url VARCHAR(2000),
    request_protocol VARCHAR(20),
    status INT,
    request_time DECIMAL(6,3),
    body_bytes_sent INT,
    http_referer TEXT,
    http_user_agent TEXT,
    http_x_forwarded_for TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

注意:time_local 字段我用的是 DATETIME,但原始日志是 [21/Oct/2025:10:13:59 +0800] 格式,需要在插入前转换 。不过为了简化脚本,我先原样存为字符串,后续用 SQL 或应用层处理时间转换(也可以在 Bash 中用 date -d 转换,但会增加复杂度)。

2. 解析脚本核心逻辑

脚本使用 tail -F 实时监听日志文件,通过正则匹配提取字段:

bash 复制代码
#!/bin/bash

LOG_FILE="/data/log/access.log"
DB_HOST="127.0.0.1"
DB_USER="log_push"
DB_PASS="log_push@123"
DB_NAME="log"
TABLE_NAME="nginx_log"

escape() {
    echo "${1//\'/\'\'}"
}

tail -F "$LOG_FILE" | while read -r line; do
    if [[ $line =~ ^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\ \-\ ([^\ ]*)\ ([0-9]+)\ \[([^\]]+)\]\ \"([^\"]+)\"\ ([0-9]+)\ ([0-9\.]+)\ ([0-9\-]+)\ \"([^\"]*)\"\ \"([^\"]+)\"\ \"([^\"]*)\"$ ]]; then
        # 提取字段
        remote_addr="${BASH_REMATCH[1]}"
        remote_user="${BASH_REMATCH[2]}"
        server_port="${BASH_REMATCH[3]}"
        time_local="${BASH_REMATCH[4]}"
        request="${BASH_REMATCH[5]}"
        status="${BASH_REMATCH[6]}"
        request_time="${BASH_REMATCH[7]}"
        body_bytes_sent="${BASH_REMATCH[8]}"
        http_referer="${BASH_REMATCH[9]}"
        http_user_agent="${BASH_REMATCH[10]}"
        http_x_forwarded_for="${BASH_REMATCH[11]}"

        # 拆解 request
        request_arr=($request)
        if [ ${#request_arr[@]} -eq 3 ]; then
            request_method="${request_arr[0]}"
            request_url="${request_arr[1]}"
            request_protocol="${request_arr[2]}"
        else
            request_method=""
            request_url=""
            request_protocol=""
        fi

        # 构造 SQL(注意转义单引号)
        SQL="INSERT INTO $TABLE_NAME (...) VALUES (...);"
        mysql -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS" -D "$DB_NAME" -e "$SQL"
    else
        echo "Failed to parse: $line" >> /var/log/nginx_log_parser.err
    fi
done

3. 关键点说明

  • 正则表达式必须严格匹配:Nginx 日志中如果有换行或特殊字符(比如 User-Agent 含引号),会导致解析失败。生产环境建议先做日志清洗。

  • 单引号转义escape() 函数简单处理了 SQL 注入风险(虽然 User-Agent 一般不会恶意,但安全起见)。

  • 性能考量 :每行日志都调用一次 mysql 命令,高频场景下会有性能瓶颈。如果 QPS > 100,建议改用批量插入(比如缓存 100 行再批量写)或换 Python/Go 实现。

  • 时间格式问题 :如前所述,[21/Oct/2025:10:13:59 +0800] 不能直接插入 DATETIME。如果必须转,可以用:

    bash 复制代码
    mysql_time=$(date -d "${time_local//\// }" +"%Y-%m-%d %H:%M:%S")

    但要注意时区一致性。


四、部署与守护

脚本写好后,用 systemdsupervisor 守护起来:

ini 复制代码
# /etc/systemd/system/nginx-log-parser.service
[Unit]
Description=Nginx Log Parser to MySQL
After=network.target

[Service]
Type=simple
User=root
ExecStart=/bin/bash /opt/scripts/nginx_log_to_mysql.sh
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

然后:

bash 复制代码
systemctl daemon-reload
systemctl start nginx-log-parser
systemctl enable nginx-log-parser

五、为什么不直接用 Filebeat + Logstash?

  • 轻量:我们的日志量不大(日均百万级),没必要上 ELK。
  • 可控:自己写脚本,字段解析逻辑完全掌握,调试方便。
  • 成本低:省去了维护 Elasticsearch 集群的资源和人力。

当然,如果未来日志量暴涨或需要全文检索,再迁移到 ELK 也不迟。


六、总结

这个方案虽然"土",但在中小项目中非常实用。它不依赖复杂中间件,开发成本低,且能快速满足业务对结构化日志的需求。

技术选型没有银弹,适合的才是最好的。

如果你也在寻找一个轻量级的日志入库方案,不妨试试这个 Bash 脚本。代码虽糙,但能跑就行 😄


相关推荐
Zzzzmo_19 小时前
【MySQL】JDBC(含settings.xml文件配置/配置国内镜像以及pom.xml文件修改)
数据库·mysql
FirstFrost --sy20 小时前
MySQL内置函数
数据库·mysql
eggwyw20 小时前
MySQL-练习-数据汇总-CASE WHEN
数据库·mysql
mygljx1 天前
MySQL 数据库连接池爆满问题排查与解决
android·数据库·mysql
Bdygsl1 天前
MySQL(1)—— 基本概念和操作
数据库·mysql
身如柳絮随风扬1 天前
什么是左匹配规则?
数据库·sql·mysql
jiankeljx1 天前
mysql之如何获知版本
数据库·mysql
小李来了!1 天前
数据库DDL、DML、DQL、DCL详解
数据库·mysql
我科绝伦(Huanhuan Zhou)1 天前
【生产案例】MySQL InnoDB 数据损坏崩溃修复
数据库·mysql·adb
海棠蚀omo1 天前
从零敲开 MySQL 的大门:库与表的基础操作实战(保姆级入门指南)
数据库·mysql