Caveat Configurator:当配置变成代码

原标题Caveat Configurator: When Configuration Becomes Code

Dominick LoBraico 解释灵活的 OCaml 插件为何给 Jane Street 的生产环境带来问题,以及配置生成和规范的插件管理如何应对这些问题。

DLDominick LoBraico讲者 · Jane Street
频道
Jane Street
发布
2017年6月20日
视频
52:51
阅读
11 分钟
幻灯片
5
原文
英语
在 YouTube 打开
往下滚动阅读时,视频会缩到右下角继续播放。字幕跟随阅读语言。
阅读语言共 2 种语言可读
由 AI 从英语原文翻译。打开“对照原文”可以逐段核对。
摘要

30 秒看懂

Jane Street 的 Dominick LoBraico 回顾了配置从简单数据演变为领域专用语言和动态加载的 OCaml 插件的过程。他介绍了生产环境中出现的安全、版本兼容和工具问题,并比较了生成静态配置与集中管理、测试插件两种方法。

  1. 1考虑零配置如果合理的默认值能满足需求,重新构建和部署可能更简单。
  2. 2DSL 也是一门要维护的语言自定义语法意味着除了应用本身,还要维护解析器、语义和工具,并让使用者学习它。
  3. 3插件兼容性可能影响部署插件必须能针对加载它的应用所打包的库版本编译。
  4. 4用代码生成数据生成器提供熟悉的抽象能力,同时保留可以检查和比较差异的静态结果。
  5. 5像应用代码一样管理插件Plugd 集中管理版本,只在插件经过审查、编译和测试后提供给应用。

要点建议

  1. 首先要问的是系统是否真的需要独立配置。
  2. 自定义配置语言意味着要维护语义、修复缺陷、开发工具,并让使用者掌握专门技能。
  3. 运行时插件执行的是真实代码,需要严格管理兼容性、部署和安全。
  4. 当系统不需要运行时函数时,可以用代码生成可检查的静态配置。
  5. 确实需要独立部署插件时,应集中管理版本、审查、构建和测试。
全文

按章节阅读整理后的全文

只看原始文稿
01

灵活性、安全性与插件试验

Jane Street 的交易系统既要能复用,又要安全。一个前景看好的 OCaml 插件方案在生产环境中遇到了实际问题。

在 Jane Street 配置系统时,我们面临两个要求:灵活和安全。我们在全球交易多种金融工具,使用的大多是内部开发的工具,因此每位工程师要负责许多系统。只需做少量修改就能复用通用系统,可能意味着我们可以迅速抓住机会,而不用从头构建新系统。

我们交易的也是自己的钱。小缺陷可能让我们错失机会,或无法在某个市场交易;紧密循环中的大缺陷则可能威胁整家公司。配置是一种让通用系统发挥更大作用、同时管理这些风险的方式。

2014 年,我和同事介绍了 ocaml_plugin,一个让动态加载 OCaml 代码更容易的库。我们可以将代码插入系统,以比以往配置架构更灵活的方式改变其行为。我们对它很有信心,于是在新的邮件系统实现中使用了它。

第一次在生产环境中简单使用插件,结果却是一场灾难。我想解释我们遇到了什么问题、为什么会选择插件,以及最终采用了哪些配置方法。

02

配置有什么用

配置把变化的行为与应用核心分开。有些系统完全可以不设独立配置。

配置让我们不必每次重新编译、打包和部署应用,就能改变系统行为。它把提供给操作人员的决策与核心功能分开,也能为原作者没有预见到的情况提供应急手段。

以邮件服务器为例:它接受连接、接收消息、处理消息——例如添加邮件头或删除带病毒的附件——然后投递或转发。一个可能的设计是完全没有独立配置:直接修改应用,再重新构建和部署。

零配置并不荒唐。如果合理的默认值和方便的部署能满足需求,就可以避开这里讨论的许多问题。邮件系统通常需要在运行时做修改,但引入独立配置之前,值得先问一句:真的需要吗?

当我们把更多决策从应用核心移入配置,系统可能变得更灵活。走到极端,配置会处理几乎所有事情。无论边界画在哪里,这些选择都必须存在应用能读取的地方,例如文件或数据库。

03

从纯数据到领域专用语言

简单数据直观透明,但随着复杂度增长会出现大量重复。自定义语言能提供更强的表达能力,同时带来设计、工具和培训成本。

