type
status
date
slug
summary
tags
category
comment
icon
password
先求实现,再求优美,最后求速
前几天有人问了我一个有趣的问题:“你是如何解释在运营一家初创公司的同时,还要冒这么大风险去构建 Storm 的决定的?”(Storm 是一个实时计算系统)。我能理解从外部人士的角度来看,投资这样一个庞大的项目对于一家初创公司来说似乎风险极大。然而,从我的角度来看,构建 Storm 根本没有风险。它很有挑战性,但并非冒险。
我遵循一种开发风格,它极大地降低了像 Storm 这样大型项目的风险。我称这种风格为“面向痛苦的编程”(suffering-oriented programming)。面向痛苦的编程可以这样总结:除非你深切体会到缺乏某项技术所带来的痛苦,否则就不该去构建它。这既适用于重大的架构决策,也适用于较小的日常编程决策。面向痛苦的编程通过确保你总是在处理重要的事情来大大降低风险,并且它确保你在尝试大规模投入之前,已经对问题领域有深入的了解。
我有一个面向痛苦编程的信条:“先求实现,再求优美,最后求速。”
先求实现 (First make it possible)
当遇到一个你不熟悉的问题领域时,一开始就试图构建一个“通用”或“可扩展”的解决方案是错误的。你对问题领域的理解还不够深入,无法预测未来的需求。你会将不需要通用的东西做成通用的,增加了复杂性并浪费了时间。
更好的做法是就直接上手解决手头的问题。这让你能够完成需要做的事情,避免无用功。在你动手实践的过程中,你会越来越了解问题空间的复杂性。
Storm 的“求实现”阶段,是为期一年使用队列和工作进程(workers)来构建流处理系统的实践。我们学会了使用“ack”协议来保证数据处理。我们学会了通过队列和工作进程的集群来扩展我们的实时计算。我们了解到有时你需要以不同的方式对消息流进行分区,有时是随机的,有时使用哈希/取模技术,以确保同一个实体总是流向同一个工作进程。
我们甚至不知道自己正处于“求实现”的阶段。我们只是专注于构建我们的产品。然而,队列和工作进程系统的痛苦很快变得严重起来。扩展队列和工作进程系统非常繁琐,而且容错性远未达到我们的期望。很明显,队列和工作进程范式并未处于正确的抽象层次,因为我们大部分代码都与路由消息和序列化有关,而不是我们关心的实际业务逻辑。
与此同时,开发我们的产品促使我们在“实时计算”问题领域发现了新的用例。我们为产品构建了一个功能,用于计算一个 URL 在 Twitter 上的触达人数(Reach)。触达人数是指在 Twitter 上接触到某个 URL 的独立用户数量。这是一个困难的计算,单次计算可能就需要数百次数据库调用和数千万次的展示(impressions)来进行去重计算。我们最初在单台机器上运行的实现,对于难处理的 URL 可能需要超过一分钟,很明显我们需要某种分布式系统来并行化计算,使其变得快速。
催生了 Storm 的一个关键认识是,“触达人数问题”和“流处理”问题可以通过一个简单的抽象来统一。
再求优美 (Then make it beautiful)
通过动手实践,你在探索问题空间的过程中会形成一张该领域的“地图”。随着时间的推移,你会积累该问题领域内越来越多的用例,并对构建这些系统错综复杂之处产生深刻的理解。这种深刻的理解可以指导你创造出“优美”的技术来取代现有系统,减轻你的痛苦,并使得那些以前难以构建的新系统/功能成为可能。
开发“优美”解决方案的关键在于找出能够解决你已有的具体用例的最简单的抽象集合。试图预测你实际上没有的用例是一个错误,否则你最终会过度设计你的解决方案。根据经验法则,你试图进行的投资越大,你就需要越深入地理解问题领域,并且你的用例需要越多样化。否则你就有可能陷入第二系统效应的风险。
“求优美”是运用你的设计和抽象能力,将问题空间提炼成可以组合在一起的简单抽象的地方。我认为开发优美抽象的过程类似于统计回归:你在图上有一组点(你的用例),而你正在寻找拟合这些点的最简单曲线(一组抽象)。

