实战:将 Nginx 日志实时解析并写入 MySQL,不再依赖 ELK
最近在做系统监控和日志分析时,遇到一个很现实的问题:我们不想引入 ELK(Elasticsearch + Logstash + Kibana)这么重的架构,但又需要把 Nginx 的访问日志结构化存入数据库,用于后续的业务分析、异常追踪或安全审计。
于是,我决定用一个轻量级方案:用 Bash 脚本实时解析 Nginx 日志,并直接写入 MySQL。整个过程踩了一些坑,也积累了一些经验,今天就来分享一下这个“小而美”的实现思路。
一、Nginx 日志格式定制
首先,得确保 Nginx 输出的日志格式是我们可控的。默认的 combined 格式虽然通用,但缺少一些关键字段,比如后端响应时间、服务端口等。
我在 nginx.conf 中自定义了一个 main 格式:
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+):
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 实时监听日志文件,通过正则匹配提取字段:
#!/bin/bashLOG_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; doif [[ $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]}"# 拆解 requestrequest_arr=($request)if [ ${#request_arr[@]} -eq 3 ]; thenrequest_method="${request_arr[0]}"request_url="${request_arr[1]}"request_protocol="${request_arr[2]}"elserequest_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"elseecho "Failed to parse: $line" >> /var/log/nginx_log_parser.errfi
done
3. 关键点说明
- 正则表达式必须严格匹配:Nginx 日志中如果有换行或特殊字符(比如 User-Agent 含引号),会导致解析失败。生产环境建议先做日志清洗。
- 单引号转义:
escape()函数简单处理了 SQL 注入风险(虽然 User-Agent 一般不会恶意,但安全起见)。 - 性能考量:每行日志都调用一次
mysql命令,高频场景下会有性能瓶颈。如果 QPS > 100,建议改用批量插入(比如缓存 100 行再批量写)或换 Python/Go 实现。 - 时间格式问题:如前所述,
[21/Oct/2025:10:13:59 +0800]不能直接插入DATETIME。如果必须转,可以用:
但要注意时区一致性。mysql_time=$(date -d "${time_local//\// }" +"%Y-%m-%d %H:%M:%S")
四、部署与守护
脚本写好后,用 systemd 或 supervisor 守护起来:
# /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
然后:
systemctl daemon-reload
systemctl start nginx-log-parser
systemctl enable nginx-log-parser
五、为什么不直接用 Filebeat + Logstash?
- 轻量:我们的日志量不大(日均百万级),没必要上 ELK。
- 可控:自己写脚本,字段解析逻辑完全掌握,调试方便。
- 成本低:省去了维护 Elasticsearch 集群的资源和人力。
当然,如果未来日志量暴涨或需要全文检索,再迁移到 ELK 也不迟。
六、总结
这个方案虽然“土”,但在中小项目中非常实用。它不依赖复杂中间件,开发成本低,且能快速满足业务对结构化日志的需求。
技术选型没有银弹,适合的才是最好的。
如果你也在寻找一个轻量级的日志入库方案,不妨试试这个 Bash 脚本。代码虽糙,但能跑就行 😄