常见的起点是纯数据:命令行参数、环境变量、键值文件,或者 YAML、JSON 和 S 表达式等格式。一个小型扁平文件很容易理解:如果它写着邮件服务器监听 25 端口,就没有隐藏逻辑需要解读。

简单数据也相对容易做版本管理。升级程序可以读取旧格式,并生成新版本应用所需的内部表示。缺点是表达能力有限:配置越来越复杂时,内容可能变得冗长、重复。

结构化邮件配置
幻灯片 1 / 5结构化邮件配置示例指定端口、日志、下一跳和重试间隔,展示配置增长时出现的重复。

在类似 JSON 的邮件配置里,我们可以列出端口、日志目录和级别,再按发件人域名指定下一跳服务器及重试间隔。增加另一个域名时,可能需要重复端口和重试计划。我们可以把重复值提取为默认值,但有些字段有多种常见情况,找不到一个显然合适的默认值。

接下来很容易想到加入宏或变量。这能减少重复,却也开始形成一门临时拼凑的语言。另一种办法是有意识地设计领域专用语言,从一开始就选择结构和功能,而不是不断累加特殊情况。

DSL 让设计者掌握控制权,表达能力可以非常强。但语言设计耗时,即使对熟练的设计者来说也很困难。解析器或解释器也可能有缺陷,尤其在生产事故中,很难分辨问题来自语言还是应用。

即使借鉴了现有语言的特性,它的语义对其他人仍可能陌生。语法高亮、导航、补全和测试等工具需要开发或适配。有经验的邮件运维人员也必须学习这门新语言;凌晨两点出了问题,作者可能是唯一能接电话的人。

04

用代码配置邮件系统

Exim 的加号地址示例展示了领域专用语言的复杂性。OCaml 消息处理函数则用熟悉的编程语言表达相同的行为。

举个具体例子:Exim 的基础配置可以把邮件投递到以地址本地部分命名的文件,检查用户列表,并拒收未知用户的邮件。加入类似 Gmail 的加号地址后,收件人可以在地址中添加后缀,同时保留原来的投递目标。

Exim 中的加号地址
幻灯片 2 / 5Exim 中的加号地址Exim 示例展示了为一个相对简单的功能增加的语法和条件。

即使这个功能并不复杂,如果允许每个用户自行启用,Exim 配置也会复杂不少。里面有嵌套花括号、特殊的后缀模式,以及引用和检查变量时的不同规则;顺序和作用域也很重要。示例或许能运行,但要写对需要花费真正的精力。

我们已经有表达复杂行为的工具:编程语言,以及熟悉的库和工作流程。在 OCaml 中,可以用发件人、收件人和正文对消息建模,再定义一个处理函数,返回已投递、带目标地址的转发,或者带错误信息的拒收。

用 OCaml 对邮件服务器建模
幻灯片 3 / 5用 OCaml 对邮件服务器建模消息类型和处理函数签名用代码表达投递、转发和拒收。

要实现加号地址,处理函数可以按加号拆分地址的本地部分,检查用户偏好,然后拒收或投递。熟悉这门语言的人看到的是普通代码,而不是另一套 DSL 专属规则。许多语言都支持动态加载;ocaml_plugin 让它在 OCaml 中更方便。

05

简单插件方案为何失败

插件引入生产代码风险、依赖兼容难题、分散的文件和工具缺口。一些应用最终重新选择将插件与应用一起部署。

起初,用 OCaml 取代较大的配置让内容更简洁易读。随后人们开始充分使用获得的能力:插件越来越多,问题也随之出现。代码几乎什么都能做,而配置生产服务的插件会在服务进程内运行,使用服务的用户身份和机器。

我们部署了一些系统:用户自行编写插件,应用开发者则独立开发核心程序。起初一切正常,直到我们需要修改并重新部署核心应用。我们对版本管理考虑不足:插件必须能针对特定应用版本链接的库进行编译。

如果库接口改变,而插件没有同步更新,它在运行时可能无法编译。我们确实因此放弃并重新启动过一些应用。独立的插件文件还需要考虑存放在哪里、如何部署和追踪,以及如何随应用一起更新。

令人意外的是,工具体验也很差。正常代码库之外的独立插件不会自动告诉编辑器依赖在哪里,因此跳转到定义等功能可能失效。把配置代码和其他代码区别对待,会破坏工具原有的假设。

