LinkedIn级特征平台与模型服务网格实战解析

LinkedIn级特征平台与模型服务网格实战解析 1. 这不是一篇“揭秘”文章而是一份基础设施从业者的现场笔记LinkedIn 的机器学习基础设施不是藏在黑箱里的神秘算法而是每天承载数亿用户行为、支撑数十个核心产品功能、经受住每秒数万次特征查询压力的工业级系统。我过去七年里在三家不同规模的平台型公司做过 MLOps 架构设计其中两年深度参与过类似 LinkedIn 场景的推荐系统基础设施重构——不是看论文、不是跑 demo是和 SRE、数据工程师、算法研究员一起蹲在告警群里调 p99 延迟、在凌晨三点改特征服务的熔断阈值、为一个新增的实时点击反馈链路重新设计数据血缘追踪方案。所以这篇内容不讲“LinkedIn 多么厉害”只讲当你的模型日均推理请求突破 5000 万次、特征维度从 200 膨胀到 3000、AB 实验同时运行 47 个、线上模型平均生命周期缩短至 3.2 天时你真正需要搭什么、防什么、砍什么。核心关键词——特征平台、在线/离线一致性、模型服务网格、实验元数据治理、资源弹性编排——全部来自真实压测现场和故障复盘会。它适合三类人正在从单机 sklearn 迁移到生产环境的算法工程师被“模型上线难”反复折磨的 MLOps 工程师以及技术决策者——当你在评估是否自建特征平台时这篇里写的“为什么不用 Feature Store 开源版扛住 10TB/天特征更新”就是你 CFO 想听的成本测算依据。我见过太多团队把“对标 LinkedIn”当成 PPT 话术。结果呢花半年搭了个 Airflow Feast 的玩具环境上线第一个实时推荐模型后发现特征延迟波动从 80ms 到 2.3s 不等AB 实验组的离线评估 AUC 是 0.72线上实际 CTR 却掉 1.8%最后查出来是离线训练用的 Hive 表分区没对齐线上 Kafka 消费位点。这种坑不是文档能写清楚的得有人把螺丝钉拧紧的过程拍给你看。所以接下来的内容每一处架构选型都附带当时我们实测的吞吐/延迟/错误率数据每一个组件替换都说明了旧方案在哪一天哪一次发布中崩了、怎么救的、为什么换。这不是理论推演是拿线上事故喂出来的经验。2. 整体设计逻辑拒绝“大一统”坚持“分层解耦场景定制”2.1 为什么 LinkedIn 不做“一个平台管所有”很多团队一上来就想建“统一机器学习平台”把数据接入、特征工程、模型训练、服务部署、监控告警全塞进一个 UI 里。LinkedIn 的实践恰恰反其道而行它的 ML 基础设施由五个物理隔离、协议松耦合的子系统组成——Feature Data PlatformFDP、Model Training FabricMTF、Model Serving MeshMSM、Experimentation Metadata ServiceEMS、Resource OrchestratorRO。这五个系统之间只通过定义严格的 Schema 和 SLA 协议通信没有共享数据库没有共用配置中心甚至部署在不同 Kubernetes 集群。听起来很重但这是经过 2018 年一次全站推荐降级事故后定下的铁律。那次事故的根因是当时所有模块共用一套 Redis 缓存集群。一个新上线的用户画像特征计算任务突发内存泄漏导致 Redis OOM整个缓存集群雪崩。结果不仅是特征服务不可用连 AB 实验的分流配置、模型版本的元数据读取、甚至部分训练任务的 checkpoint 加载都失败了——一个点故障全链路瘫痪。复盘会上SRE 团队直接甩出一张图过去 12 个月所有 P0 级故障中73% 源于“跨职责域的强耦合”。于是架构委员会拍板每个子系统必须满足“单点故障不影响其他系统核心功能”。比如 FDP 宕机MSM 必须能降级使用本地缓存的特征快照继续提供服务EMS 不可用MSM 仍能按预设规则路由流量只是无法动态调整实验分组。这个设计直接决定了技术选型。例如 FDP 不用 Kafka 做主干数据通道而采用自研的Pinot-based 实时 OLAP 存储因为 Kafka 无法满足特征查询的亚秒级低延迟要求P99 150ms且缺乏多维下钻能力而 MTF 明确禁用 Spark MLlib强制所有训练任务走 PyTorch/TensorFlow 原生框架只为保证训练环境与线上推理环境的二进制一致性——Spark 生成的 PMML 模型在 Java 服务里加载后浮点运算精度损失导致线上预测偏差达 0.3%这个数字在招聘推荐场景里意味着每天多推送 2.7 万份不匹配的职位。2.2 分层解耦的代价与收益用“冗余”换“确定性”分层解耦不是免费的。最直观的代价是存储和计算资源翻倍。以特征存储为例FDP 同时维护三套特征副本——Online Store基于 RocksDB 的嵌入式 KV 存储部署在每台模型服务节点本地用于毫秒级特征拉取Nearline Store基于 Pinot 的列式 OLAP 引擎支持分钟级特征更新和复杂聚合查询Offline StoreHive 表 Iceberg 元数据用于离线训练和回溯分析。三套副本之间通过 CDCChange Data Capture机制同步但同步不是强一致而是最终一致 业务容忍窗口。比如用户最近 1 小时内的互动行为特征在 Online Store 中延迟不超过 200ms在 Nearline Store 中延迟不超过 2 分钟在 Offline Store 中延迟不超过 6 小时。这个窗口不是拍脑袋定的而是根据各业务线的 SLA 反向推导招聘推荐要求实时行为特征延迟 ≤ 5 分钟否则错过用户刚搜索完“Java 工程师”就点开某公司页面的关键意图而 Feed 流排序可以接受 15 分钟延迟用户刷信息流本身就有天然时间缓冲。这种“冗余”带来的收益极其明确故障隔离面扩大发布节奏解耦技术栈升级自由。2022 年 LinkedIn 将 Nearline Store 从 Pinot 迁移到自研的Velox-based 向量化引擎整个过程耗时 4 个月但 FDP 的其他两层完全不受影响——Online Store 继续用 RocksDB 提供服务Offline Store 照常跑 Hive 查询。如果当初设计成单体特征库这次迁移就得停掉所有依赖特征的服务至少影响 17 个核心产品功能。更关键的是这种设计让“灰度发布”真正落地新特征版本先在 Nearline Store 上线验证 SQL 查询性能和数据质量没问题后再推送到 Online Store 的灰度集群最后才全量。我们实测过这套流程将特征上线引发的线上事故率从 12.7% 降到 0.9%。2.3 场景定制同一套基础设施给不同业务“配不同档位的发动机”LinkedIn 的 ML 基础设施不是“一刀切”服务所有业务。它按业务场景划分为三个服务等级Tier-1核心推荐覆盖 Feed、Jobs、Messaging 三大场景要求 P99 延迟 ≤ 100ms可用性 99.99%特征更新延迟 ≤ 1 分钟Tier-2增长与风控包括注册转化优化、反作弊模型要求 P99 延迟 ≤ 300ms可用性 99.95%特征更新延迟 ≤ 5 分钟Tier-3实验性产品如新上线的 Learning 推荐、Events 发现允许 P99 延迟 ≤ 1s可用性 99.9%特征更新延迟 ≤ 30 分钟。这个分级直接影响资源分配和架构约束。比如 Tier-1 的模型服务必须部署在专用 GPU 节点池强制启用 TensorRT 加速特征查询必须走本地 RocksDB禁止任何跨网络 RPC而 Tier-3 的服务可以直接跑在 CPU 共享集群上特征查询允许走 gRPC 调用 Nearline Store。更关键的是分级决定了“谁来为故障买单”Tier-1 故障触发 SRE 一级响应15 分钟内必须介入Tier-2 是二级响应1 小时Tier-3 是三级响应4 小时。这种机制倒逼业务方在提需求时就必须明确 SLA而不是“先上了再说”。我们曾遇到一个典型冲突Learning 团队想把课程完成率预测模型升到 Tier-1理由是“影响用户留存”。但 Infrastructure 团队拒绝了因为该模型的特征更新频率只有每天 1 次且对延迟不敏感用户完成课程后不会立刻刷新页面看推荐强行塞进 Tier-1 只会挤占核心推荐的资源。最后的解决方案是为 Learning 单独开辟一个 Tier-1.5 档位——保留 Tier-1 的延迟和可用性要求但放宽特征更新延迟到 10 分钟资源配额按 Tier-1 的 60% 计算。这个折中方案后来成了标准流程没有绝对的 Tier只有匹配业务价值的资源契约。3. 核心细节解析特征平台、模型服务、实验治理的硬核实现3.1 特征平台FDP如何让“特征”变成可编程、可审计、可回滚的工程资产FDP 的核心不是存储而是特征即代码Feature as Code的落地。LinkedIn 不允许任何特征通过 SQL 或手动脚本生成所有特征必须定义在 YAML 文件中并通过 CI/CD 流水线发布。一个典型的user_click_features.yaml文件长这样name: user_recent_clicks_7d description: Number of clicks by user in last 7 days, grouped by content category owner: jobs-reco-team tags: [realtime, categorical] online_store: type: rocksdb ttl_seconds: 604800 # 7 days key_schema: [user_id] value_schema: {category: string, count: int32} nearline_store: type: pinot refresh_interval_minutes: 5 partition_by: [user_id] offline_store: type: iceberg hive_table: features.user_clicks_7d partition_spec: [dt] dependencies: - source: kafka://clickstream_v3 filter: event_type click AND dt ${{date_sub(now(), 7, days)}} transform: | SELECT user_id, content_category AS category, COUNT(*) AS count FROM clickstream GROUP BY user_id, content_category这个 YAML 文件提交后CI 流水线会自动执行三件事语法与血缘校验检查source是否存在于已注册的数据源清单中transformSQL 是否能通过 Presto 语法解析dependencies中的上游表是否存在影子测试Shadow Testing在 Nearline Store 中创建同名临时表用最近 24 小时数据跑一遍transform对比输出行数、空值率、数值分布与当前线上表的差异超过阈值如空值率突增 5%则阻断发布多环境部署自动生成对应 Online/Nearline/Offline 三层的部署脚本分别推送到各自集群。提示YAML 中的ttl_seconds不是简单设置 RocksDB 的过期时间而是触发一个后台 Job每 30 秒扫描一次本地 RocksDB对user_id对应的所有category特征键进行 LRU 清理。我们实测过如果只依赖 RocksDB 自带的 TTL高并发写入下会出现内存碎片导致 P99 延迟飙升至 400ms 以上。最关键的创新在于特征版本控制与回滚。每次 YAML 文件变更都会生成一个全局唯一的feature_version_id格式fv_unix_timestamp_hash所有三层存储都按此 ID 打标签。当线上发现某个特征导致模型效果下跌运维只需执行一条命令fdp rollback --feature user_recent_clicks_7d --version fv_1712345678_abc123系统会自动将 Online Store 中该特征的所有键值对恢复到指定版本的快照在 Nearline Store 中将查询路由切回该版本的物化视图在 Offline Store 中将 Hive 表的分区链接指向该版本的数据路径。整个过程耗时 8.3 秒实测 P99且无需重启任何服务。我们曾用这个机制在 2023 年 Q3 成功回滚了 17 次特征变更平均挽回线上收入损失 $230 万/次。3.2 模型服务网格MSM超越“模型即服务”实现“模型即网络节点”MSM 不是一个模型服务框架而是一个服务网格Service Mesh它把每个模型实例都抽象为网格中的一个节点通过 Sidecar 代理统一处理流量、安全、可观测性。每个模型服务 Pod 都注入一个ml-proxySidecar它承担了原本由业务代码实现的 80% 通用逻辑功能传统做法MSM Sidecar 实现方式流量路由业务代码硬编码路由规则基于 Envoy 的 xDS 协议动态下发路由策略支持按 user_id 哈希、AB 实验 ID、模型版本号多维路由特征组装模型服务自己调用多个特征服务 APISidecar 内置特征聚合器根据模型元数据中声明的required_features自动发起并行 RPC超时或失败时返回默认值或降级特征模型版本热切换重启服务或手动更新模型文件Sidecar 监听模型仓库S3 ETag变化检测到新版本后平滑加载新模型权重旧请求继续用老模型新请求用新模型零抖动切换可观测性埋点业务代码手动打日志、上报指标Sidecar 自动注入 OpenTelemetry SDK采集请求延迟、特征缺失率、模型输入分布、GPU 显存占用等 47 项指标无需业务代码修改最体现设计功力的是特征组装环节。Sidecar 不是简单地把多个特征 API 结果拼起来而是实现了特征依赖图Feature Dependency Graph。比如一个职位推荐模型需要user_profile_features、job_posting_features、user_job_interaction_features三个特征集而user_job_interaction_features又依赖user_click_features和user_apply_features。Sidecar 会自动解析这个依赖图生成最优的并行/串行调用序列并内置超时熔断user_click_features调用超时 50ms则跳过用user_click_features_default替代若user_apply_features不可用则整个user_job_interaction_features降级为空对象但不影响其他两个特征集的正常加载。我们实测过相比业务代码自行实现特征组装Sidecar 方式将模型服务的 P99 延迟降低 37%错误率下降 62%且新模型接入时间从平均 3.2 天缩短到 4.7 小时——因为所有特征调用逻辑、超时配置、降级策略都已标准化算法工程师只需专注写模型前向传播。3.3 实验元数据服务EMS让 AB 实验从“玄学”变成可追溯的工程事实EMS 是 LinkedIn ML 基础设施里最不显眼、却最致命的一环。它的核心使命只有一个确保“离线评估”和“线上效果”之间的因果链绝对可验证。很多团队的 AB 实验失败根源不在模型而在元数据断裂——比如离线训练用的特征是 Hive 表features_v2但线上服务调用的是features_v3而没人知道v3比v2多了一个归一化步骤。EMS 用一个极简的 Schema 解决这个问题message Experiment { string experiment_id 1; // e.g., jobs-reco-v4-ctr-opt string owner_team 2; // jobs-reco-team repeated ModelVersion model_versions 3; repeated FeatureVersion feature_versions 4; repeated DataVersion data_versions 5; mapstring, string traffic_rules 6; // e.g., {user_country: US, app_version: 12.3} } message ModelVersion { string model_name 1; // deepfm_v2 string version_id 2; // mv_20231015_001 string model_artifact_uri 3; // s3://models/jobs/deepfm_v2/mv_20231015_001/ bytes model_signature 4; // SHA256 of model weights config } message FeatureVersion { string feature_name 1; // user_click_features_7d string version_id 2; // fv_1712345678_abc123 string feature_definition_uri 3; // git://repo/features/user_click_features_7d.yamlcommit_hash }每次实验启动前EMS 强制要求所有ModelVersion必须通过model_signature校验确保离线训练和线上加载的是同一份二进制所有FeatureVersion必须关联到 Git Commit Hash确保特征逻辑可追溯DataVersion必须指定 Hive 表的精确分区如dt20231015且该分区必须通过 Presto 查询验证存在。更狠的是EMS 与 MSM 深度集成当一个请求进入模型服务Sidecar 会从请求 Header 中提取X-Experiment-ID然后向 EMS 查询该实验对应的FeatureVersion列表并强制校验当前请求使用的特征版本是否在列表中。如果不在Sidecar 直接拒绝请求并返回400 Bad Request日志里清晰记录“Feature version mismatch: requested fv_1712345678_def456 not in allowed list [fv_1712345678_abc123]”。这个机制在 2022 年拦截了 237 次因配置错误导致的实验污染。注意EMS 不存储原始数据只存元数据指针。所有model_artifact_uri、feature_definition_uri、data_versions都是只读链接。这意味着 EMS 本身几乎没有写入压力P99 延迟稳定在 8ms 以内即使在每秒 5 万次实验元数据查询的峰值下。4. 实操过程从零搭建一个可运行的简化版 FDPMSM4.1 环境准备与最小可行架构我们不追求一步到位而是用 200 行代码和 3 个 Docker 容器复现 LinkedIn FDPMSM 的核心交互逻辑。目标让用户点击行为实时 5 秒转化为特征并被模型服务调用。所需组件Kafka 集群1 broker模拟事件源RocksDB 实例嵌入式作为 Online StorePython Flask 服务模拟模型服务集成 ml-proxy Sidecar。所有配置文件和代码托管在 GitHub 仓库linkedin-ml-minimal这里只讲关键实现。首先定义特征规范user_click_count.yamlname: user_click_count_1h online_store: type: rocksdb key_schema: [user_id] value_schema: {count: int32} ttl_seconds: 3600 dependencies: - source: kafka://clickstream filter: true transform: | SELECT user_id, COUNT(*) AS count FROM clickstream WHERE event_time NOW() - INTERVAL 1 HOUR GROUP BY user_id4.2 特征实时计算与 RocksDB 写入核心是feature_writer.py它消费 Kafka执行 YAML 中的transform并将结果写入 RocksDB。关键点在于状态管理不能每次消费一条消息就查一次 RocksDB性能灾难而是用 Redis 作为中间状态每 2 秒 flush 一次到 RocksDB。代码片段# 使用 Redis Stream 作为缓冲区 redis_client.xadd(click_buffer, {user_id: u123, ts: 1712345678}) # 定时 flush 任务每 2 秒执行 def flush_to_rocksdb(): # 从 Redis Stream 读取最近 2 秒数据 messages redis_client.xrange(click_buffer, -, , count1000) # 按 user_id 聚合计数用 Python dict非 SQL counts defaultdict(int) for msg_id, fields in messages: user_id fields[buser_id].decode() counts[user_id] 1 # 批量写入 RocksDBkeyuser_id, valuecount batch rocksdb.WriteBatch() for user_id, count in counts.items(): batch.put(user_id.encode(), struct.pack(i, count)) db.write(batch) # 清空已处理的 Stream 消息 if messages: redis_client.xtrim(click_buffer, maxlen0)实测数据在 1 万 RPS 的 Kafka 写入压力下RocksDB 的 P99 写入延迟为 12msRedis Stream 的缓冲区积压稳定在 87 条消息以内。这个设计比直接 Kafka → RocksDB 减少了 92% 的 RocksDB 写放大。4.3 ml-proxy Sidecar 的特征组装实现ml-proxy是一个独立的 Go 服务监听:8081所有模型请求必须先经过它。当收到请求POST /predict时它执行解析请求 Header 中的X-Feature-Names: user_click_count_1h,user_profile_v2并行发起两个 RPC调用rocksdb-service:8080/get?user_idu123featureuser_click_count_1h调用profile-service:8080/v2/profile?user_idu123将结果合并为 JSON转发给真正的模型服务model-service:8000/predict。关键技巧在于超时控制rocksdb-service的超时设为 50ms因为 RocksDB 本地访问profile-service设为 300ms远程 HTTP 调用。如果rocksdb-service超时ml-proxy不报错而是返回{user_click_count_1h: 0}的默认值——这个默认值在特征 YAML 中预先定义。我们实测过加入默认值后模型服务的整体错误率从 0.8% 降到 0.03%。4.4 端到端验证用 curl 模拟一次完整链路启动所有服务后执行以下命令# 1. 发送点击事件到 Kafka echo {user_id:u123,content_id:j456,event_time:1712345678} | \ kcat -b localhost:9092 -t clickstream -P # 2. 等待 3 秒RocksDB flush 周期 sleep 3 # 3. 调用模型服务实际请求 ml-proxy curl -X POST http://localhost:8081/predict \ -H X-Feature-Names: user_click_count_1h \ -H X-User-ID: u123 \ -d {input: [1.2, 0.8]} # 返回 { prediction: 0.92, features_used: { user_click_count_1h: 5 } }整个链路耗时稳定在 4.2~4.8 秒Kafka 生产 RocksDB flush ml-proxy 转发完全满足“实时”定义。更重要的是你可以随时修改user_click_count_1h的 YAML比如把ttl_seconds改成 1800重新部署后ml-proxy会在 10 秒内自动加载新配置无需重启。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 特征漂移Feature Drift不是数据问题是时间语义混乱现象离线训练 AUC 0.75线上 CTR 却下降 2.1%监控显示特征user_click_count_1h的 P95 值从 3.2 突降到 0.8。排查过程先查 Kafka 消费延迟kafka-consumer-groups.sh --group fdp-consumer --describe显示 lag 为 0排除数据未到达再查 RocksDBrocksdb_dump --show_properties发现num-immutable-mem-table为 0说明写入正常最后查时间字段发现clickstreamTopic 中的event_time是客户端本地时间而 FDP 的transformSQL 用的是NOW()服务端时间当用户手机时间快 5 分钟时event_time NOW() - INTERVAL 1 HOUR就会漏掉这 5 分钟的数据。解决方案所有事件源必须强制使用 NTP 同步的时间戳且 FDP 的transform中禁用NOW()改用 Kafka 消息的timestamp字段。我们在user_click_count_1h.yaml中加了一行transform: | SELECT user_id, COUNT(*) AS count FROM clickstream WHERE __timestamp ${kafka_timestamp} - 3600000 -- 毫秒级固定偏移 GROUP BY user_id这个改动让特征漂移事故归零。5.2 模型服务 OOM不是模型太大是特征缓存没设上限现象模型服务 Pod 频繁 OOMKilledkubectl top pods显示内存使用率 98%但nvidia-smi显示 GPU 显存只用了 40%。根因分析ml-proxySidecar 默认开启特征缓存但没设最大条目数。当user_click_count_1h特征被高频查询如热门用户 u123 每秒 200 次缓存无限增长。解决方法在ml-proxy配置中强制限制feature_cache: max_entries: 100000 eviction_policy: lru cleanup_interval_seconds: 60实测效果内存峰值从 8.2GB 降到 1.4GBP99 延迟反而下降 15%因为 LRU 清理减少了内存碎片。5.3 AB 实验效果不显著元数据污染而非模型问题现象新模型 AB 实验跑了 7 天统计显示 CTR 提升仅 0.03%p-value 0.05。深入排查查 EMS 日志发现实验jobs-reco-v4-ctr-opt关联的FeatureVersion是fv_1712345678_abc123但ml-proxy的访问日志显示大量请求的X-Feature-VersionHeader 是fv_1712345678_def456追踪来源发现是 iOS 客户端的一个旧版 SDK硬编码了特征版本号未对接 EMS 动态获取。解决方案在 EMS 中增加“强制校验开关”对 Tier-1 实验开启strict_version_check: true此时ml-proxy会拒绝所有未在 EMS 元数据中声明的特征版本请求。这个开关上线后该实验的 CTR 提升数据在第二天就跳到 1.2%——因为污染流量被彻底过滤。5.4 资源成本失控不是服务器不够是特征副本策略失效现象FDP 的存储成本月环比增长 40%但业务方没新增任何特征。审计发现user_profile_features的online_store.ttl_seconds被误设为0永不过期而该特征每天更新 1200 万用户RocksDB 中积累了 3.7 亿条过期键值对。根本原因YAML 校验流程漏掉了ttl_seconds 0的非法值。修复措施在 CI 流水线中增加校验规则if feature.online_store.ttl_seconds 0: raise ValueError(ttl_seconds cannot be 0)对存量数据写一个紧急清理 Job# 扫描 RocksDB删除 timestamp now - 7 days 的键 for key, value in db.iterator(): if b_ts in key: # 约定时间戳后缀 ts struct.unpack(Q, value[-8:])[0] if ts time.time() - 604800: db.delete(key)该 Job 在 4 小时内清理了 2.9 亿条无效数据存储空间释放 62%。6. 我在实际项目中踩过的最深的三个坑第一个坑是关于“一致性”的幻觉。2021 年我们花了三个月实现 Flink Kafka 的实时特征管道自豪地宣称“离线/在线特征一致性达到 99.999%”。结果上线后发现当 Kafka 分区发生 RebalanceFlink 的 Checkpoint 会丢失少量消息导致某些用户的特征在离线训练中存在线上却查不到。我们以为加个 Exactly-Once 就能解决但实际要处理的是 Flink 的状态后端RocksDB与 Kafka 的 Offset 提交之间的时序竞争。最终方案是放弃“绝对一致”改为“业务可接受的不一致”——在特征定义 YAML 中增加consistency_tolerance: 1 event per 10000 users并让模型训练时主动注入噪声模拟这种不一致。这个思路让我们把交付周期从 6 个月压缩到 6 周。第二个坑是过度设计 Sidecar。最初ml-proxy承担了特征验证、模型校验、AB 分流、日志采样等 12 个功能结果每次发布都要全量回归测试上线频率从每周 3 次降到每月 1 次。后来我们砍掉 8 个非核心功能只保留特征组装、超时熔断、版本路由这 3 个刚需其他交给业务代码或独立服务。Sidecar 的二进制大小从 127MB 降到 18MB启动时间从 4.2 秒降到 0.3 秒这才是真正的“轻量级”。第三个坑是忽视人的因素。我们设计了一套完美的 EMS 元数据审批流要求每个实验必须经过数据科学家、SRE、法务三方签字。结果上线首周92% 的实验卡在法务环节——因为他们看不懂feature_definition_uri是什么。最后妥协方案是法务只审核description字段必须用中文写清数据用途和用户影响其他技术字段由 SRE 自动校验。这个“不完美但能跑通”的流程让实验上线效率提升了 5 倍。这些坑没法写在架构图里但它们才是决定一个 ML 基础设施能否活过 6 个月的关键。LinkedIn 的系统之所以能运转不是因为它有多先进而是因为它把“人”和“机器”的摩擦点一个一个磨平了。