ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从3到100+平台:多平台内容分发系统的架构重写与工程实践

从3到100+平台:多平台内容分发系统的架构重写与工程实践

好久没动这个项目了,前几天把仓库拉下来跑了一遍,看到配置目录里静静躺着的一百多个平台描述文件,说实话有点恍惚。两年前它还是三个平台、一个 for 循环、加一堆 if-else 的脚本,跑在我自己的机器上,谁要发稿子就喊我一声。现在它变成了一个能覆盖长文平台、图文社区、短视频平台、问答社区、音频平台等一百多个渠道的一键分发系统,从"写完稿子复制粘贴一晚上"变成了"提交一次、跑完看报表"。

这篇想聊的不是"多平台发布有多爽",而是这里面真正难的部分:当平台数量从 3 涨到 100 多,架构上到底发生了什么变化,哪些地方是必须重写的,哪些地方是可以用配置糊过去的,以及我在这个过程里踩过、被坑过、后来写进检查清单的那些细节。如果你正在做一个内容多平台分发的工具,或者你手上任何一个系统正在经历"接入方从个位数涨到三位数"的阶段,这里面的经验大概率能直接抄。文章里不会给出某个具体平台的对接细节,讲的是那套能被复用的骨架和判断。

1. 从三五个平台到一百多个:这套东西是被什么逼着重写的

1.1 最早的脚本版:三个平台、一个 for 循环、一堆 if

最开始的需求特别朴素:一篇稿子,发到三个地方,省得手动复制。于是我写了一个函数,参数是稿件路径,函数体里三个 if 分支,每个分支里塞一段专门为那个平台写的拼接逻辑。跑起来没问题,改起来也还好,因为只有三个。代码大概是这个味道:

def publish_all(draft): if "platform_a" in targets: body = draft_to_markdown(draft) api_a.post(title=draft.title[:30], content=body) if "platform_b" in targets: body = draft_to_html(draft) api_b.submit(title=draft.title, html=body, tags=draft.tags[:5]) ...

这种写法在前十个平台之内其实是可以忍的,因为每次新增只是一段代码,虽然丑但直观。它的问题不是"不优雅",而是新增成本是线性的,而维护成本是超线性的。你想想,三个平台的时候,改一次公共逻辑要改三处,还能接受;到第十五个平台,改一次公共逻辑要改十五处,而且每一处的上下文都不一样,改完还得逐个回归。

1.2 崩溃点出现在第 12 个平台左右

我印象很深的转折点是接到第 12 个平台的对接需求。那次的需求是"这个平台要支持定时发布,但定时时间必须是整点之后 5 分钟,而且封面必须是 3:4"。听起来就是个特殊逻辑,加个 if 就完了。但加上去之后我发现,之前有另一个平台是"封面必须 16:9 且不能超过 2MB",再往前还有个平台"标题按字节算不能超过 60 字节",也就是说中英文混排的时候字数根本不是固定的。

这些规则散落在十几个 if 分支里,没有任何地方能一眼看全。更要命的是校验:稿子提交上来的时候,我得知道"这篇稿子在这 12 个平台里,哪几个是发不了的、为什么发不了"。在老结构里,唯一的办法是真的去调一遍接口,让对面报错。这就导致一个很蠢的循环:用户提交 → 跑到第 7 个平台失败 → 前面 6 个已经发出去了 → 用户改稿 → 重新提交 → 前面 6 个重复发。

这就是压垮旧架构的最后一根稻草:没有发布前的全局校验能力。一旦有平台是"发出去就撤不回"的,你就必须在提交之前把所有平台的适配性问题全部算出来,让用户一次性改完。这个能力在 if-else 结构里几乎不可能做好,因为校验逻辑和发布逻辑是缠在一起的。

1.3 重写时定下的第一条原则:平台是数据,不是代码

