Backend

采集渠道的官网接口持续返回 501,我们能修和不能修的部分

2992 字约 10 分钟

摘要Job Market Monitor 里有一个渠道从 9 月 16 日起反复采集失败。官网列表接口间歇性返回 501,同时我们的解析器也和新版详情接口不兼容。修好自己的部分、用当天早上保存的完整列表加实时详情逐条核验后恢复了数据,但上游的 501 到最后也没有消失。

Job Market Monitor 每天要采集近百个渠道,每个渠道对应一家公司的一类招聘入口,比如社招、校招或者实习。从 9 月 16 日开始,其中一家出行公司的社招渠道反复采集失败。起初我只是在页面上看到一行提示:部分招聘信息更新延迟,当前展示最近一次完整数据。

先别让一个渠道拖累全站

这行提示本身就有问题。我问助手这是什么意思,它的解释是,某个渠道没采到今天的数据,页面就用最近一次完整的数据代替,并在全站提示更新延迟。可是其他九十多个渠道今天都是完整的,没有理由为一个渠道让所有页面都带上提示。

我提出的要求是渠道之间应该互相独立,没有必要的地方不要显示这句话,让用户感到困惑,而且“数据采集状态”这类运维页面也不该让普通用户看到。于是改成这样:

  • 每个渠道各自使用自己最近一次完整的数据,其他 94 个渠道用当天的,失败的渠道沿用前一天,互不拖累。
  • 去掉通用的延迟警告,只在确实需要的地方显示真实的数据日期。趋势仍然只比较完整的、可比的日期,失败的日子不补零、不复制上一次的数量。
  • 采集状态只有管理员能看到,普通用户没有入口,直接访问也会被后端拒绝。

这样把影响隔离了,但渠道本身还没有修好。

两个叠在一起的问题

助手翻了这个渠道的历史记录,发现是两个问题叠在一起:

  • 官网的岗位列表接口,有时会直接返回 HTTP 501。
  • 能访问的时候,又出现了跨页重复。官网声称有 1,056 个岗位,实际取回 1,056 行,但只有 1,051 个不同的岗位。9 月 18 日同一个渠道曾经完整采到过 1,042 个。

501 的表现很奇怪。助手用同一个脚本、同一组参数,在不同的地方各请求了一次。

请求发出的地方列表接口分类目录岗位详情
我的 Mac(早上 8 点多)200,官网声称 1,079 个岗位200200
生产服务器,同一个脚本501200200
我的 Mac 上正常打开官网页面自己发出的列表请求 200,显示 68 页200没有单独测

生产服务器上的 501 返回得很快,大约 0.07 秒,是一页标准的 501 Not Implemented,没有 Retry-After。分类和详情接口都正常,只有列表出问题。

到这里我以为是服务器出口的问题,但后来的测试把这个判断推翻了。一段时间之后,本机、生产服务器,甚至正常打开官网页面的浏览器,列表请求全都开始返回 501,旧版页面和新版页面都一样。助手又试了几种办法:在生产服务器上用真实的浏览器上下文发请求、加载整个官网页面再采集、显式使用我本机的代理,结果都没有变化。

所以我的判断是,最初“生产出口被区别对待”这个说法,只在那个时间点是对的,不能当成原因。到底是官网网关的策略、地域规则还是别的什么,从外面无法判断。

没有去绕过它

助手排查时始终守着一条界线:不去绕过验证码、登录或站点限制,不使用代理池,也不伪装浏览器指纹。官网在公开页面上自然发出的请求和我们的采集请求参数完全相同,没有发现需要登录或额外签名。无法确认的原因,就如实写成无法确认。

我们自己的两个缺陷

上游的 501 没法解决,但助手在抽查详情接口的真实响应时,发现我们自己的解析器也有问题。它抽了列表首、中、尾三个岗位,全部复现:

  • 详情里不再返回“招聘类型”这个字段,而我们的解析器把它当成必填项。
  • 发布时间和刷新时间改成了毫秒时间戳,解析器只接受日期文本。

也就是说,即使列表接口恢复,采集依然会在解析阶段失败。官网同时还有新旧两套前端:旧入口的参数是 page、recruitType、size,新版入口的参数不同,也没有稳定的排序参数。这和我们观察到的“发布时间相同的岗位在分页里会交换顺序、出现跨页重复”是吻合的。

修复分成几步:兼容两种时间格式,招聘类型改为根据官网明确的社招入口来识别,原始响应照原样保存。

用早上的数据恢复当天

更棘手的是当天的数据。早上 8 点半左右,助手保存下来了一份完整的列表,68 页,1,079 个岗位。能不能用这份列表,加上实时抓取的详情,恢复当天的数据?

