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