重写的核心决定就一句话:把"某个平台长什么样"从代码里挪出来,变成一份可读的描述文件。代码只保留"怎么按描述去执行"的通用能力。

这个决定带来的直接好处是显而易见的:新增一个平台,理想情况下不需要改代码,只加一个配置文件;平台的规则变了(比如标题上限从 30 改成 40),改一行 YAML 提交 PR 就行,不需要重新理解那段 if 分支在干什么;更重要的是,因为所有平台的描述都在同一个结构下,我可以写一个通用的校验器,把一篇稿子对着全部平台的描述跑一遍,输出一份"哪些能发、哪些要改、改哪里"的报告。

当然,实际做下来没有这么理想,总有一些平台的规则没法用声明式描述表达(后面会讲),但即使是这样,能做到 80% 配置化、20% 代码,也比 100% 代码强太多。因为那 20% 的例外是被清楚地隔离出来的,你知道它们在哪里,也知道新加平台的时候大概率不用碰它们。

这条原则其实适用于所有"接入方数量会持续增长"的系统:支付渠道、物流承运商、短信服务商、第三方登录,都是同一个套路。判断标准很简单——如果新增一个接入方需要改核心代码,那架构就是有问题的。

2. 一个平台到底有多少不一样:把差异做成能力矩阵

2.1 我实际记录下来的 14 个差异维度

刚开始我以为差异无非是"这个平台支持 Markdown,那个不支持"。真做下来才发现,差异是立体的。我后来整理了一张表,把实际遇到过的差异分成四类,一共 14 个维度:

类别维度典型差异
字段限制标题长度按字符 / 按字节 / 按 UTF-16 单元
字段限制正文字数有上限 / 无上限 / 按图片折算
字段限制标签数量上限、单个长度、是否必填
字段限制摘要平台不自动截取,需显式提供
内容能力富文本节点是否支持表格、代码块、脚注、分割线
内容能力标题层级有的只支持到三级
内容能力外链允许 / 转纯文本 / 直接拒绝
媒体能力图片数量3 张 / 20 张 / 无限制
媒体能力图片规格大小上限、格式白名单、防盗链
媒体能力封面比例、尺寸、是否必填
媒体能力视频时长、码率、是否要单独转码
发布行为定时支持 / 不支持 / 只支持整点
发布行为审核先审后发 / 先发后审 / 无审核
发布行为修改发布后能否编辑、编辑是否重新审核

这张表的价值不在于它多全,而在于它给了我一个检查清单。后来每接一个新平台,我就照着这 14 个维度过一遍,把答案填进去。填空的过程本身就是对接口文档的一次有效阅读——很多时候文档里没写清楚的地方,正是你填不下去的那一格,那就得去问、去试。

2.2 能力矩阵的落地形式:一份 YAML 加一个校验器

描述文件最后长这样,字段名尽量直白:

platform: platform_a display_name: 某长文社区 adapter: rest_v2 limits: title_unit: byte # char / byte / utf16 title_max: 60 body_max: 20000 tag_max_count: 10 tag_max_chars: 12 summary_required: true features: scheduled: true scheduled_align_minute: 5 editable_after_publish: false cover_ratio: "3:4" cover_required: true content: support_nodes: [paragraph, heading, image, list, quote, code_block] drop_nodes: [table, footnote, hr] degrade: table: image footnote: endnote_list external_link: forbidden auth: type: oauth2 token_ttl: 7200 refresh_before: 900

有了这个结构,校验器就变成一个纯函数:validate(draft, capability) -> list[Issue]。它遍历所有目标平台,对每个平台跑一遍检查,产出一份按平台分组的报告。用户在提交前就能看到"平台 C 标题超了 12 字节、平台 F 不支持表格已自动转图、平台 H 外链会被删除"。