这是一个需要很谨慎的操作,因为不能拿“多轮不完整的并集”冒充一次完整的结果。助手给恢复脚本定了严格的条件,不满足就不入库:

  • 列表必须来自同一个上海自然日,且在 4 小时内。
  • 原列表每页的哈希必须和保存时一致,状态码 200,页码和页长正确,总数吻合,没有重复,前后第一页的内容一致。
  • 分类目录和全部 1,079 个岗位的详情都实时重新读取,不使用缓存。
  • 列表和详情里的编号、名称、职责、要求、部门和工作地点,必须逐条完全一致。

第一次恢复实验失败了,而且失败得很有意思。它在 61168 号岗位上被拦住,原因是实验脚本里临时加了一条原数据契约里没有的假设,要求所有列表成员的状态都必须是“在招(2)”,而这个岗位的状态是 3。助手去看了新版官网的前端代码,发现 jdStatus 不等于 2 只是让“投递”按钮不可点,岗位详情依然公开,浏览器里打开标题、职责和要求都和列表完全一致。

而项目的正式连接器从来不依据这个状态去删除列表里的岗位,只是原样保存状态值。所以问题出在实验脚本多加的这条假设上,不是数据有问题。助手把这条假设撤掉,保留状态 3 的原值,在报告里单独统计各种状态的数量,其余检查一条不少,然后重新读取了全部详情,没有混用第一次的结果。这件事也写进了渠道文档。

第二次全部通过,1,079 个岗位正常入库,原始证据和之前的失败记录都保留着。入库时有不少岗位被识别为内容变更,助手追查后确认这是官网的变化:新版详情不再返回“招聘人数”,系统把它记为“未提供”,而不是沿用旧值。

之后是后续流程:1,079 个岗位的画像在并发 3 的限制下重新生成,期间有几次模型输出被长度截断,或者缺少必填信息,都被拒绝并自动重试,没有把不合格的结果当作成功画像保存。语义索引也更新到了 69,702 个岗位,大部分向量直接复用,只新增编码了 43 份。

给其他渠道的保护

既然上游的 501 修不了,助手顺带在共用的采集层加了一项保护。以前一旦网关持续返回错误,采集器会在同一批次里反复重试整个渠道,多层重试还会叠在一起,对方的网关本来就不稳定,还密集地重复请求显然不合适。现在连续的网关类失败会被标记为“稍后重试”,当次不再密集重试,保留失败证据,按调度在之后补试,也不影响其他渠道。

结果,以及没有解决的部分

当天结束时,95 个渠道全部完整,共 69,702 条岗位观测。出行公司这个渠道的 1,079 个岗位、画像和索引都已恢复。

但最后一次复查,官网列表接口仍然返回 501。数据是靠早上保存的完整列表加实时详情恢复的,这条路只在“早上碰巧采到过完整列表”的情况下才走得通,上游不恢复,之后每天都会失败。

长期失败的渠道怎么办

恢复数据的几天之后,我又想起一个问题:如果一个渠道连着失败很多天,应该怎么处理?

助手检查后发现一个隐患。失败的渠道会一直沿用它最后一次完整的数据,其他渠道照常更新,这在短期没有问题,但没有任何期限。渠道如果连续失败一个月,一个月前的岗位仍然会参与列表、查询和简历匹配,这会误导用户。

它给的建议是按距离最后一次完整采集的时间分档,天数允许管理员在后台调整:

距最后一次完整采集处理
1 到 2 天保留原数据,有限补试,后台记录失败原因
3 到 6 天升级管理员告警,减少无效补试
7 天以上暂时退出默认列表、简历匹配和当前统计,保留历史数据
14 天以上标记为待修复,降低探测频率,管理员可补跑、暂停或恢复

有几条原则我很认同。采集失败不代表岗位下架,所以过期只是暂停作为当前数据展示,不删除岗位,也不生成虚假的下架记录。渠道恢复后必须拿到完整的快照才能重新展示,一页成功不算。渠道退出或恢复引起的数量变化,不能被解释成招聘需求的下降或增长,趋势继续只使用可比的日期。

我只补了一条要求:普通用户最好不要看到任何提示,只给管理员看。最后上线的默认规则是 3 天告警、7 天退出当前展示、14 天降低重试频率,支持单个渠道单独设置,渠道恢复并完整采集成功后会自动重新展示。失败原因、受影响的岗位数和重试安排都放在管理后台的“渠道恢复管理”里。

这个渠道后来还是一直在失败。到 9 月 28 日,我猜可能和服务器的出口 IP 有关,就让助手先不要管它,把精力放在检查其他渠道的数据质量上。按上面的默认规则,它连续失败超过 7 天之后,就会退出当前展示。

这次排查值得带走的几点

一是先隔离影响,再追原因,页面上的误导信息比渠道失败更让人困惑。二是同一个请求在不同地方结果不一样,只能说明当时存在差异,不能直接当作原因。三是给恢复脚本加检查条件时,每一条都要有数据契约作依据,多加一条自己想当然的假设,就可能把真实的数据当成错误。

评论