你拥有的用例越多,你就越能找到拟合这些点的正确曲线。如果你没有足够多的点,你很可能会过度拟合或欠拟合图形,导致无用功和过度设计。
求优美的很大一部分是理解问题空间的性能和资源特性。这是你在“求实现”阶段学到的复杂细节之一,你应该在设计优美解决方案时利用这些学习成果。
对于 Storm,我将实时计算问题领域提炼为一小组抽象:streams(流)、spouts(数据源)、bolts(处理单元)和 topologies(拓扑)。我设计了一种新的保证数据处理的算法,消除了对中间消息代理的需求,而这部分是导致我们系统复杂性和痛苦最多的地方。流处理和触达人数计算这两个表面上截然不同的问题,都能与 Storm 如此优雅地契合,这是一个强烈的迹象,表明我意识到这非同小可。
我采取了额外的步骤来获取更多 Storm 的用例并验证我的设计。我广泛征询了其他工程师的意见,了解他们正在处理的实时问题的具体细节。我不仅仅问了我认识的人。我还发推特说我正在开发一个新的实时系统,并想了解其他人的用例。这引发了许多有趣的讨论,使我对问题领域有了更深入的了解,并验证了我的设计思路。
最后求速 (Then make it fast)
一旦你构建了优美的设计,你就可以安全地投入时间进行性能分析和优化。过早进行优化只会浪费时间,因为你可能还会重新思考设计。这被称为过早优化。
“求速”并非关乎系统的高层级性能特性。对这些问题的理解应该在“求实现”阶段获得,并在“求优美”阶段进行设计。“求速”是指微优化和精简代码以提高资源效率。因此,你可能在“求优美”阶段关注渐进复杂度等问题,而在“求速”阶段专注于常数时间因子。
周而复始 (Rinse and repeat)
面向痛苦的编程是一个持续的过程。你构建的优美系统赋予你新的能力,使你能够在问题空间的新领域和更深层次“求实现”。这会将学习成果反馈给技术。你通常需要调整或增加你已经提出的抽象,以处理越来越多的用例。
Storm 就经历了许多这样的迭代。当我们刚开始使用 Storm 时,我们发现需要能够从单个组件发出多个独立的流。我们发现增加一种称为“direct stream”(直接流)的特殊流,可以让 Storm 将元组(tuples)批处理为一个具体单元。最近我开发了“transactional topologies”(事务性拓扑),它超越了 Storm 的至少一次处理保证,使得几乎任意的实时计算都能实现精确一次消息语义。
就其本质而言,在一个你不太了解的问题领域动手实践和不断迭代可能会导致一些粗糙的代码。面向痛苦的程序员最重要的特征是对重构的持续专注。这对于防止偶然复杂性破坏代码库至关重要。
结论
在面向痛苦的编程中,用例就是一切。它们极其宝贵。获取用例的唯一方法是通过动手实践积累经验。
大多数程序员都会经历某种演变。你一开始还在费劲地让程序运行起来,代码毫无结构。代码粗糙,复制/粘贴很普遍。最终你会了解到结构化编程和尽可能共享逻辑的好处。然后你学会了进行通用抽象和使用封装来更容易地推理系统。接着你变得痴迷于让所有代码都通用,让事情可扩展以让你的程序能够适应未来的需求。
面向痛苦的编程反对你能有效预测你当前没有的需求。它认识到,在没有深入理解问题领域的情况下试图使事情通用化,会导致复杂性和浪费。设计必须始终由真实的、具体的用例驱动。
引用与参考
动手实践积累经验
- 作者:KAI
- 链接:https://blog.985864.xyz/technology/1d5805b5-5b95-805d-87d8-cd10715761bb
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。