这里有个实际的取舍值得说:校验要严到什么程度?我的做法是分成两级,blocker和warning。blocker 是那种一定发不出去的(标题超长、封面缺失、正文为空),必须改;warning 是"能发但效果会打折"的(表格被降级、外链被删、标签被截断),提示但不拦。一开始我把所有问题都设成 blocker,结果用户被拦得死死的,体验极差;后来改成只有真正会失败的才拦,反而没人抱怨了。

2.3 矩阵之外,那些没法描述的差异

必须承认,有一部分平台的规则是描述不了的。我遇到过几类:

一种是跟内容语义相关的审核规则。比如某平台对"导流意图"的判定,你很难写成一条规则,只能靠人。这类东西我的处理是不去建模,把它归到"发布后需要人工回查"的流程里。

一种是隐式的上下架规则。比如某平台对同一账号连续发布有频率限制,但这个限制不写在文档里,是运行中撞出来的。这类只能靠观测积累,把它变成运行时的限流参数,而不是配置里的静态字段。

还有一种是接口行为的不一致。比如同样是"上传图片",有的平台是先申请上传凭证再 PUT 二进制,有的是直接 multipart 表单一次搞定,有的是先传到一个临时地址再引用。这些差异在适配器代码里消化掉,不进配置。

所以说,"平台是数据"这条原则做到 80% 就够了,剩下 20% 用代码兜住但必须集中放在适配器里,不能漏回主流程。

3. 适配器接口只留四个方法,但每个都有明确的边界

3.1 接口设计:validate、transform、publish、query

抽象层最后收敛成四个方法,我把它定义成协议:

class Adapter(Protocol): capability: Capability def validate(self, draft: Draft) -> list[Issue]: ... def transform(self, draft: Draft) -> Payload: ... def publish(self, payload: Payload) -> RemoteRef: ... def query(self, ref: RemoteRef) -> RemoteState: ...

为什么是这四个?因为它们正好对应发布链路的四个阶段,而且每个阶段的失败后果不一样,分开之后重试策略也能分开设计。validate是纯计算,不产生副作用,可以随便重跑;transform也是纯函数,同样的输入永远产出同样的 payload,这个特性后面做幂等键的时候会用到;publish是唯一有副作用的,必须幂等保护;query是用来对抗"假成功"的,后面单独讲。

我刻意没有加update和delete。不是不需要,而是这两个操作的平台差异太大——有的平台发出去就不能改,有的改了要重新审核,有的删了还留个"已删除"的壳。硬把它们塞进统一接口,最后一定会变成一堆if capability.support_edit的判断。现在的做法是把它们放成可选的扩展方法,用能力位控制,调不到就明说"这个平台不支持"。

3.2 transform 的输出必须是中间表示,不能是 HTML 字符串

这一点我走了弯路。最早transform返回的是 HTML 字符串,想着"反正大部分平台都吃 HTML"。结果很快发现不行:有的平台吃 HTML 但会过滤掉<table>,有的平台只吃它自己的一套标记,有的平台吃 HTML 但会把<img src>换成自己的 CDN 地址,还有的平台对 HTML 里的空白处理方式完全不一样。

一旦输出是 HTML 字符串,你就失去了结构信息,后面想按平台能力做降级都无从下手——你没法从一个字符串里可靠地判断"这里原本有个表格"。

所以最终transform返回的是一个中间表示(IR),本质上是 Markdown AST 的一个裁剪版,节点类型被我限制在十几个之内。降级逻辑全部在 IR 层做,做完之后再序列化成目标平台要的格式。这样"表格转图片"这类降级就是一次 AST 变换,干净可测。

3.3 假成功:返回 200 但线上什么都没有

这是所有多平台发布系统里最恶心的一类问题。接口返回成功,你记了个 remote_id,任务状态标成"已发布",结果去线上一看,什么都没有。可能的原因有很多:进了审核队列但审核层面静默失败了、内容被判定为重复所以没上、接口内部异步处理失败但没回传、甚至就是对面的 bug。

