读重试

读重试

版本依赖:读重试依赖 tcapdir 3.87.1 及其以后的版本。更早版本的 tcapdir 不会下发 conf:// 读重试配置,此时即使业务调用了 SetReadRetryTimeout,读重试也不会生效(SDK 不会报错,只是不重试)。

1. 功能简介

读请求因为网络抖动、proxy 异常等原因迟迟收不到响应(或返回了指定的错误码)时,SDK 会在内部自动换一个 proxy 重发,业务只会收到最终结果,中间的失败不会抛给业务。

  • 默认关闭,需要业务调用 SetReadRetryTimeout 开启。
  • 只对下列读请求生效,写请求永远不会重试:
类型命令字
简单请求TCAPLUS_API_GET_REQ、TCAPLUS_API_PB_FIELD_GET_REQ、TCAPLUS_API_LIST_GET_REQ
复杂请求TCAPLUS_API_BATCH_GET_REQ、TCAPLUS_API_PB_BATCH_FIELD_GET_REQ、TCAPLUS_API_LIST_GET_BATCH_REQ、TCAPLUS_API_GET_BY_PARTKEY_REQ、TCAPLUS_API_LIST_GETALL_REQ
  • 具体对哪些命令字、哪些错误码重试,以及最多重试几次,由 tcapdir 下发的配置决定(默认重试 2 次,最多 8 次),业务侧只需要设置超时预算。
  • 重试次数不含首次发送:重试 2 次表示一个请求最多发出 3 个包。次数按每个请求各自计算、累积,只有”重发”才计一次,用完即判定超时。
  • 重发时会尽量避开已经试过的 proxy:优先换一台没试过的机器,其次换一个没试过的端口,都没有未试过的可用 proxy 时才复用已试过的。

2. 接口说明

/**
@brief  设置读重试的开关与超时预算,Init 之后调用,两个参数默认都是 0,即不开启读重试
@param [IN] simple_timeout_ms   简单请求一次读操作的总超时预算,单位毫秒,推荐 3000,0 表示不开启
@param [IN] complex_timeout_ms  复杂请求分包续期的总超时预算,单位毫秒,推荐 6000,0 表示不开启
*/
void TcaplusServer::SetReadRetryTimeout(IN int simple_timeout_ms, IN int complex_timeout_ms);

/**
@brief  设置单个 zone 最多缓存多少个读重试上下文,Init 之后调用,不调用默认每个 zone 50w 个
@param [IN] max_count  单个 zone 的上下文条数上限,小于等于 0 按默认值 50w 处理
@note   到达上限后新的读请求不再重试,照常发送
*/
void TcaplusServer::SetReadRetryMaxContextCountPerZone(IN int max_count);
  • 两个接口在 RegistTable 之前或之后调用都可以,之后新加入的 zone 会自动继承已设置的值。
  • 运行中可以再次调用 SetReadRetryTimeout 调整预算,传 0 即关闭对应类型的读重试。

3. 使用示例

// 1. 初始化
int32_t iRet = g_stTcapSvr.Init(g_pstTlogger, /*module_id*/0, APP_ID, ZONE_ID, SIGNATURE);
if (0 != iRet)
{
    return iRet;
}

// 2. 开启读重试:简单请求预算 3000ms,复杂请求预算 6000ms
g_stTcapSvr.SetReadRetryTimeout(3000, 6000);

// 3. (可选)调整单个 zone 的上下文上限
g_stTcapSvr.SetReadRetryMaxContextCountPerZone(500000);

// 4. 注册表、发送请求,与未开启读重试时完全一样
iRet = g_stTcapSvr.RegistTable(TABLE_NAME, g_szTableMeta, 10000);
// ...

// 5. 接收响应:需要周期性调用 RecvResponse,读重试的超时判断和重发都在这里驱动
TcaplusServiceResponse* pstRsp = NULL;
iRet = g_stTcapSvr.RecvResponse(pstRsp);
if (1 == iRet && NULL != pstRsp)
{
    int32_t result = pstRsp->GetResult();
    if (TcapErrCode::API_ERR_READ_RETRY_TIMEOUT == result)
    {
        // 重试次数用完仍没有响应,按超时处理
        // 通过 GetAsynID / GetUserBuff 等仍可取回发送时的异步 ID 和 userbuffer
    }
    else if (0 == result)
    {
        // 正常处理
    }
}

开启读重试后业务代码无需改动:

  • 响应中的异步 ID、seq、userbuffer 与发送时一致。
  • 每个请求只会交付一次最终结果,重试产生的重复响应由 SDK 丢弃。
  • 复杂请求依旧按原来的方式处理分包(HaveMoreResPkgs 等),SDK 只补发缺失的部分,保证记录不重复、不遗漏。

4. 超时预算说明

4.1. 简单请求

simple_timeout_ms 是一次读操作从首发到最终超时的总时长。SDK 把它切成 n+1 段(n 为重试次数),第 k 段时长为 k*x:

(1 + 2 + ... + (n+1)) * x = simple_timeout_ms

以推荐值 3000ms、重试 2 次为例,x = 500ms:

时间点动作
0ms首次发送
500ms仍无响应,换 proxy 第 1 次重试
1500ms仍无响应,换 proxy 第 2 次重试
3000ms仍无响应,返回错误码 API_ERR_READ_RETRY_TIMEOUT(-19219)