当配置从 12、13 行的例子增长到 4,000 行 OCaml 时,这个问题更加突出——我们的实际邮件服务器配置就有这么长。到了这个规模,糟糕的开发流程已经不是小麻烦。

很多情况下,我们把边界移了回去:将插件与应用打包并一起部署,尽管仍通过 ocaml_plugin 加载。我们保留了核心与可配置行为之间清晰的分离,以及运行时的应急手段,但失去了原本希望获得的大部分独立配置优势。

我们走了两条大致不同的路。对一些应用,我们认定独立插件的灵活性并非必要,于是随应用一起交付插件。另一些地方,我们投入工具来编辑、做版本控制,以及变基和合并分散的插件修改,实际上重新构建了普通开发环境的一部分。

06

生成配置

代码可以生成可检查、可比较差异且有版本管理的静态配置。当所需行为能表示为有限数据时,这种方法很合适。

代码有明显优势:表达能力强,可以通过熟悉的抽象扩展,并有成熟工具。它还自带代码审查、测试和重构的文化。这些实践有助于质量和正确性,但人们不会自然而然地把它们用于配置文件。

一种替代方案是生成配置:编写代码产出有限的静态配置,再由应用读取这些数据。这样既能在编写配置时利用代码的表达能力,又能在加载前直观检查结果。

生成方案要求配置能以有限形式写下来;运行中的函数不能直接序列化成普通数据。磁盘格式仍需要版本管理,但升级数据格式比维持动态加载代码与应用全部依赖之间的兼容性要简单。

已经使用手写数据的系统可以改用生成器,而不一定需要修改应用:生成器输出相同的格式即可。这种方法也适用于第三方系统,因为生成器可以用任何语言编写,只要输出目标应用要求的格式。

在 Jane Street,一种选择是使用固定编译器和库版本的 OCaml 脚本;另一种是使用带代码审查、构建守护进程和测试的代码仓库。把生成的配置也提交进去,可以方便地检查变更,并在不重新运行生成器的情况下回滚。

我们的监控配置描述主机、服务、检查命令和计划。由于加载前就能看到生成的文件,操作人员可以检查实际设置。我们还可以生成包含解析后主机和检查项的 CSV,比较版本,准确看到哪些检查消失了,又增加了哪些主机和检查。

比较生成的检查项
幻灯片 4 / 5比较生成的检查项生成的主机和检查项列表使配置变更对运维的影响清晰可见。
07

规范插件管理与决策树

Plugd 集中管理经过审查、编译和测试的插件版本。具体方案取决于是否需要运行时动态能力、独立部署和稳定性。

另一条路是直接解决插件问题。我们需要处理版本兼容、有缺陷的插件或顶层异常导致应用不稳定的风险、不一致的部署和文件管理,以及工具体验差等问题。Jane Street 为此构建了一个叫 plugd 的系统。

Plugd 将配置集中存放在仓库中,像应用代码一样管理。它只提供经过审查、编译和测试的版本,并让应用不必依赖插件文件的位置。它还提供在不同版本之间迁移补丁的工作流程,让旧版应用使用的插件也能保持更新。

这套基础设施花了不少功夫,但插件提供了配置生成无法提供的能力:运行时动态性。函数本身就可以是配置,因此行为不必受限于有限的静态文件。

选择配置方案
幻灯片 5 / 5选择配置方案决策树考虑运行时表达能力、解耦和稳定性。

决策树的第一步是问:是否需要运行时的计算灵活性?如果不需要,配置生成是很好的选择。如果需要,再问插件编写者是否必须与应用开发者解耦;如果不必,就把插件和应用一起打包部署。

如果独立插件不可或缺,而且稳定性很重要,就应该采用规范的版本管理和基础设施,而不是简单地使用插件。还要重新考虑是否真的需要插件:许多情况下,只要把应用组织好,就能实现核心与可配置功能之间吸引人的分离。

08

观众问答

讨论涉及非开发人员、版本兼容、基于库的应用、沙箱隔离和插件基础设施维护。

有人问 plugd 自身是如何配置的。我认为它更接近零配置:它有合理的默认值,并与代码审查系统和构建守护进程紧密集成,可以从中获得大量所需信息。

一位观众问到开发团队之外的研究人员和运维人员。编写插件的人必须懂 OCaml。虽然 Jane Street 的很多非工程师也能读写代码,但有时我们会刻意保留更简单的格式,让人不必学习编程语言就能打开和修改。