纯靠 publish 的返回值是发现不了的,所以query这个方法不是可选项,是必需品。我的做法是:发布成功后不立刻标记终态,而是进入一个"待确认"状态,由定时任务按退避策略回查(比如 2 分钟、10 分钟、1 小时、6 小时各查一次),查到线上确实存在才转成"已发布";超过 24 小时还查不到,转成"疑似失败"并告警。

def confirm_loop(ref): for delay in [120, 600, 3600, 21600]: sleep_until(ref.created_at + delay) state = adapter.query(ref) if state == "online": return mark_published(ref) if state == "rejected": return mark_failed(ref, reason=state.reason) return mark_suspect(ref)

这套机制上线之后,"莫名其妙没发出去"的投诉少了一大半。代价是多了一倍的接口调用量和一套状态机,我认为完全值得。

4. 授权和账号:一百个平台里,一半的坑埋在这

4.1 令牌刷新要抢在过期之前,而且要考虑失败

多平台发布绕不开授权。一百多个平台,授权方式五花八门,但主流就是那几种:长期令牌、短期令牌加刷新令牌、带签名的请求。真正麻烦的不是首次授权,是长期运行中的令牌续期。

我踩过的第一个坑是刷新时机。最开始是"请求返回未授权了再去刷新",看起来很合理,但实际会造成两个问题:一是每次刷新都伴随着一次失败请求,日志里全是噪音;二是如果刷新本身也失败(比如刷新令牌也过期了),那次发布就彻底失败了,而且失败得很晚。

后来改成提前刷新:配置里记token_ttl和refresh_before,后台任务在剩余寿命小于refresh_before的时候就主动刷新。refresh_before我一般设成 ttl 的 10% 到 20%,比如 ttl 是 7200 秒就提前 900 秒刷。留这个余量的原因是刷新请求本身可能失败,你得有重试窗口。

def maybe_refresh(cred): if cred.expires_at - now() < cred.refresh_before: try: new = refresh(cred) store(encrypt(new)) except TransientError: schedule_retry(cred, backoff=60) except FatalError: mark_need_reauth(cred) # 通知用户重新授权

mark_need_reauth这个状态很重要。刷新令牌彻底失效是必然会发生的(用户改了密码、撤销了授权、平台策略变更),系统必须能优雅地告诉用户"这个平台的授权掉了,去重新连一下",而不是让任务一直失败重试。我在后台做了个"授权健康度"页面,把所有快过期和已失效的授权列出来,这个页面的使用频率比我预想的高很多。

4.2 一个账号下有多个身份,别拿账号名当主键

这个问题在矩阵运营的场景下特别明显:同一个登录账号下可能挂着多个发布身份(不同栏目、不同子账号、不同站点)。早期我拿"平台 + 账号名"当唯一键,结果发现同一个账号名在同一个平台下能对应好几个发布目标,数据全乱了。

正确的主键应该是"平台 + 授权主体 + 发布目标 ID"三元组,其中发布目标 ID 是平台返回的那个真正用来发内容的东西。这个 ID 才是稳定的,账号名会改,昵称会改,只有 ID 不变。

binding: platform: platform_b auth_subject: user_12345 # 授权主体 target_id: column_678 # 真正的发布目标 target_name: 某栏目 # 仅用于展示,随时可能变

改了主键之后还有个连带好处:同一个授权可以绑定多个发布目标,用户授权一次就能管一批账号,不用重复授权。这个体验提升是很实在的。

4.3 权限是静默变更的,别假设授权一次就永远够用

最隐蔽的一类问题是权限变更。用户在平台上调整了自己的设置,或者平台调整了接口的权限范围,你手上的令牌还是有效的,但某些操作就是会失败,返回的还是个模糊的错误码。

这类问题没法预防,只能靠失败分类来兜。我在适配器里给每个平台的错误码建了一张映射表,把平台返回的错误码翻译成内部的几种语义:auth_expired、permission_denied、rate_limited、invalid_param、content_rejected、unknown。其中permission_denied单独拎出来,触发"重新检查授权范围"的流程,而不是简单地重试。

