日志socket发送必须非阻塞,应使用独立线程+无锁队列缓存日志,发送前设置socket为非阻塞,send()返回eagain/ewouldblock时暂存重试;首选tcp长连接,加4字节长度头解决粘包;日志格式需含时间戳和级别,采用零拷贝二进制协议;连接异常须指数退避自动重连,并设队列硬上限防积压。

日志 Socket 发送必须非阻塞,否则主线程卡死
程序日志写入和网络发送不能串在同一线程里——send() 遇到远端接收慢、网络抖动或 SOCK_STREAM 缓冲满时会阻塞,直接拖垮业务逻辑。真实场景下,哪怕只是远程服务器临时 GC 或磁盘忙,都可能让 send() 停 100ms+,对实时性要求高的服务不可接受。
实操建议:
- 用独立线程 + 无锁队列(如
moodycamel::ConcurrentQueue)缓存日志条目,主线程只做enqueue() - 发送线程调用
send()前必须设置 socket 为非阻塞:fcntl(sockfd, F_SETFL, O_NONBLOCK)(Linux)或ioctlsocket(sockfd, FIONBIO, &on)(Windows) - 检查
send()返回值:返回-1且errno == EAGAIN || errno == EWOULDBLOCK时说明内核缓冲区满,应暂存日志并稍后重试,不是报错退出
TCP 还是 UDP?看日志丢弃容忍度和顺序要求
UDP 看似简单,但生产环境几乎不适用:没有重传、无序、易被防火墙拦截、单包上限约 64KB(实际常被限制在 1400 字节以内),一条长日志切分后丢失任意一包就整条失效。而 TCP 虽有粘包问题,但可靠性、顺序性、跨网段穿透能力更强,是远程日志同步的默认选择。
实操建议:
- 坚持用
SOCK_STREAM+AF_INET,别图省事选 UDP - 解决粘包:每条日志前加 4 字节大端长度头(
htonl(len)),接收端按头读取完整包,避免recv()多次调用拼接错误 - 连接管理:用长连接(keep-alive),不要每次日志都
connect()→send()→close(),开销大且易触发 TIME_WAIT 暴涨
日志格式必须带时间戳和级别字段,且序列化要零拷贝
远程服务器靠结构化字段做过滤、告警、入库;如果只传裸字符串,后续所有解析、提取都得在服务端重复做,性能差还容易出错。更关键的是,C++ 中拼接字符串(如 std::ostringstream)或频繁 std::string::append() 会触发多次堆分配,高并发日志下成为瓶颈。
实操建议:
- 定义紧凑二进制协议:例如 1 字节 level + 8 字节 nanotime(
clock_gettime(CLOCK_REALTIME_COARSE, &ts)) + 4 字节 msg_len + 原始 UTF-8 日志内容 - 用
std::vector<char></char>预分配缓冲区,写入时用指针偏移直接填充,避免中间std::string对象 - 禁止在日志线程里调用
std::time()或std::localtime()——它们不是 async-signal-safe,多线程下可能死锁或返回脏数据
连接异常必须自动恢复,且重连间隔要退避
远程服务器重启、网络闪断、防火墙策略变更都会导致 socket 断开。如果程序不处理 recv() 返回 0(对端关闭)或 send() 返回 -1 且 errno == EPIPE || ECONNRESET,日志就会静默丢失,监控也难发现。
实操建议:
- 发送线程每次
send()后检查返回值,遇到连接错误立即关闭 socket 并标记“需重连” - 重连不能轮询
connect(),要用指数退避:首次失败后等 1s,再失败等 2s,然后 4s、8s……上限设为 60s,避免打爆远端连接数 - 重连成功后,把断连期间积压在队列里的日志按时间戳排序重发(注意别重复发已确认的),队列长度需设硬上限(如 10MB),超限时丢最老日志而非阻塞
真正麻烦的是日志时间精度与网络延迟的博弈——本地纳秒时间戳发过去,服务端收到时可能已延迟几毫秒,但只要不跨秒,对聚合分析影响不大。反而容易被忽略的是:不同机器时钟未 NTP 同步时,clock_gettime(CLOCK_REALTIME) 的误差可达百毫秒级,调试时看到“日志倒序”别急着修代码,先查 ntpq -p。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