4.2. 复杂请求

复杂请求会分多个包返回,complex_timeout_ms 按同样的方式切成 n+1 段、第 k 段时长为 k*x,但这一段的时长是”两个分包之间最多等多久”,不是整个请求的总时长:

  • 以推荐值 6000ms、重试 2 次为例,x = 1000ms:
  • 初始时,等分包超时时间就是1000ms。
  • 如果分包超时了,此时重试发送,等分包的超时时间提高到 2000ms,累积重试了1次。
  • 如果后面还有分包超时了,此时重试发送,等分包的超时时间提高到 3000ms,累积重试了2次。
  • 如果后面再次有分包超时,超过累积总次数2,此时构造错误码API_ERR_READ_RETRY_TIMEOUT返回给业务。
  • 重试次数是整个请求累积的:ReadRetryMaxCnt(默认 2)就是一个请求最多重发几次,收了多少分包都不会把次数清零或补回来,重发满次数仍收不全就返回 API_ERR_READ_RETRY_TIMEOUT。

重试时只补发还没收到的部分:批量查询只补发未返回的 key,索引查询(GetByPartKey、ListGetAll)从已收到的位置往后续传。

4.3. 错误码重试

响应的错误码命中 tcapdir 配置的错误码名单时,SDK 会立即换 proxy 重发,不必等到分段超时

5. tcapdir 侧如何开启(运维)

读重试的名单和重试次数由 tcapdir 下发,配置保存在 center 库的 service_api_conf 表,按 (AppId, ZoneId) 粒度生效。只有 tcapdir 3.87.1 及以后的版本才会读取这三列并下发配置。

字段含义默认值
ReadRetryCmd参与读重试的命令字,多个值用竖线分隔7|103|15|23|117|153|25|11(8 个读命令字)
ReadRetryErrorCode命中即立即重试的错误码,多个值用竖线分隔-7953(PROXY_ERR_SWIFT_TIMEOUT)
ReadRetryMaxCnt最多重试次数,不含首次发送,最大 82

8 个默认命令字与接口名的对应关系:

命令字接口
7 / 103 / 15TCAPLUS_API_GET_REQ / TCAPLUS_API_PB_FIELD_GET_REQ / TCAPLUS_API_LIST_GET_REQ
23 / 117 / 153TCAPLUS_API_BATCH_GET_REQ / TCAPLUS_API_PB_BATCH_FIELD_GET_REQ / TCAPLUS_API_LIST_GET_BATCH_REQ
25 / 11TCAPLUS_API_GET_BY_PARTKEY_REQ / TCAPLUS_API_LIST_GETALL_REQ

下发方式:tcapdir 把这三列拼成一个 conf:// 元素搭载在 CollectUrlList 中下发,与数据采集地址(tcp://)共用同一列,用 ; 分隔,因此不需要改协议:

conf://ReadRetryCmd=7|103|15|23|117|153|25|11&ReadRetryErrorCode=-7953&ReadRetryMaxCnt=2

生效方式:tcapdir 从数据库加载配置(周期加载,也可通过 ReloadDB 命令强制加载),API 每次向 tcapdir 更新路由时都会重新收到配置,业务进程无需重启。

要关闭读重试,把 ReadRetryCmd 置空(ReadRetryCmd=)或把 ReadRetryMaxCnt 置 0 即可;三列都不下发时(如老版本 tcapdir),SDK 保持当前配置不变。

6. 注意事项

  • 超时预算应小于业务上层的超时时间,否则业务已经放弃等待,读重试就失去了意义。
  • 内存占用:每个在途的读请求都会在本地缓存一份上下文(含打包后的请求和 userbuffer),开启后额外内存约为 读 qps × 超时秒数 × (key 大小 + userbuffer 大小)。上下文上限按 zone 各自计算,多 zone 场景内存约为 zone 个数 × 上限条数 × (key 大小 + userbuffer 大小),调大前请评估。
  • 读重试超时返回的错误码是 API_ERR_READ_RETRY_TIMEOUT,与普通等待超时 API_ERR_WAIT_RSP_TIMEOUT 不同,业务可据此区分。
  • 只有在 SetReadRetryTimeout 设置了有效预算,并且 tcapdir 下发了读重试配置(重试次数大于 0、命令字名单非空)时,读重试才会生效。
  • SQL 查询等其余复杂请求暂不支持读重试。

7. 常见问题

Q:已经调用了 SetReadRetryTimeout,为什么没有触发重试?

按以下顺序排查:

  1. SDK 日志中搜索 set read retry option,确认设置的预算不为 0。
  2. SDK 日志中搜索 recv read retry conf,确认 tcapdir 已下发配置,且其中的 ReadRetryCmd 包含当前命令字、ReadRetryMaxCnt 大于 0;没有该日志说明 tcapdir 未下发配置,先确认 tcapdir 版本不低于 3.87.1,再联系运维按 §5 开启。
  3. 确认上下文是否已达上限(SetReadRetryMaxContextCountPerZone):日志中搜索 reach max context count,有则说明达到上限,达到上限的请求不重试、照常发送。

Q:重试用完后日志里有什么?

SDK 会打印 error 日志 read retry exhausted after N retries, cost Xms,并给业务返回 API_ERR_READ_RETRY_TIMEOUT。