这张映射表是纯体力活,但收益极高。有了它之后,所有的失败统计、告警、自动处置都能建立在统一的语义上,而不是每个平台一套 if。

5. 内容转换成一百份稿件:降级策略比转换能力更重要

5.1 从 Markdown AST 到各家格式的映射与降级

前面说了 transform 输出 IR,这里说说降级。核心思路是:先按平台能力做一次能力裁剪,再做一次节点降级,最后序列化。

裁剪是去掉平台完全无法理解的东西,降级是把不支持的东西替换成能表达同等意思的替代品。我维护了一张降级表,实际用下来覆盖了绝大多数情况:

源节点平台不支持时的处理说明
表格渲染成图片保留信息完整性,代价是不能复制
脚注转成文末编号列表语义基本无损
分割线直接删除视觉影响小
四级及以下标题转成加粗段落层级感弱化但可读
代码块转成引用块并保留缩进语言高亮丢失
外链转纯文本并附提示平台禁止外链时的唯一选择
内嵌视频转封面图加链接部分平台不支持正文嵌视频

这里有个经验:降级要做成"可解释的"。用户看到的不是"这篇稿子在某平台少了点东西",而是"某平台不支持表格,已自动渲染为图片"。前者会让人怀疑系统,后者会让人信任系统。所以每个降级动作都要在报告里留痕,我在 Payload 里塞了一个transform_log字段专门记这个,虽然平台用不上,但给用户看。

5.2 图片的三件事:转存、压缩、裁切

图片处理是另一个重灾区,因为几乎每个平台对图片都有自己的要求,而且很多平台的图片地址带防盗链,你直接把源图地址塞过去,用户看到的就是一张裂图。

第一件事是转存。我的做法是所有图片统一先落到自己的对象存储,生成一个稳定可访问的地址,再拿这个地址去对接各平台。有些平台会自己去抓这个地址然后转到它自己的 CDN,有些平台只接受上传二进制,那就走上传流程。两种情况都要能处理。

第二件事是压缩。不同平台的大小限制差异很大,从 2MB 到 10MB 都有。我的默认策略是长边不超过 1440 像素、质量 82,这个参数在绝大多数场景下肉眼几乎看不出差别,体积能压到原来的三分之一左右。如果目标平台限制更严,再按需降一档。

第三件事是裁切,主要是封面。封面比例各家不同,16:9、3:4、1:1 都见过。直接拉伸会变形,我的做法是居中裁切,并且在裁切前先做一次主体检测——如果检测不到明显主体就居中裁,检测到就按主体位置偏一点。这一步不是必须的,但做了之后封面"把人脸裁掉一半"的情况基本没有了。

5.3 字符计数这种"小问题"最能磨人

这个必须单独拎出来说,因为它坑了我最久。标题长度限制看起来简单,实际上有三种算法:

  • 按字符数(一个汉字算 1)
  • 按字节数(UTF-8 下一个汉字算 3)
  • 按 UTF-16 单元数(一个 emoji 算 2,一个汉字算 1)

同样的一个标题,在三种算法下的长度能差三倍。更坑的是有些平台文档写的是"字符",实际校验用的却是字节。我最后的处理是:配置里显式声明title_unit,校验器按声明的单位算;同时对每个平台维护一个"实测值",跑测试的时候用超长标题去试,把真实上限记下来,跟文档对不上就以实测为准。

def count(text, unit): if unit == "char": return len(text) if unit == "byte": return len(text.encode("utf-8")) if unit == "utf16": return len(text.encode("utf-16-le")) // 2

6. 调度、限流与重试:把"提交了"变成"线上真的有"

6.1 配额到底是账号级还是应用级,先搞清楚再设计限流

