HAProxy日志时间打印优化实践
HAProxy 日志是排查负载均衡故障、分析流量特征、优化性能的核心依据,不同日志格式(默认 / 自定义)包含的信息维度不同,下面从日志配置、日志格式日志输出等方面分析。
HAProxy 日志格式
HAProxy 日志默认有两种输出格式,需先确认你的配置(haproxy.cfg 中 log-format 字段)
- 默认格式(HTTP 模式)
HAProxy 对 HTTP 流量的默认日志格式,包含请求方法、URL、状态码、客户端 / 后端信息等核心字段。
- TCP 模式格式
若 HAProxy 代理 TCP 流量(如数据库、SSH),日志会简化为「连接层面」的信息(无 HTTP 相关字段)。
日志输出位置
HAProxy 日志默认发送到系统日志(/var/log/messages/、/var/log/syslog),也可配置为独立文件(如 /var/log/haproxy.log),需确保 rsyslog/syslog-ng 已正确配置。
下面主要通过分析TCP 模式日志格式(适配数据库),使用三种方式【syslog、stdout和源码改造】研究HAProxy标准日志输出。
HAProxy 日志输出方式
- 输出到本地 syslog 服务
HAProxy 最经典的日志输出方式,依赖系统 rsyslog/syslog-ng 服务,将日志发送到本地 syslog 后落地到文件
缺点:需配置 syslog 服务,容器化场景适配性差。
- 输出到本地 Unix 套接字(比 syslog 更高效)
用本地 Unix 套接字(/dev/log)替代网络套接字(127.0.0.1:514),减少网络开销,局限性:仅适用于 HAProxy 与 syslog 同机的场景。
配套配置
无需额外监听 514 端口,仅需 rsyslog 配置 local2.* /var/log/haproxy.log 即可。
适用场景
物理机 / 虚拟机部署,HAProxy 与 syslog 同机,不适用容器化场景;
追求日志传输效率(无 UDP 网络开销)。
- 输出到标准输出(stdout)(容器化首选,HAProxy 2.4+)
HAProxy 2.4 新增的输出方式,直接将日志打印到进程 stdout,适配 Docker/K8s 容器化场景(容器日志默认采集 stdout)
**适用场景:**Docker/K8s 容器化部署的 HAProxy;无 syslog 服务的轻量环境(如极简容器);调试场景(直接看控制台输出)
- 输出到远程 syslog 服务器(集中化日志)
将日志发送到远程 syslog 服务器(如 rsyslog 服务端、ELK、Graylog),实现多台 HAProxy 日志集中管理。
**适用场景:**多台 HAProxy 集群部署;集中化日志分析、存储的场景;
1、HAProxy默认日志配置
通过配置log 127.0.0.1:514 local2和log global执行日志打印,将 HAProxy 日志发送到本机 514 端口的 syslog 服务,并标记为 local2 日志设施,defaults继承 global 段定义的日志输出规则。
global log 127.0.0.1:514 local2 maxconn 1000 ulimit-n 1048576 daemon stats socket /usr/local/stats.sock mode 666 level admin pidfile /var/run/haproxy.pid nbthread 8 defaults mode tcp retries 6 timeout http-request 10s timeout queue 1m timeout connect 10s timeout client 3601s timeout server 3601s timeout http-keep-alive 3601s timeout check 10s log global option tcplog option dontlognull option http-server-close option redispatch resolvers mysql-resolver parse-resolv-conf resolve_retries 6 timeout retry 3s hold valid 10s hold nx 10s hold timeout 10s hold refused 10s hold other 10s listen inter_listen bind :::3306 v4v6 mode tcp option tcpka balance roundrobin server mysql1 a4243a5ca8a-6b4370-headless.200cec7c.svc.cluster.local:3306 check inter 30000 fall 3 rise 3 resolvers mysql-resolver |
日志输出:
**存在问题:**打印日志无时间信息,不方便运维人员定位异常时间点。
2、log-format配置日志打印格式
精简并定制日志输出字段,只保留连接、时间、前后端、流量、连接数等核心维度,去掉了 HTTP 请求行、Cookie 等冗余字段,适合对日志体积敏感、仅关注基础流量监控的场景。
格式定义:log-format "%ci:%cp [%t] %ft %b/%s %Tw/%Tc/%Tt %B %ts %ac/%fc/%bc/%sc/%rc %sq/%bq"
占位符 | 完整名称 | 中文含义 | 示例 |
%ci | client_ip | 客户端 IP 地址(发起请求的客户端 / 上游设备 IP) | 192.168.1.100 |
%cp | client_port | 客户端源端口(客户端发起请求的端口) | 54321 |
%t | time_local | HAProxy 接收请求的时间(精确到毫秒,格式:[dd/MMM/YYYY:HH:MM:SS.ms]) | [20/Nov/2025:10:23:44.123] |
%ft | frontend_name | 接收请求的前端名称(haproxy.cfg 中 frontend 配置的名称) | http_front |
%b | backend_name | 选中的后端名称(haproxy.cfg 中 backend 配置的名称) | http_back |
%s | server_name | 选中的后端服务器名称(backend 中 server 配置的名称) | server1 |
%Tw | time_queue_wait | 客户端请求在 HAProxy 队列中的等待时间(毫秒,无等待则为 0) | 0 |
%Tc | time_connect | HAProxy 与后端服务器建立连接的时间(毫秒) | 10 |
%Tt | time_total | 从请求入队到响应完成的总耗时(毫秒,= % Tw + % Tc + 后端处理时间) | 30 |
%B | bytes_out | HAProxy 发送给客户端的响应字节数(不含 TCP/IP 头部) | 1234 |
%ts | termination_state | 会话终止状态 | |
... |
stdout输出方式:容器化友好、无需配置 syslog、调试直观。Syslog,需配置 rsyslog/syslog-ng,使用成本高。
指定stdout输出格式,必须指定log global,否则日志不输出到stdout。log stdout local3 info 是 HAProxy 2.4+ 版本新增的日志输出到标准输出(stdout) 的配置,核心作用是「将 HAProxy 日志直接打印到进程的标准输出,标记为 local3 设施,日志级别为 info」,特别适合容器化(Docker/K8s)部署的 HAProxy(容器日志默认采集 stdout)。
版本要求:
HAProxy 版本 >= 2.4,HAProxy 版本 < 2.4,不支持 stdout 目标。
配置示例:
日志输出:
**存在问题:**发现使用stdout输出方式日志出现重复打印情况,存在有时间和无时间两行日志,造成日志冗余,增加了日志采集负担和排查复杂度。
- 改造HAPROXY源码,修改默认日志打印格式
优化思路:通过修改v3.2.10版本HAProxy日志打印函数print_message,通过扩展前缀缓冲区空间,将格式化后的时间信息拼接至原有label前,实现日志默认打印时间信息目的,主要改造内容:
- 前缀缓冲区扩展
扩展前缀缓冲区:原11字节 + 时间戳(21字节) + 预留空间,总计64字节(避免溢出)
char prefix[64] = {0};
- 时间查询
struct timeval tv;
gettimeofday(&tv, NULL);
- 时间解析
localtime_r(&tv.tv_sec, &tm_info);
线程安全的时间解析,避免localtime()竞态问题。
- 格式化时间戳
格式:[YYYY-MM-DD HH:MM:SS]
char time_buf[32] = {0};
snprintf(time_buf, sizeof(time_buf),
"[%04d-%02d-%02d %02d:%02d:%02d]",
tm_info.tm_year + 1900, tm_info.tm_mon + 1, tm_info.tm_mday,
tm_info.tm_hour, tm_info.tm_min, tm_info.tm_sec);
- 拼接时间戳
先写入时间戳
strncpy(prefix, time_buf, sizeof(prefix) - 1);
拼接原有label(格式:[时间] [label]), label最多7字符,兼容原逻辑。
snprintf(prefix + strlen(prefix), sizeof(prefix) - strlen(prefix) - 1, "[%-.7s] ", label ? label : "");
时间戳格式设定:
[2025-12-29 15:30:45] [label] (1234) : 消息内容
兼容性:
完全保留原有函数的入参、上下文逻辑、日志写入、控制台输出行为,仅新增时间戳前缀,无需修改调用该函数的其他代码。
**修改源码位置:**haproxy-3.2/src/errors.c文件
代码开头添加头文件: #include <sys/time.h> // 用于gettimeofday ... /* Generic function to display messages prefixed by a label + timestamp */ static void print_message(int use_usermsgs_ctx, const char *label, const char *fmt, va_list argp) { struct ist msg_ist = IST_NULL; char *head, *parsing_str, *msg; char prefix[64] = {0}; struct timeval tv; struct tm tm_info; char time_buf[32] = {0}; gettimeofday(&tv, NULL); localtime_r(&tv.tv_sec, &tm_info); snprintf(time_buf, sizeof(time_buf), "[%04d-%02d-%02d %02d:%02d:%02d]", tm_info.tm_year + 1900, tm_info.tm_mon + 1, tm_info.tm_mday, tm_info.tm_hour, tm_info.tm_min, tm_info.tm_sec); strncpy(prefix, time_buf, sizeof(prefix) - 1); snprintf(prefix + strlen(prefix), sizeof(prefix) - strlen(prefix) - 1, "[%-.7s] ", label ? label : ""); ... |
日志配置:
global log 127.0.0.1 local3 info maxconn 700 ulimit-n 1048576 daemon stats socket /usr/local/stats.sock mode 666 level admin pidfile /var/run/haproxy.pid nbthread 8 defaults mode tcp retries 6 timeout queue 1m timeout connect 10s timeout client 3601s timeout server 3601s timeout check 10s log global
option dontlognull option redispatch resolvers mysql-resolver parse-resolv-conf resolve_retries 6 timeout retry 3s hold valid 10s hold nx 10s hold timeout 10s hold refused 10s hold other 10s listen inter_listen bind :::3306 v4v6 mode tcp option tcpka balance roundrobin server mysql1 abe601669c2-8665a0-headless.971a0e06.svc.cluster.local:3306 check inter 30000 fall 3 rise 3 resolvers mysql-resolver |
改造后日志输出:
连通性测试验证:
优化源码后的HAPrxoy可以达到日志打印时间信息目的,并避免修改引入缓冲区溢出和线程安全等问题。