libcurl偶发下载无数据写入缓冲区 内存流为空问题排查
问题现象
基于libcurl实现URL资源下载到内存流的逻辑,约2%概率出现下载完成后流大小为0的异常,同一URL发起重试即可正常下载,暂未确认根因为网络异常、CPU高负载还是代码边界逻辑缺失。
现有实现代码如下:
static size_t write_data(char *ptr, size_t size, size_t nmemb, void *userdata) { std::vector<uchar> *stream = (std::vector<uchar>*)userdata; size_t count = size * nmemb; stream->insert(stream->end(), ptr, ptr + count); return count; } static void CurlUrl(const char* img_url, std::vector<uchar>* stream) { CURL *curl = curl_easy_init(); // curl_global_init is called eleswhere. curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); curl_easy_setopt(curl, CURLOPT_URL, img_url); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data); curl_easy_setopt(curl, CURLOPT_WRITEDATA, stream); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10); CURLcode res = curl_easy_perform(curl); curl_easy_cleanup(curl); }
排查方向
按优先级从高到低排查:
- 缺失核心返回值校验:现有代码完全没有判断
curl_easy_perform的返回值res,出现空流时首先要打印该返回码以及curl_easy_strerror(res)对应的错误描述,可直接区分是连接失败、超时、SSL错误还是其他libcurl层面的异常,是定位问题的首要依据。 - 未校验HTTP业务响应码:即使
curl_easy_perform返回CURLE_OK,也不代表资源请求成功。服务端偶发返回302跳转、403限流、502/503网关错误时,可能返回空响应体,必须通过curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code)获取实际HTTP状态码,2%的偶发概率很大概率匹配服务端限流、网关临时故障的场景。 - 回调逻辑边界缺失:现有write回调默认直接写入数据、返回count值,没有处理
ptr为空、count为0的异常场景;如果极端场景下vector内存扩容失败抛出C++异常,会导致C回调没有正常返回值,触发libcurl异常终止,留下空流。 - 超时配置不合理:当前仅设置了10秒总超时,没有单独配置连接超时
CURLOPT_CONNECTTIMEOUT,网络波动时连接阶段耗时过长会直接触发总超时中断请求;高CPU负载场景下线程调度延迟也可能导致libcurl超时计数不准,提前中断请求返回空数据。 - 全局初始化时序问题:虽然注释说明
curl_global_init在外部调用,需要确认该调用是在所有libcurl工作线程启动前完成,且传入参数为CURL_GLOBAL_ALL,多线程场景下初始化时序错误会偶发libcurl内部状态异常,导致请求失败。
修复方案
- 补全全链路校验逻辑:
- 校验
curl_easy_init返回值是否为空,处理资源不足导致的初始化失败场景 - 校验
curl_easy_perform返回值,非CURLE_OK时直接记录错误、进入重试逻辑 - 请求执行完成后获取HTTP响应码,仅2xx区间的状态码判定为业务成功
- 校验
- 补全必要配置项:
- 增加单独的连接超时配置,例如设置
CURLOPT_CONNECTTIMEOUT为3秒,避免连接阶段占用全部超时时长 - 设置
CURLOPT_FOLLOWLOCATION为1L,自动处理3xx类跳转响应,避免跳转场景下拿到空body - 可选配置低速率检测:设置
CURLOPT_LOW_SPEED_LIMIT为10字节/秒、CURLOPT_LOW_SPEED_TIME为5秒,快速识别网络僵死场景,避免空等满10秒超时
- 增加单独的连接超时配置,例如设置
- 优化回调与前置逻辑:
- 调用libcurl发起请求前先清空传入的stream向量,避免历史残留数据干扰
- write回调中增加入参校验,
ptr为空或count为0时直接返回0,触发libcurl报错 - 对vector插入逻辑做异常保护,避免C回调中抛出C++异常引发未定义行为
- 增加有限重试机制:针对可重试错误(连接失败、超时、HTTP 5xx类错误)做2-3次重试,重试时增加短时间退避间隔,覆盖偶发网络波动、服务端临时故障场景。
修复后的核心参考代码:
static size_t write_data(char *ptr, size_t size, size_t nmemb, void *userdata) { if (ptr == nullptr || userdata == nullptr) { return 0; } std::vector<uchar> *stream = static_cast<std::vector<uchar>*>(userdata); size_t count = size * nmemb; if (count == 0) { return 0; } try { stream->insert(stream->end(), ptr, ptr + count); } catch (...) { return 0; } return count; } static CURLcode CurlUrl(const char* img_url, std::vector<uchar>* stream, long& http_code) { if (img_url == nullptr || stream == nullptr) { return CURLE_BAD_FUNCTION_ARGUMENT; } stream->clear(); CURL *curl = curl_easy_init(); if (curl == nullptr) { return CURLE_FAILED_INIT; } curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); curl_easy_setopt(curl, CURLOPT_URL, img_url); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data); curl_easy_setopt(curl, CURLOPT_WRITEDATA, stream); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 10L); curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 5L); CURLcode res = curl_easy_perform(curl); if (res == CURLE_OK) { curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); } curl_easy_cleanup(curl); return res; }
内容的提问来源于stack exchange,提问作者Weiqi Wang
相关产品推荐
相关产品推荐