限流这块,最容易犯的错是假设所有平台的配额维度一样。实际上至少有三层:

  • 应用级:你整个系统对这个平台的调用总量
  • 账号级:单个授权账号的调用量
  • 接口级:具体某个接口单独的限制

我有一次就是把配额当成应用级来设计的,结果一个矩阵账号批量发了 40 篇,把账号级配额打满,导致另一个业务线的账号也被牵连(因为它们在同一个授权主体下)。后来改成三层都要考虑,用令牌桶在每一层各放一个桶,请求要同时通过三层才放行。

buckets = [app_bucket(platform), account_bucket(platform, subject), api_bucket(platform, endpoint)] for b in buckets: if not b.try_acquire(): raise RateLimited(retry_after=min(b.wait_time() for b in buckets))

另外一个实用技巧是加抖动。定时发布如果都卡在整点,很容易形成瞬时洪峰。我的做法是在目标时间前后加一个随机偏移(±90 秒),把流量摊平。这个偏移对用户完全无感,但对系统的稳定性帮助很大。

6.2 幂等键怎么设计,才能既不重复发也不漏发

重复发布是比失败更严重的问题,因为失败还能重试,重复发出去的内容要一条条去删。

幂等键的设计我用的是这个组合:

idempotency_key = sha256(platform + target_id + draft_version + slot_time)

四个要素各有作用:platform和target_id定位发布目标,draft_version保证内容改了之后算新任务(否则改稿重发会被当成重复),slot_time保证定时任务在同一个时间槽内只发一次。

光有键还不够,还得有本地状态表配套:

状态含义重试行为
pending已入队未执行可直接执行
inflight正在调用接口不允许并发执行,需等待超时
published已确认线上存在拒绝重复执行
failed终态失败拒绝重试,需人工介入
suspect疑似成功待确认只允许 query,不允许 publish

inflight这个状态特别关键。没有它,网络超时的时候你不知道请求到底有没有到达对面,重试可能造成重复。有了它之后,超时的任务先转inflight挂着,等query结果出来再决定,而不是盲目重试。

6.3 失败分三类,处理方式完全不同

把失败分类是让系统"不吵人"的关键。我的分类和处置是这样的:

类型典型场景处置策略
瞬时失败超时、5xx、限流指数退避重试,最多 3 次,间隔 30s/2m/10m
终态失败参数非法、权限不足、内容被拒不重试,落失败,聚合告警
需人工风控验证、二次确认、验证码挂起,通知人工处理
疑似成功返回成功但回查不到转 suspect,定时回查

这个表看起来简单,但实施的时候有一个细节很容易漏:重试必须重新走 validate 吗?我的答案是瞬时失败不走(因为内容没变,校验结果也不会变),但如果失败原因里包含参数类问题就必须走。为了避免这个判断出错,我在重试入口加了断言:如果上次失败语义是invalid_param,直接拒绝重试。

7. 观测、灰度与新增一个平台的真实工时

7.1 成功率看板要能回答三个问题

多平台系统上线后,看板是每天都要看的。我一开始做的看板很花哨,各种维度都有,但实际没用——因为它回答不了我最关心的问题。后来砍到只剩三个必答问题:

第一,哪个平台今天成功率掉了?按平台维度的成功率折线,掉超过 5 个点就要查。这个指标能第一时间发现对面接口变更。

第二,失败原因的 Top N 是什么?按内部语义聚合的失败原因排行。如果rate_limited突然涨,说明配额策略失效;如果permission_denied涨,说明授权批量失效了。

第三,从提交到线上可见的中位耗时是多少?这个指标反映的是真实体验。我遇到过一次中位耗时从 3 分钟涨到 40 分钟,查下来是某个平台的审核变慢了,但我们的回查退避策略太保守,一直以为自己没发出去在反复查。这个指标不单独看是发现不了的。

