好了,大家聚在一起,让我告诉你一个(几乎)我有幸遭受的最大工程灾难的故事。这是一个政治,建筑和沉没成本谬误的故事[我正在喝酒, Aberlour木桶强度单一麦芽苏格兰威士忌]
那是2016年。唐纳德·特朗普还不是总统,因此运动尚未发生。 TK仍然是首席执行官,我们仍处于国际推广的高速增长阶段,公众情绪绝大多数是积极的,Uber处于高潮。
但是,过度增长并非没有问题,该应用程序本身开始出现裂缝。几乎每年,工程组织的规模都会增加一倍,而当您如此快速地成长时,您最终会获得难以置信的广泛技能。加上骇客的心态
我们称其为“让构建者构建”,意味着该应用程序架构既复杂又脆弱。当时的Uber十分重视客户端逻辑,因此该应用程序会崩溃很多。我们一直在进行热修复,刻录发行等。设计的扩展性也很差。
由于所有这些问题,组织的各个级别开始出现越来越多的运动,围绕着“从头开始重写应用程序”的想法。快一点
因此,成立了一个团队来为该新应用构建新的移动架构。该团队的主要工作是建立一种架构,以“在未来5年内维持Uber的移动开发”。我们同时完成了两个平台。产品和设计也重新开始。
在iOS的世界上,重写提供了采用Swift的机会(在此期间,该版本为2.x)。 Uber以前曾尝试过Swift,但像许多早期采用它的人一样,它极有问题,因此在重写之前已被禁止。
但是架构团队的普遍感觉是,Swift的大多数问题都集中在那时的Objective-C互操作性上,所以如果我们编写一个纯Swift的应用程序,就可以避免出现主要问题。
此外,还推动在Android和iOS上使用相同的主要架构模式。当时的android爱好者是RxJava的忠实拥护者,并且有一个等效的RxSwift库利用了Swift中的功能编程范例。看起来直截了当
因此,这个较小的设计,产品和架构核心团队在几个月之内就开始使用其新的功能/反应模式,新的语言和新的应用程序。一切顺利。该架构在很大程度上依赖于Swift的高级语言功能。
UI设计可针对Uber提供的越来越多的产品进行扩展,功能性编程范例非常强大(尽管有些学习曲线),该架构围绕我们基于实时流的新型网络协议(这就是我写的部分)。
几个月后,经过一些浮华的演示,势头开始增强。该项目看起来很成功。他们在短时间内与少量工程师建立了惊人的经验。大多数核心产品都是内置的。高管被卖掉了。
因此,公司范围内的部署开始了。团队开始将所有精力都转移到将功能引入新应用中。起初,新产品的兴奋创造了一连串的动力和生产力。该架构是为实现功能隔离而构建的,使团队可以快速行动
但是,一旦Swift开始扩展到超过十名工程师的规模时,车轮就开始脱落。 Swift编译器的速度仍然比Objective-C慢得多,但那时实际上是不可用的。建造时间花在了屋顶上。提前输入/调试完全停止工作。
在我们的谈话之一中,有一段视频是关于Uber工程师在Xcode中键入一条单行语句,然后等待45秒让这封信慢慢一步一步出现在编辑器中的。
然后我们用动态链接器碰壁。当时,您只能动态链接Swift库。不幸的是,链接程序是在多项式时间内执行的,因此Apple建议单个二进制文件中的最大库数为6。我们有92个,并且还在继续。
结果,在敲击应用程序图标后,花了8-12秒才调用main。我们灵巧的新应用程序比旧的笨拙的应用程序慢。然后二进制文件大小问题出现。
但是要回答@tapbot_paul的原始问题,当问题开始认真出现时,我们已经远远超过了不退缩(沉没成本谬误)的地步。此时,整个公司都将精力投入到新的应用程序中。
每个学科都有成千上万的人,花费了数百万美元(我无法告诉您真实的数字,但实际上是超过1美元)。整个管理链完全被买断了。我私下里与我的董事进行了“我们需要停止”的谈话。
他告诉我,如果这个项目失败,他不妨收拾行囊。一直到副总裁,他的老板也是如此。没有出路。
因此,我们袖手旁观,将最好的人放在每个问题上,并对发布的关键问题(动态链接,二进制大小)进行优先级排序。我被分配了动态链接和二进制大小的顺序。
我们很快发现,将所有代码放入主可执行文件可解决App启动时的链接问题。但是众所周知,Swift用框架来填充命名空间。因此,这样做将需要进行大量的代码更改,其中涉及无数的名称空间检查。
那时,才华横溢的Richard Howell(不确定他是否在Twitter上)在读取Xcode构建输出时发现,他可以获取所有中间目标文件,并在构建完成后使用自定义脚本将它们重新链接回主可执行文件。
由于Swift在编译时会将对象名称空间转换为符号名称本身,因此这意味着他可以安全地保留名称空间。这使我们可以有效地静态链接我们的库,并将主时间从10减少到基本上为0。
下一个问题:应用大小。当时,我们计划将新应用程序包含在旧的应用程序包中,并在运行时作为安全网缓慢地推出它。我们购买空间的第一件事就是删除旧应用。我们将此发布策略称为“ Yolo”。 TK本人打了电话。
我们还用类替换了所有* Swift *结构。值类型通常由于对象变平以及复制行为和自动初始化程序等所需的额外机器代码而导致大量开销。这节省了我们的空间,因此我们继续进行下去。
但是随着应用程序的不断发展。很快,我们的通用二进制文件(iOS 8和更早版本)达到了地窖下载限制(100 mb)。这表示大量的注册失败(要花掉我们尚未升级的8位用户的费用,这笔钱要花掉我们的钱)。
至此,我们距离公开发布日期还差几周。我们很高兴地收到了我仍与NDA合作的某家公司的帮助,但他们无法解决我们的问题。我们唯一能做的就是重新生成所有模型代码(占总行数的25%)
返回到Objective-C或放弃对iOS 8的支持。由于iOS 9引入了单独的体系结构切片,因此它的大小实际上是(允许或接受)大小的一半。只剩一个星期了,我们决定吃掉8位数字并放弃对iOS 8的支持。
人们普遍认为,iOS 9二进制文件的大小只有一半,我们仍然有足够的空间,重写完成后,我们可以在将来的某个时候解决问题,因为事情会有所放慢。不幸的是,我们对此完全错误。
应用发布后,我们举办了一个大型聚会。该应用程序受到了媒体的好评。它快速,灵活,并采用了新颖的设计。一堆人得到了晋升。我们都松了一口气。 90个工作周停了几个星期。
但是随后公众舆论开始转变。新的应用程序以让客户首先输入目的地为中心,这样他们就可以提前获得价格(在过去,价格旁边仅是一个乘数)。
如果不手动输入取件位置,人们的位置将显示为最后一次接收到的GPS位置。这可能是非常不准确的(尤其是在高楼大厦的城市中),并且驾驶员最终会走错路。这是一次可怕的客户体验。
因此,为了改善位置取货,我们更改了位置权限以在后台收集信号,以便可以将驱动程序发送到您当前的位置。人们吓坏了。我的一些前Twitter大学呼吁我退出这样一家邪恶的公司,该公司会像这样跟踪您。
由于上述原因,人们关闭了位置权限。(关于此的其他话题涉及@gruber和@TechCrunch,我将在以后再解释)。但是新应用没有设计任何经验来处理此用例。
所以我们争先恐后地回填了经验。我们曾讨论过关闭后台位置,但是这样会再次破坏客户的体验。
然后在特朗普进入白宫之后(这是新应用发布后约三个月),这引发了连锁反应,导致运动开始。这也是另一条线索,但基本上,纽约市出租车工会抓住了由
这项旅行禁令指控Uber通过关闭Laguardia的激增来罢工。这完全是谎言,没有激增的定价,供应会立即枯竭(没有人会在没有额外动力去机场的情况下开车去机场)。但是谎言变得病毒化了。
一直以来,Swift代码的增长一直在继续。持续的问题和缓慢的开发人员环境在Uber的iOS工程师中创建了两个交战的政治派系。我将它们称为“快速狂热者”和“ Objective-C轻弹头”。
因此,外部压力和内部派系的共同作用意味着紧张局势很高。狂热者否认斯威夫特造成的问题。在没有提供太多解决方案的情况下,curmudgeons抱怨您可以想象的一切。
恰好在这个时候,应用程序大小问题引起了我们的注意。我正在电话中,发布团队无法提交该应用程序。事实证明,我们对动态链接问题的出色解决方案现在创建了一个主要的可执行文件,对于某些拱形来说太大了。
因此,在解决了该问题之后,我和@aqua_geek进行了一些挖掘,发现我们的编译后代码大小每周增长1.3 mb。我向连锁店发出了警报。以这种速度,如果我们不做任何事情,我们将在3周内达到手机下载限制。
但是内部定义变得如此强大,以至于我们完全否认了这一点,斯威夫特阵营的一位技术负责人写了两页的论文,说明蜂窝下载限制无所谓(毕竟很久以前,Facebook炸毁了它) )我们也对灭火感到厌倦。
因此,我们的一位数据科学家设计了一个测试,方法是人为地将架构片段之一推到极限之上,然后测量对业务指标的影响。下周,我们将该切片向后拉,然后将另一个切片推到了极限(以控制足弓)。
后果是灾难性的。对业务的负面影响比一年多Swift重写的总成本大了几个数量级。事实证明,当他们第一次下载Uber应用程序(知道吗?)时,大量人都在蜂窝网络上。
因此,我们成立了另一个罢工小组。我们开始对目标文件进行反编译,并逐行遍历符号,以确定为什么我们的Swift代码大小如此之大。我们删除了未使用的功能。泰勒必须将watchOS应用重新写回objc。
(手表应用程序只有大约4400行,但是由于处理器架构不同,并且没有ABI兼容性,因此我们必须将Swift运行时的完整副本复制到手表包中)
我们正处于断裂点。很累。但是所有人都集会了。这是真正的才华横溢的工程师开始发光的时候。阿姆斯特丹的一位开发人员弄清楚了如何重新安排编译器优化的过程。对于那些不是编译器工程师的人,我将进行解释。
现代编译器对我们的代码进行了大量的传递。例如,一遍可能会内联您的函数。另一个可以用它们的值替换常量表达式。根据它们执行机器代码的顺序,它们可能更大或更小。
如果您的内联函数传递了一个常量,则编译器可以对此进行推理并替换整个内容,因此,如果内联传递首先进行,则int x = 3 func(x){X + 4}将只是一个常数(7)少得多的代码)。
如果内联排名第二,则常量传递将无法推理出函数体,最终您将获得更多代码。当然,所有这些都完全取决于您所编写的代码,因此通常很难优化通过顺序
因此,阿姆斯特丹的杰出工程师说,他在发布版本中构建了退火算法,以便以最小化尺寸的方式对优化过程进行重新排序。这使机器代码的总大小减少了11 mb,并为我们提供了足够的跑道来保持开发的进行。
这让Swift编译器工程师感到震惊,他们担心未经测试的编译器通过命令会暴露出未经测试的错误(即使每次通过都被认为是内部安全的,也很难为可能的组合做出推理)。不过,我们没有任何重大问题。
我们还应用了许多其他解决方案(替换了特别昂贵的代码模式)。我们用节省下来的正常开发周数衡量了每一周。但是真正的问题是增长曲线。它总是使我们的赢利减少。
最终,我们购买了足够多的设备,使苹果公司将蜂窝下载限制提高到150个。他们还添加了许多编译器功能,以帮助优化大小(-Osize)。自己承认,Swift永远不会编译成像Objective-C一样小的文件
但是到了今年,他们已经将Swift的大小降低到了Objective-C的机器代码大小的1.5倍,最终他们又将其提高到200 mb的可选限制。我们有足够的跑道可以再走几年。
我们几乎失败了。如果苹果没有提高限制,我们将不得不在ObjC中重新编写Uber App。最终,我们也能够解决其他问题。出色的@alanzeino和团队为BUCK添加了Swift支持,从而大大缩短了构建时间。
一堆人烧死了。花了一大笔钱,吸取了惨痛的教训,但是直到今天,大多数人仍然坚持认为重写是值得的。新加入的工程师热爱架构的一致性,却从不知道到达那里所付出的痛苦。
广大社区受益于我们的学习。 @ ellsk1进行了精彩的演讲,并进行了一次演讲,以分享我们的知识。在继续前进并教其他团队如何制定更好的决策后,我得以与我一起经历。
所以我的建议。计算机科学中的一切都是权衡的。没有普遍适用的语言。无论您做什么,都要了解要进行权衡的原因。不要让它陷入有思想的派系之间的政治战争。
建立故障点。弄清楚如何识别折衷方案,并在遇到某个特定点时给自己一个出路,以意识到自己犯了一个错误。艰苦的工作很辛苦,但是错误选择的时间越长,成本就会增加。
不要成为对解决方案没有帮助的Curmudgeon。不要成为一个狂热者,为其他所有人带来更大的问题。与我合作过的最好的工程师真的很擅长不落入这两个陷阱。
谢谢大家与我一起旅途。这是相当治疗的。晚安! [结束线程]