企业价值评估最重要的因素之一就是衡量商品的销售成本(COGS),换句话来说,就是每赚一美元,企业需要承担多少交付成本。
如果是传统企业,我们可以通过多种方法来尽可能的降低 COGS,例如优化供应链、寻找更便宜的原材料,或者与供应商协商以提高产品价格等等。但是这一切操作都需要时间。而在云计算时代,由于众多配置变更开始即时生效,我们面对的实际成本可能会在一夜之间增长 10 倍,此时我们最需要的也是时间。
2018 年 9 月,我们遇到了类似的问题。当时我们正在考虑进行新一轮融资,而且全部业务指标看起来都非常乐观——收入有所增长,客户忠诚度不错,新产品也得到了市场欢迎。看来看去,就只剩一个问题:我们的毛利率过低。
当时一位董事会成员指出,“毛利率正是公司整体经济状况中的致命软肋。”
为了解决这个难题,我们在接下来的六个月中(当年 7 月到次年 1 月)努力将基础设施成本降低了 30%,同一时期流量则增长了 25%。
这就是今天我要讲的话题了:如何在 90 天之内将毛利率提高 20%。
成本动因与监控
在深入探讨这个问题之前,我先简单介绍一下原先的基础设施成本情况。
Segment 提供一种 API,能够帮助公司轻松将数据从来源处(例如网络、移动、服务器或者云应用程序等)迁移到更有价值的位置(例如 Google Analytics、Mixpanel 以及 Intercom 等)。我们通常将成本建模为每百万个事件的处理成本,且具体划分为五个不同的产品领域:
核心产品——摄取百万个事件并使用 Kafka 进行处理的成本。
流传输——以可靠方式传输百万个事件的成本。
云资源——从 Salesforce 以及 Stripe 等 API 处提取百万个事件的成本。
数据仓库——将百万行数据加载至客户数据仓库中的成本。
Personas——利用 Personas 处理百万个事件中客户资源与受众管理产品的成本。
对于每一种产品,我们注明了用于支持该产品的原始 AWS 基础设施(包括 EC2 实例、EBS 存储卷、RDS 实例以及网络流量等等)。我们的第一步,就是逐个查看产品,并了解能够在哪些方面实现最大优化。
在此阶段,我们选择了战略财务团队与企业会计师最熟悉、同时也是最古老的工具选项:电子表格。
对于每一个项目,我们会估算能够节约的具体金额、成本动因,而后分配对应的“所有者”负责完成项目优化工作。
总而言之,我们列出了大约 50 个项目,每年有望节约超过 500 万美元。以下是其中最值得关注的十大重点项目。
NSQ 削减
过去五年以来,我们一直利用 NSQ 为大部分业务流水线提供支持。NSQ 充当分布式队列机制,帮助我们每月处理数千亿条消息。正是这一主干的存在,才使我们的服务能够在发生故障时也不致丢失数据信息。
NSQ 拥有良好的易用性。它简单便捷,内存占用量低,而且不需要任何 Zookeeper 或者 Consul 之类的系统进行集中协调。
遗憾的是,NSQ 的总成本在过去一年中迎来一波疯涨。为什么会这样?下面,我要给大家讲讲 NSQ 的基本工作原理。
NSQ 以单一 Go 二进制文件的形式进行分发,并作为独立进程(nsqd)运行,同时会公开服务器接口。
编写者可以将消息发布到这些二进制文件内,消息将始终驻留在内存中,直到超过内存容量警戒线。这时,读取程序可以订阅服务器上的另一个端点,从而通过守护程序继续读取消息。
推荐的 NSQ 运行方法,是在每个实例上运行一个 nsqd 二进制文件。
每个 nsqd 都会使用 nsqlookupd(NSQ 附带的发现服务)进行自我注册。当读取程序发现需要对接的 nsqd 实例时,他们会首先向 nsqlookupd 发出请求,然后再正式接入整个 nsqd 节点集。
结果就是,读取程序与编写程序之间建立起了“网状”网络。实际上,每个读取程序都会接入它发现的所有 nsqd。
这种架构的主要优势,是能够在面对网络分区时继续保持可用性。编写程序几乎始终能够向本地实例(除非发生存储故障)发布消息,而网络分区则只会影响到读取程序通过当前流水线的能力。
此外,这套架构也带来了隔离机制。即使单一 nsqd 发生故障,流水线中的其余部分也能够继续处理数据。
最后一项优势非常简单:二进制文件极易实现独立运行,只是会提升基础设施的运营成本。
这里所说的运营成本,主要分为三大类:CPU 与内存、磁盘,以及网络。
CPU 与内存成本
第一个成本问题,在于我们必须为每一台主机上的 nsqd 进程“保留”一定空间。请注意,在我们的设置中,nsqd 守护程序与发布程序处于同一位置,因此我们的每台主机上都有 nsqd 的身影。除了该守护程序之外,我们通常还需要运行其他 50 到 100 个塞满了负载的容器,这是为了充分利用所有计算核心并发挥主机的单租户优势。
在正常情况下,nsqd 的工作量一般不大(最好的队列就是空队列)。但在宕机期间,我们需要确保现有流程仍能处理积压的订单。
我们最终为每个实例保留 8 gb 内存与 8 个 vCPU(相当于实例约 20%的资源量)。虽然这些 CPU 在正常情况下长期闲置,但没办法,我们必须得为宕机阶段内 NSQ 可能面临的大量工作负载做好准备。
磁盘成本
第二大成本问题来自 NSQ 的溢出机制。当消息数量超过内存中的高位标记时,NSQ 就会将数据写入磁盘当中。在长时间宕机的情况下,我们必须确保磁盘具有充足的存储容量。
考虑到单一实例每秒需要处理数万条消息,实际数据量的累加速度自然极为惊人。我们为每台主机提供 500 GB GP2 EBS 驱动器作为补充,这意味着无论是否实际使用,我们每个运行实例每月都会增加 50 美元成本。
网络成本
最后也最不易被察觉的成本来自跨网络流量。在我们的设置中,读取程序仅从随机连接的 nsqd 实例当中读取消息。
我们希望 Segment 能够具备高可用性,因此决定在三个可用区(AZ)上运行实例。亚马逊方面承诺每个可用区都代表一座具有物理隔离设计的数据中心,因此与数据中心相关的宕机问题不致影响到全部可用区。
我们的自动伸缩组能够自动平衡各可用区之间的实例容量。通过这种跨可用区设计,即使单一可用区发生宕机,我们也能够继续正常处理数据。
但这同时也意味着,我们有三分之二的读取数据量来自其他可用区并需要占用网络传输资源(我们将在后文进一步说明)。
迁移
我们讨论了削减 NSQ 成本的几种方法。首先,我们可以将同一位置处的模型迁移至 NSQ 实例的集中集群,从而缩小各个实例间的“间隔”并减少磁盘驱动器的使用成本。其次,我们也可以减少磁盘空间(除了「前门」位置以外),让系统全程具有一定的负载回压。
最后,我们决定采用另外两种单独的方法来满足流水线架构要求。
入站:Segment 流水线的入站部分也正是这款产品的核心所在。它由我们的“跟踪 API”构成,也就是前门 API,负责每秒收集成千上万条消息。
该 API 的一大核心要求,就是必须拥有近乎完美的可用性水平。如果 API 由于任何原因而发生崩溃,那么我们最终一定会丢失数据。所以,我们可以接受牺牲一定程度的一致性,通过延迟数据传递来全力确保跟踪 API 可用性的最大化。
事实上,nsqd 也能够与跟踪 API 运行在同一实例当中,同时保证与一切其他节点相隔离。这种特质,让 nsqd 成为我们超高可用性用例的理想选择。如此一来,即使在网络分区的情况下,只要负载均衡器能够继续发送请求,总会有某个入站实例能够继续愉快地接收数据。我们不必担心领导节点选举、协调或者任何其他“分裂”行为的出现。
为了解决入站流水线上的网络流量问题,我们决定将 NSQ 以“区感知”模式进行安装。
简单来说,当某条请求传入时,跟踪 API 服务会将其发布至本地的 nsqd 实例处。该实例处于同一可用区,因此不占用网络资源。
接下来,当读取程序接入时,不同于以往直接对接 nsqlookupd 发现服务的作法,现在我们让读取程序接入代理。该代理负责两项工作:第一是对查找请求进行缓存,第二是只向区感知客户端返回当前可用区内的 nsqd 实例。
而后,从 NSQ 处读取消息的转发器将被配置为一个区感知客户端。我们运行有三套转发器服务副本(每个可用区一套),同时确保各副本仅面向所在可用区内的服务发送流量。
利用这种可用区内的路由机制,我们的服务发现机制不再依赖于中间负载均衡器,并帮助我们每月节省下近 11000 美元的支出。
其他方面:对于流水线中的其他部分,我们同样决定进行迁移以引入 Kafka。我们之前已经在流水线的部分环节当中使用 Kafka,但要想真正消除成本高昂的磁盘驱动器、NSQ 中间件以及跨网络流量等,我们必须得把更多服务与 Kafka 对接起来。
这意味着我们需要构建新的客户端库、新的消息传递语义以及新的检查点。我们使用开源 Kafka-go 库以获得一种快速且符合以往习惯的 Kafka 集群接入方法。为了实现之前提到的可用区内路由机制,我们还在其消费程序组中添加了区友好元素。
这项工作规模很大,分为多项具体任务,接下来我们将深入聊聊实现方法。
Kafka 优化
当我们决定把现有流水线全面迁移至 Kafka 时,第一步就是对 Kafka 中的各个组件进行评价在,希望最大程度提升迁移后的运行效率。
合并集群
第一项任务很快成功,我们将原有流水线合并成了单一 Kafka 集群。
在刚刚接触 Kafka 那会儿,我们会按照产品领域与工作量进行集群划分。这虽然能够带来不错的隔离性,但也意味着很多集群在正常运行时都存在严重的资源闲置问题。
这显然就很没必要了,毕竟我们的集群规模不大,空耗大量 CPU 与磁盘空间太不划算。所以我们将其迁移到每个可用区中的自动规模伸缩分组之内,从而显著提高了资源利用率。
I3 实例
以往,我们会把 Kafka 以及其他全部工作负载都运行在 c5.9xlarge 实例当中。
2017 年 2 月,AWS 推出了新的 I3 实例,且配备有 NVMe SSD 芯片。
NVMe 无需像传统 SSD 那样通过 SATA 接口进行通信,而是采用 PCIe 实现数据传输,从而以更低成本带来了更高性能。更重要的是,I3 实例使用的是本地实例存储而非 EBS,这意味着这些新的实例能够通过高速接口(而非以往的网络连接)实现本地连接。
对于像 Kafka 这样的写入密集型工作负载,10 gbps 的磁盘传输带宽经常出现,这显然超出了常规 EBS 优化型 C 实例家族所能提供的 875 mbps 的承载上限。
我们发现,最适合我们 Kafka 代理服务的实例类型就是 i3.8xlarge。在使用规模更大的实例类型之后,即使失去单一代理实例,造成的影响也会相对较小;此外,不同实例之间的干扰也有所降低、网络 I/O 更高,合并带来的磁盘空间也将更为充裕。
Analytics.js 摇树优化
先介绍背景:Analytics.js 是我们面向所有客户提供的 JavaScript 捆绑包,用于帮助他们收集数据。需要强调的是,每位客户都会根据其目的地的不同而获得对应的副本。有一些可能获得带有 Google Analytics 的 Analytics.js 副本,另一些客户获得的则可能是 Mixpanel。为此,我们建立起一套定制化构建流程,能够对于特定代码片段实现“模块化”集成。
完整的文件如下所示:
…
虽然我们的设计意图是只为客户提供适用的副本,但在实际应用中,我们发现无论用户启用哪种集成方式,仍然会收到全部依赖项集合。
最重要的是,大部分代码变更可能根本没什么影响,结果就是我们实际上是在反复捆绑类似的代码。
因此,我们决定采用“摇树”优化,也就是删除其中的无效代码。具体办法是首先通过标准 yarn 安装添加所有依赖项,而后摇树以合并各个独立的软件包。例如,如果 Google Analytics 依赖于 validator 软件包,但用户实际上并不使用 Google Analytics,我们就会在捆绑中将其忽略。
通过这一设计,我们的 Analytics.js 成功实现了 30%的瘦身效果。每个月,该脚本都会从不同的 CloudFront 发行版本处进行数十亿次加载操作,而单此一项调整就让网络传输量降低了 30%。
使用 Go 语言重写核心验证
相信很多朋友都看过将各类生产服务重写为 Go 形式的文章,Segment 也没什么不同。我们将 Segment 核心数据流水线中的几乎每一项服务都重写成了 Go 形式,包括前门跟踪 API 以及请求重试服务等等。
除了内部验证服务(用 node.js 编写而成)之外,我们的其他所有服务都重写成了 Go 形式。
内部验证服务是干嘛用的?它主要负责:
验证传入的消息以确保其符合我们确定的消息格式。
将 API 密钥解析为对应的特定“源 ID”。
将这些消息发布至 NSQ。
每个容器都配备有一个完整的 vCPU 外加 4 gb 内存。要想每秒发布 20 万条消息,我们大概需要 800 个这样的容器,意味着每个容器每秒处理 250 条消息。
但我们知道,其中还有优化的空间。
现在,每项新的 Go 服务仍然使用相同的资源分配量(1 个 vCPU 加 4 gb 内存),但其中的逻辑已经完全重写并得到优化。现在,我们已经在利用 pprof 以及 flamegraphs 等工具来优化高吞吐量 Go 代码方面积累起更丰富的经验。
如今,我们只需要 340 个容器就能完成每秒 22 万条消息的处理任务。换言之,我们的单容器数据吞吐量接近 650 条,是原来的两倍有余。这也让我们将组件使用成本削减了一半以上。
此外,我们现在同时为服务运行三个实例,每个可用区各一个,这是为了避免跨区数据传输。这样的基础,让我们得以推动下一步优化。
优化数据传输成本
数据传输是云端业务运营当中最大的成本来源之一。网络传输往往独立于特定服务之外,因此极易被人们所忽略。根据我们的经验,这方面成本往往会扩大为“公共惨剧”——每个人都受到影响,但却没人愿意着手解决。
如果任其发展,必然会产生巨大的负面影响。
有些朋友可能不太清楚,这里我要稍微解释解释 AWS 根据数据量提供的几种不同网络流量计费模式。
区域间与互联网流量的发送与接收费用,为每 GB 0.02 美元。
可用区间流量的发送与接收费用,为每 GB 0.01 美元。
可用区内流量免费。
在我们的系统当中,数据传输成本几乎占总体运营支出的六分之一。太可怕了。
以下是 2018 年 10 月至 2019 年 2 月期间的费用明细(略去具体数额):
为了降低网络传输成本,我们主要采取了以下三项举措:
1.用服务发现代替 ALB
纵观 Segment 的发展历程,我们的大部分基础设施长期依赖一种非常简单的服务发现方法。我们通常使用 Route53 为运行在 AWS 上的应用程序负载均衡器(ALB)分配内部 DNS 名称。
这些 ALB 随后会将请求转发至支持服务。换言之,我们不需要自己管理服务发现,也不必指导负载均衡器如何完成消息传递以及健康检查之类的任务。
听起来倒是很省心,但在研究账单时,我们发现其中一部分负载均衡器由于传输大量数据而带来了可观的开销。具体来讲,单是负载均衡器自身的请求,每月就给我们带来数万美元的成本。
ALB 方法的第二大缺点在于,客户端会访问 ALB 当中的任意 IP——无论该 IP 是否存在于当前可用区。
结果就是,由于客户端访问的是由 DNS 查询返回的随机 IP,因此在所有指向 ALB 的请求当中,有三分之二可能跨越可用区网络边界。
到这里,问题已经非常明确:为了降低成本,我们必须转而使用更合理的服务发现解决方案,从而在实现可用区内部路由的同时消除对 ALB 的依赖。
Consul 也由此闪亮登场。
有些朋友可能不太了解:Consul 堪称 Segment 这类分布式系统中的瑞士军刀。它支持分布式锁、键值存储并内置用于服务发现的原语。
在消除 ALB 方面,我们共使用三大基础设施组件:Registrator、Consul 以及 Consul 感知客户端。
Registrator 运行在各个主机之上,同时接入 Docker 守护程序以查询运行在该主机上的各相关服务。
Registrator 随后将所有数据同步至 Consul,并以标签形式注明这些服务运行所在的可用区。
客户端在 Consul 中执行查找,以了解各服务目前运行所在的可用区。
客户端尽可能接入与自身处于同一可用区内的服务。
为了实现整个流程,客户端需要利用 EC2 元数据 API 检测自身运行所在的可用区。
对于每一条请求,客户端都会利用一项负载均衡算法考量自身与服务器间的网络拓扑。这有助于我们在整个集群体系之内平均分配工作负载。
均衡操作以客户端及服务器的数据与位置为基础,同时必须确保即使分发不够均衡,也不致在可用区内造成过高的客户端对服务器比例热点。
通过这种方式,我们有效降低了 ALB 自身成本以及可用区间的网络传输成本。
2.NAT 与公共子网
我们还决定对网络体系的整体运行情况进行审查。
传统上,我们一直将服务旋转在专用子网内以作为额外的安全层。虽然 VPC 可能会限制服务与互联网之间的连接,但这一额外的 L4 层同时也有助于控制入站流量,所以算是有得有失。
通常,与互联网通信的服务必须先经由 NAT 网关才能到达公共子网,然后经过互联网网关以最终接入公共互联网。问题在于,对 NAT 进行遍历会带来高昂的成本——既包括跨可用区流量传输成本,也包括实例吞吐量成本。
我们找到的最佳解决办法,就是将 VPC 端点与 S3 等服务结合起来。VPC 端点能够直接替代众多亚马逊服务所支持的公共 API;更重要的是,VPC 端点不会影响到用户的公共网络流量。
我们将所有通过 NAT 网关的 S3 调用,都替换成了自定义 VPC 端点,从而进一步节约了成本。
听起来太简单太有效了,对吧?但真实情况就是如此。长久以来,我们一直没有对请求进行资源优化,而且自首次使用 VPC 端点以来就一直使用公共端点。能一口气解决这么多老问题,感觉真棒!
3.Kafka 的可用区内路由
从 NSQ 转换至 Kafka 不仅成功削减了磁盘空间使用量,同时也给我们的可用区路由带来了重大提升。
不同于以往在各个可用区内分配随机队列与客户端的实现方式,如今我们以 Kafka 消费程序分组 API 为基础,专门为特定代理程序以及分区建立起“可用区友好”机制。消费程序会优先选择在自身所在的可用区内读取分区消息,从而显著减少了跨可用区间的传输流量。
从 Kafka 0.10.0.0 版本开始,来自代理的元数据响应会返回该代理所在机架的结果。下图所示为 Kafka 说明文档中的对应内容:
接下来,客户端能够从 EC2 元数据 API 中处发现自己的可用区,并决定从同一可用区内的分区处获取所需数据。
我们还就此向 Kafka 开源库提交了贡献,让这种方式成为默认选项,从而轻松减少我们自身以及其他用户的可用区间传输成本。没错,这种优先在所在可用区内读取消息的方式已经成为标准,开发人员无需做出额外调整。
成本优势是产品的核心优势
我们的这一番调整主要针对基础设施改进,并未对功能性做出任何变动;但在此期间,我们也发现系统当中存在不少冗余性质的资源浪费。
无效源
Segment 从多种来源处收集数据并将其发送至目的地。我们充当着这些数据的传输通道——以可靠方式实现数据传递,并根据需求对数据内容进行转换及过滤。但经过初步分析,我们得出了惊人的结论。
目前存在 6000 多个并未指向任何目的地,或者只能向目的地发送已过期凭证的源!换言之,我们实际上是在为这些直接接入/dev/null 的数据基础设施付费。
因此,我们采取新的举措,旨在减少这些无效源的数量。我们首先向客户发送通知,询问他们是否愿意放弃这些无效的来源。通过剔除无效源,我们每月又额外节省下超过 2 万美元的运营成本。
未过滤消息
我们还注意到,目前的基础设施中存在重复复制问题。
数据在由 Segment 转向分析工具时,会经由两种不同的传递方式。
在第一种情况下,我们直接通过浏览器传输客户端数据。如果我们的用户正在使用 Google Analytics,我们会帮助其将全部指向 Google Analytics 的调用打包成 JavaScript 对象。这种作法,能够确保用户的会话数据拥有统一的格式。
在第二种情况下,我们会通过服务器进行数据代理。采取这种方式的原因有很多,包括帮助当前页面减重、提高数据保真度,或者相关数据属于服务器代码的带外生成结果等等。
这两组数据都会被发送至我们的服务器,从而为用户提供所收集数据的完整归档。接下来,一切尚未从客户端发出的后续数据都将通过服务器到服务器 API 完成传递。
下面,我们将整个流程简化为五个步骤:
请求传入至我们的 API。
这些消息被传递给 Kafka。
消费程序从 Kafka 中读取消息,以确定应该将该消息传递至何处(消息 A 可传递至 Google Analytics,消息 B 应传递至 Salesforce)。
我们的 Centrifuge_system 负责确保在发生故障时,数据仍能得到可靠的交付与处理。
代理服务将数据转换为面向对应目的地的相关 API 调用。
在传统上,数据传递的决策点仅存在于“代理”步骤的末尾位置。
换句话说,我们需要为已经发出的消息进行大量额外的记录、写入与重复复制操作。问题是,消息都已经发出了,我们还折腾个什么劲?
所以,我们决定将整个逻辑转移到流程内的“消费程序”步骤当中。如此一来,以上冗余部分就全都自然消失了。
我们注意到逻辑的移植非常重要,我们原本的代理使用 node 编写,但消费程序却使用 Go 编写。由于同一逻辑需要在多个不同区间中运行,因此检测与控制流程也就变得非常复杂。
相信大家已经注意到这里的核心问题——我们为那些重复或者不必要的工作浪费了太多资源。因此必须认真检查每个产品区间,确保在不影响实际业务的前提下清理那些无效却又带来额外支出的部分。
经验教训
先从量化入手
如果无法衡量,也就不可能实现。首先应当以系统方法确定有望实现成本节约的所有区间,准确衡量具备可行性的成本削减与收益提升途径。这能帮助我们弄清楚哪些可以具体调整,而不只是对性能改进做出模糊不清的猜测。在这方面,我们将 CloudHealth、AWS 计费 CSV 以及 Tableau 结合起来,整理出了确切可信的量化结论。
制定计划
一旦确定了资源节约的可行区间,接下来就是制定计划以确保后续优化。团队中的每个人每周都需要执行成本削减工作。各个团队先从最简单的工作入手,而后逐步推进至最重要的成本中心。我们的 SRE 团队负责工作协调,并确保我们能够在截止日期之前完成各项成本控制任务。
建立可重复的监控流程
在成本节约方面,我们也投入了不少资源与精力,因此巩固胜利果实同样非常重要。如果在接下来的六个月中需要再次重复成本削减与优化,那么我们目前的工作将毫无意义。
为了实现必要的持续可见性,我们建立起一组可重复计算的价格动因清单,每天计算一次。整个成本流水线都被输入至我们的 Redshift 实例当中,同时每天都通过 Tableau 以可视化方式监控这些“成本动因”。
如果单项成本动因在一天内的增幅超过了预定的阈值百分比,系统就会自动向团队发送电子邮件,帮助我们快速响应并解决问题。这是一种非常高效的处理方式,我们不再需要像过去那样等待数周甚至数月才能发现成本飙升迹象。
通过业务扩大内部影响力
我们的财务团队一直在强调,毛利率的提升能够显著增强企业的整体价值。这一点在上市企业当中体现得尤为明显。以下数据来自我们的 2019 年年中内部演示文稿。
在图中,我们可以清晰看到收入倍数(x 轴)与毛利率(y 轴)之间的正相关性。
其中部分企业主营“SaaS”业务(Zoom、Cloudflare 以及 Datadog),因此 SaaS 毛利率直接决定其收入倍数。相比之下,其他企业的表现则要差一些。
毛利率达到 70%到 80%的企业,能够凭借单位经济性以及对市场的有力掌控而获得丰厚的回报。我们当然也希望成长成这样的公司。
总结
一句话来概括,我们在 90 天之内设法将公司的毛利率提高了 20%。
但接下来,还有更多工作需要完成。我们目前的实例利用率仍然远低于目标(目标为 70%,实际仅为 40%)。此外,我们也有大量 EBS 快照未经清理。但总体来看,目前的成就已经足够让我们感到自豪。
现在,我们不再需要建立一次性优化规程,而是真正将系统与监控部署到位,能够重复跟进支出占比最高的领域,并通过业务流程持续实现成本节约。
如果大家也在使用类似的基础设施,希望今天的文章能够为大家带来启发。如果能够帮助各位实现同样甚至更为出色的成本节约效果,那就太好啦!
最后,我要感谢参与此次毛利率提升计划的各位团队成员: Achille Roussel、Travis Cole、Steve van Loben Sels、Aliya Dossa、Josh Curl、John Boggs、Albert Strasheim、Alan Braithwaite、Alex Berkenkamp、Lauren Reeder、Rakesh Nair、Julien Fabre、Jeremy Jackins、Dean Karn、David Birdsong、Gerhard Esterhuizen、Alexandra Noonan、Rick Branson、Arushi Bajaj、Anastassia Bobokalonova、Andrius Vaskys、Parsa Shabani、Scott Cruwys、Ray Jenkins、Sandy Smith、Udit Mehta、Tido Carriero、Peter Richmond、Daniel St. Jules、Colin King、Tyson Mote、Prateek Srivastava、Netto Farah、Roland Warmerdam、Matt Shwery。同时感谢 Geoffrey Keating 提供的编辑协助,以及 Jarrod Bryan 为我们创作的这些精美插图。
原文链接:
https://segment.com/blog/the-10m-engineering-problem/
评论