看板之外还有一个低成本但极其有效的机制:每日探针。用固定的测试账号、固定的测试内容,每天对每个平台跑一次端到端发布,成功就删掉。这条链路跑在独立环境里,不受业务流量影响,是发现"接口悄悄变了"最灵敏的手段。

7.2 新平台接入清单:照着填就不会漏

接入一个新平台,我固定走这套清单,时间久了就变成肌肉记忆:

  1. 读接口文档,把能力矩阵的 14 个维度填满,填不出来的标记为待验证
  2. 用测试账号跑一次手动发布,记录真实的字段要求(尤其注意必填字段和可选字段的实际行为差异)
  3. 用超长标题、超多标签、含表格的正文做一次"过载测试",确认实测限制
  4. 确认授权方式,把 ttl、刷新方式、失效表现记录下来
  5. 确认图片处理方式:是接受 URL 还是必须上传,是否要求特定尺寸
  6. 确认返回结构:成功返回什么、失败返回什么、错误码有没有文档
  7. 确认发布后的可见性:是立即可见还是延迟,审核状态怎么查
  8. 写适配器,跑一遍单元测试和探针
  9. 内部账号灰度发 20 篇真实内容,观察 3 天
  10. 放开给全部用户,加进每日探针

第 3 步的"过载测试"我想多说一句。很多平台文档里写的限制和实测不一致,尤其是字节vs字符这种。与其相信文档,不如用一篇明显超规的内容去撞一次,把报错信息记下来,这个信息是最准的。

7.3 工时从两天压到三个小时的真正原因

最开始接一个新平台要两天,现在熟手三个小时能搞定,我复盘了一下,压缩主要来自三个地方:

一是模板化的目录结构。每个适配器就是一个目录,里面固定有capability.yaml、adapter.py、fixtures/、test_adapter.py。新建的时候直接复制模板,改内容就行,不用想"我该放哪"。

二是共享的测试夹具。fixtures/里有一组标准稿件:纯文本的、带表格的、带多级标题的、带 20 张图的、超长标题的、带 emoji 的。每个新适配器跑一遍这组夹具,能自动暴露大量的适配问题。这个投入在第三个适配器的时候就开始回本了。

三是错误码映射脚本。之前每个平台都手工写错误码映射,后来做了个半自动的东西:把平台的错误码文档粘进去,生成一份待确认的映射草稿,人工只做校对。这一步从两小时压到二十分钟。

至于剩下的那三个小时,主要花在读文档和实测验证上,这部分是没法省的,也不该省。

最后说一个我个人判断上的变化。做这件事之前,我总觉得"支持更多平台"是个数量问题,多一个平台多一份工作量,线性增长。真做到一百多个之后才明白,它其实是个结构问题:平台数量越多,通用能力的价值越高,因为每次优化都是对全部平台生效的。反过来,任何一处"这个平台特殊,单独写一段"的技术债,都会被一百倍地放大。所以现在我定新平台时的第一反应不是"这个怎么实现",而是"这个能不能被现有的描述结构表达出来"——如果不能,那大概率是结构需要扩,而不是该写特例。

7.4 关于"支持 100 多个平台"这个数字本身

最后聊点稍微不那么技术的。这个数字对外看挺唬人的,但实际跑起来,真正高频使用的平台可能就十几个,剩下的大部分是长尾。不过长尾的价值在于覆盖面:用户的需求是分散的,你觉得冷门的渠道,可能就是某个人的主战场。所以对这些低频平台,我的优化目标不是性能,而是"别出问题、别维护不动"。具体做法是低频平台的适配器不做特殊优化,共用通用链路,探针频率降到每周一次,出问题就降级成"暂不可用"而不是死磕修复。

这个取舍我也纠结过,后来想通了:一个一百多个平台的分发系统,最重要的能力不是"每个平台都完美支持",而是"任何时候都知道哪些平台能用、哪些不能用、为什么不能用"。透明度比覆盖率更值钱。

返回列表