两种方法也可以结合:插件提供运行时动态能力,同时在插件内部定义一种更简单的配置格式。另一些情况下,交易员自己编写插件,熟悉应用核心的人再审查,帮助确保代码安全合理。

关于版本管理,有人问问题是否只是缺少版本控制。版本控制确实重要,配置文件默认也更容易漏掉版本控制;但更棘手的问题是插件与应用运行时可用的精确库版本之间的兼容性。

OCaml 插件会把编译器和依赖打包进应用。运行时,应用展开这些内容,针对这些依赖编译插件,然后动态加载结果。因此,如果插件依据另一个版本的库编写,就可能在应用加载时编译失败。

另一种做法是把应用作为库提供,让用户在其上构建一个小程序。我们广泛使用类似模式:代码库由一组库组成,其中一些提供启动已配置应用的函数。配置和使用库之间的界限并不总是十分清楚。

什么时候应该选择这种方式?答案部分取决于哪些功能已经打包,哪些留给用户。一位观众补充说,如果程序执行时必须改变行为,加载新插件就很重要;如果一切都在运行前设置好,差异可能主要体现在工具上。

有人问到沙箱隔离。我介绍了一些库,它们启动工作进程来构建插件,使失败的工作进程不会直接影响主进程。这需要大量配套代码,尚未成为我们的主要用法,但仍是一种选择。

所谓“好的”插件,是通过整个流程的插件:代码审查、编译和测试。构建守护进程为每个发布版本运行相关测试,plugd 在向应用提供插件之前,也会独立构建和测试插件。

观众问到构建和维护 plugd 需要多少人。演讲者估计,大部分工作约由一人完成,维护只需一部分人力;不过算上所有参与者,总开发投入仍然可观。

如果负责人不在怎么办?这是更普遍的运维问题。其他人对生产运行有足够了解,可以应对问题;代码审查也能让更多人了解变更。我们鼓励写文档,但实际情况参差不齐;对特别重要的系统,我们会确保几个人持续了解日常运行。

最后,有人问插件功能是否会逐渐迁入应用核心。我预计会发生这种情况,但一时想不起具体例子。

同频道的其他文章

1:34:29EN
JSJane Street· 2026年6月24日Jacob Baskin on Building Superstore and Scheduling the HiveYaron Minsky, Jacob Baskin · Jane StreetThe Jane Street engineer traces the design choices behind a distributed analytical database and the challenge of allocating a growing research cluster.基础设施与部署Software engineeringDecision Making+1
1:34:36EN → 简体中文
JSJane Street· 2026年6月1日Nate Foster:把网络当作程序Yaron Minsky, Nate Foster · Jane Street从可编程交换机到 BGP,一位编程语言研究者解释高层策略和形式模型如何让网络更易于修改。基础设施与部署Programming LanguagesSoftware engineering+2
5 张幻灯片15:48EN → 简体中文
JSJane Street· 2026年5月15日走进 Jane Street 的得州 AI 数据中心Dwarkesh Patel, Yaron Minsky, Daniel Pontecorvo · Dwarkesh PatelDwarkesh Patel 与 Ron Minsky、Dan Pontecorvo 一同参观拥有 4,032 个 GPU 的训练设施,了解液冷、配电和运行保障。基础设施与部署AI cost & performance
7 张幻灯片1:20:57EN → 简体中文
JSJane Street· 2025年12月23日Matt Godbolt:深入 Skylake 的乱序流水线Matt Godbolt以逆向工程和细致测量为基础,导览指令解码、寄存器重命名、执行与内存顺序。SemiconductorsSoftware engineeringBenchmarks
1:12:58EN → 简体中文
JSJane Street· 2025年9月4日Chris Lattner:Mojo 与可移植 GPU 性能的追求Yaron Minsky, Chris Lattner · Jane StreetRon Minsky 与 Chris Lattner 讨论 Modular 如何通过语言和编译器设计,努力让加速器编程兼顾可移植性与速度。Programming LanguagesGPU programming & kernelsSoftware engineering+1
58:47EN → 简体中文
JSJane Street· 2025年7月28日Daniel Pontecorvo:为交易基础设施和办公空间降温Yaron Minsky, Daniel Pontecorvo · Jane Street从高密度 GPU 机柜到可移动办公桌,Jane Street 的物理工程团队围绕热量、电力、故障和人的舒适度进行设计。基础设施与部署Energy可观测性与可靠性+1