阅读 10418

我在掘金这3年 - 如何给飞行中的飞机换引擎

打算写几篇文章作为我在掘金担任技术总监这3年的总结. 分别从技术, 产品, 管理这三个角度谈一谈我的思考和收获.

本篇是其中第一篇, 以时间顺序来描述一下我2016年底刚到掘金的时候, 将一个运行在ServerLess的技术社区--掘金, 整站重构迁移到公有云上的技术规划和实施过程. 并作一些反思.

故事的开始

我刚到掘金的时候, 有2个Android工程师, 2个iOS工程师, 3个前端工程师. 没了. 这就是开发力量的全部人员配置. 这个人员配置支撑了掘金的前20万注册用户的服务.

而掘金则是用一个ServerLess平台搭建起来的, 我们的服务器端实现全是node.js. 然后提供接口给所有客户端调用. 平台代替我们完成了基础的数据库驱动, 数据模型的存储, 基础的ACL机制, 内容存储等底层抽象, 用户相关功能(整套用户中心解决方案)等. 使用它的SDK就可以搭建一个小规模应用了. 掘金正是这样快速诞生的.

但很快, 我们就遇到了一些问题.

故障. 是的, 而且是系统性故障, 由于ServerLess平台集成度非常高, 导致系统性故障的情况下我们连操作面板都打不开, 如果是自己搭建的系统, 起码也可以有个降级. 但完全依赖一整个系统的话, 在系统性故障面前除了等待平台方修复以外我们束手无策.

性价比问题. 大家都知道按量收费在请求量小的情况下是十分划算的, 而请求量超过一定程度反而固定量收费的模式会更经济. 这与手机流量套餐是一个道理. 我们业务的请求量已经越过了按量收费的经济区间. 成本开始大幅上升了.

平台架构完全的黑盒. 除了我们使用的SDK, 我们对平台的架构和基础设施一无所知. 最外侧能操作的边界是自己的域名, 内侧能操作的边界是自己业务逻辑的代码. 除此之外, 是完全的黑盒. ServerLess 虽然卖点是开发者完全不需要关注这些, 但实际工业实践中 ServerLess 并不是万能的.

架构腐败. 代码复杂度已经膨胀到了现有人手无法处理的情况. ServerLess 的理念是讲究尽可能轻巧的, 但现有架构的耦合成都相当高, 平时甚至需要经常单独安排时间来重构代码. 看似重构是为了让代码组织更流畅, 但其实这是发生了更深层次的问题.

代码需要经常重构直接说明了这么几个问题:

  • 现有架构设计跟新的需求冲突很大, 每次修改都与原有架构不兼容
  • 现有架构开发复杂度高, 需要对当前的布局和模式有相当深入的理解才能进行修改, 否则架构就会腐败.
  • 现有架构扩展横向复杂度(代码量则增加)会导致纵向复杂度(组织代码的代码)剧烈增加, 而初创公司业务发展又是十分迅速的, 导致代码量增长非常快, 这就会被迫疯狂重构. 这也是为什么大型项目需要设计模式的原因. 大家需要一个统一的方式来管理业务横向复杂度的增长, 而管理本身会增长业务的纵向复杂度. 通俗点来讲就是代码量太大需要适当的设计模式来组织代码, 而代码越多, 需要的设计模式和组织代码也越复杂, 即纵向复杂度增加了. 横向复杂度可以通过人手来解决, 而纵向复杂度则需要富有经验的,熟悉当前架构的,并对接下来业务衍化有一定预知能力的工程师来解决.

我给出的能很好的处理纵向复杂度的这三个必要条件是经过经验和思考得出的结论:

  • 富有经验的代表能看懂当前架构
  • 熟悉当前架构的代表能迅速针对当前架构做出修改
  • 并对接下来业务衍化有一定预知能力的则代表能对当前架构做出正确方向的修改

基于这些原因和思考, 我打算彻底改变旧有架构, 为掘金设计一个面向100万注册用户服务的架构.

刚有这种想法的时候我也是很谨慎的的, 相信各位多少在职业生涯中都经历过重构项目. 重构有多大风险就有多大.

但支撑我更换架构的最主要动力是无论做不做出改变都会面临同样的风险的局势. 如果做出改变, 可能会有技术失败的风险, 时间和成本的消耗导致公司无法继续运营下去. 如果不做出改变, 虽然当前没问题, 但在不远的未来一定会陷入无尽的故障修复, 疲于维护, 业务无法前进的地步.

但唯一能确定的是, 如果掘金在短时间都无法增长到100万注册用户, 那后续发展也没有什么意义. 只有快速增长, 未来才有存活的机会. 因此只有做出改变这一条路.

好, 既然决定去做了, 那就要尽可能快的去实施. 不过就在这个时候, 我遇到了新的问题. 人手不够.

是的, 工程师的数量补充不上来. 虽然我已经尽力降低招聘的工程师的要求了, 但是在面试中可能10个面试者里能简单实现链表的人连一个都没有. 更别提招聘到具有一定水平, 丰富经验的工程师了. 初创公司没什么名气的话, 本身就不是很吸引人, 想要招聘到大厂工程师不是给到大厂工资就能解决的问题, 初创公司去大厂挖人很可能都是去了就Double的. 而我们没有这么多预算. 而通常招聘的话, 根据我的经验一般在头部公司, 初级职位的话2周内都能招聘到理想的候选人. 但摆在掘金面前的是3周都不一定有满意的候选者.

这使我不禁做出了一个假设, 会不会掘金可能在很长一段时间都面临这种工程师整体水平无法提升的情况?

在我写这篇文章的时候, 距离做出这个猜想已经过去了4年. 事实证明, 这个猜想是正确的. 初创公司如果不花大价钱, 那就无法稳定获得和维持优秀的工程师. 这是得到的血的教训.

那么, 回到当时, 在工程师水平不高, 人手不够的情况下, 如何构建一个能支撑100W用户,能迅速推进新业务,成本又不是特别高的系统? 在继续之前, 我们需要一些理论基础, 这些理论和思考, 可能还处于非常肤浅的程度, 不过从实践经验上说, 是成功的, 导出了我的答案. 因此与大家分享一下, 希望抛砖引玉.

枯燥的概念环节

服务大小的概念

服务的大小是个很有意思的话题. 我们为什么不把所有代码组织到一个服务? 该如何设计服务的大小?

我们不如先来看服务大小的区分. 服务大小并不是指一个服务代码量的大小, 而是指组成一个业务的服务之间的组织方式. 根据组织方式我们可以尝试对服务大小进行分类.

我们先假设一个固定大小的业务, 比如 Wordpress, 他是一个CMS类型的完整的业务.

那么显而易见, 服务大小的上边界就是这个 Wordpress 了. 我们可以先简单定义, 整个业务只依靠进程内部通信 (无任何网络通信) 并将整个业务组织成一个 repo, 即为服务大小中的最大模式.

接下来我们将 Wordpress 无限拆分, 拆分到每个 function 都构成一个服务 (因为再细分已经毫无意义, 把一个大小, 功能适当的 function 再拆开只会降低性能徒增复杂度). 那么以 function 为服务最小单位的 repo 组成的业务就是服务中的最小模式了.

为了讨论的简便, 我们直接把最小服务模式叫 Picoservice. 最大模式叫 Polyservice. Polyservice 的话我们刚才说的 Wordpress 就是个很好的例子, 现实中 Polyservice 有很多, 传统业务可能都是这样组织代码的, 相信大家也都见到过. 但每个 function 都拆分成服务的业务可不多见, 我粗略统计了下 Wordpress 大概有一万个 function. 可以想象将 Wordpress 拆分成 Picoservice 最后组织成业务是多么可怕的事情. 现实中只有 FAAS 平台上运行的业务可能是以单个 function 的形式组成并运行的.

依据服务大小的定义, 我们可以把现有的服务类型按照大小进行排序:

Picoservice <= FAAS(or ServerLess) < Microservice < Monoservice <= Polyservice

注意服务大小服务部署规模没有关系, 无论是 Picoservice 还是 Polyservice, 只要设计得当都可以多机部署以提升性能.

而为什么 FAAS 这种接近于每个 function 都要单拆分成服务的模式会出现呢? 很大的原因在于, 它可以降低每次编写成本. 如果程序足够小, 目标足够单一, 那么每次编写或修改的成本就会足够的低. 这种思想类似unix, 编写目标和功能单一的程序, 并用管道将程序组织起来完成更复杂的工作. 另外一点则是, 现在的硬件性能足够强大了, 可以这么挥霍, 牺牲一部分性能来换取便捷性.

微服务则是这种思想的以业务为单元的架构模式. 所以我觉得微服务也可以叫做 BAAS (业务即服务, Business as a Service).

那么同理, 现在很流行中台这个概念, 中台其实就可以看作将微服务中共通的基础部分抽象出来, 将这部分形成Monoservice, 以提升性能和管理水平(即由专门部门来维护提供较高的可用性和符合预期的一致性).

服务依赖模式的概念

引出了服务大小的概念, 我们就可以开始讨论服务之间的依赖模式.

首先我们知道, 是不是 Polyservice 与是否满足低耦合高内聚没有必然关系. 那么如果是接近 Picoservice 的情况呢? 当然我们不会真的每个 function 都弄个接口来跑, 但我们假设我们的服务要拆分, 该怎么去拆分呢? 很显然, 我们需要尽可能在容易断开的地方进行切割. 容易断开的地方比如业务的自然逻辑, 业务的子功能, 外部依赖, CPU内存等资源密集型逻辑的边界, 复用的多的逻辑等等.

我们同样以理想模型开始入手. 这里我们讨论的都是纯粹的数据耦合(Data Coupling).

串行ABC模式 (ABC Serial Pattern)

即:

A->B->C
复制代码

这样的业务进行拆分的话, 只要不将 A, C 放到一起, 就不算失误, 因为 A, C function 毫不相干, 放在一起就成了 偶然内聚性(Coincidental Cohesion) 毫无内聚可言.

相互 AB 模式 (AB Mutual Pattern)

A <-> B
复制代码

两节点的话, 决定是否拆分都可以, 没有别的条件的话就不需要过多考虑.

树形ABC模式 (ABC Tree Pattern)

即这样的模式:

ABC-Tree-Pattern:

 A
↓ ↓
B C
复制代码

ABC-Turn-Tree-Pattern:

B C
↓ ↓
 A
复制代码

对于这两种模式, 同样保证只要不行成 偶然内聚性(Coincidental Cohesion) 即把 B, C 放到一起, 就不算失误.

环形ABCD模式 (ABCD Loop Pattern)

即:

A -> B
↑    ↓
D <- C
复制代码

这种模式通常在回调函数中常见. 一般适合大端放到一起以提升性能.

这4种最小模式就是组成业务调用的最常见模式了, 接下来我们来看真实世界的程序的调用模式:

我们可以看到程序的调用图 (Call Graph), 基本都是这几种模式的组合. 一个大的业务的话, 其实也是类似这样的树形的调用模式(如图, 微服务的Kiali调用图).

那么, 我们根据上面的基础模式, 知道了不适合拆分成什么状态. 但我们对适合拆分成什么状态还没有头绪, 这时候我们就要引入一些其他参考条件了.

自然业务的最小单元

我们都知道需求产生业务, 没需求就不用写代码了. 那么业务的最小单元是什么呢?

我们定义, 只包含一个功能的操作的集合即为最小业务单元(Minimal Business Unit). 比如文章业务(包含创建文章, 读取文章, 更新文章, 删除文章等操作). 而其中的操作即为最小操作单元(Minimal Operating Unit).

一般来讲, 按照业务拆分的最小极限即按照最小业务单元进行拆分. 而如果为了性能, 可以在局部提升到按照最小操作单元拆分. 甚至为了极限性能, 操作单元内部还要进行拆分. 比如微博的数据流的生产, 大V和普通用户的模式是不一样的.

我的实践经验是, 如果程序的一部分逻辑有以下一个或多个行为, 就可以考虑拆分了:

  • 只被其他部分调用, 即处于调用图的底部的逻辑 (通常可能是工具类或存储类)
  • 被很多其他部分调用
  • 调用很多其他部分
  • 业务需求变化非常频繁的部分
  • 使用者不同的部分
  • 请求耗时长的部分
  • 代码量大显著大于其他部分

用最小业务单元模式拆分

我们通过服务大小的概念了解了, 服务越小越容易做出修改, 当然性能也会越低. 而通过服务依赖模式的概念我们知道了, 该如何组织服务以让我们的服务间依赖更简单.

我们在上面讲了中台这个概念的出现, 它将微服务之间公用的部分抽取出来以提升性能. 那么我们自然可以想到, 有没有与中台相对的概念呢? 比如, 散台(这里将中台的中理解为集中的中, 而不是中间的中)? 又或者说, 有没有一种, 不但容易修改, 而且修改不仅对自身影响较小, 对其他周边影响也很小的模式?

好的, 我们来讲一个服务大小中介于FAAS和微服务之间的模式. 我叫它最小业务单元模式(Minimal Business Unit Pattern). 即业务单元为模式的基础单元, 它在实践中的特殊之处在于, 鼓励以复制的方式复用, 来降低不同业务对同一逻辑的复用程度. 进而将业务的变化对业务之间的影响降低到最小.

比如一个业务被很多其他业务调用, 现在新业务需要再次调用这部分逻辑, 并且有很多与原来逻辑不一样的地方. 传统的重构模式会在原来的代码并在基础上开发, 中台模式会在这个逻辑足够大的时候抽取出来集中化形成基础设施, 而这个模式会直接写一部分重复代码连同新的业务拆分出去.

这是一种反模式, 虽然它也是一种复用, 但直接违反了DRY(Don't repeat yourself)原则. 带来的坏处直接导致代码量膨胀, 维护成本上升, 维护困难. 但这次我们来看它会有什么好处:

  • 首先它最大的好处是可以降低代码修改引入Bug的几率. 因为旧代码不变就不会引入新问题.
  • 其次可以以横向复杂度的增长(代码量的膨胀)替代代码纵向复杂度(为了组织代码而产生的管理逻辑, 设计模式就是个最好的例子)的增长.
  • 再次它的成本很低, 在原有代码基础上修改不仅需要熟悉原有业务, 修改成合适的组织模式, 修改完毕后还需要测试原有业务(因为不能保证修改后原有业务没问题), 这些都是成本. 而新业务只需要复制需要的逻辑并新增逻辑就行了.
  • 最后不会导致故障集中化, 这与生物学中遗传变异的原理是一致的, DNA(可以看作逻辑)在复制过程中产生的变化, 可以在病毒(可以看作Bug)大范围爆发的时候降低感染面积. 同时复制后, 单个程序里复制的部分挂掉不会影响到其他使用这份代码的副本, 而在一个业务里共享这部分代码的程序, 一旦这部分代码挂掉了, 则整个使用这部分代码的业务都会挂掉, 最好的例子就是配置中心这个在大公司常见的业务, 一旦配置中心挂了, 所有业务都会挂掉. 而把配置放到代码里, 虽然不是个好的实践, 但却不会面临这种问题.

但是, 一定要搞清楚这种拆分反模式什么时候适用:

  • 实施反模式的业务一定要拆分得足够小, 这是反模式成立的前提, 接下来几条都会依赖这一条规则.
  • 这种反模式适合实验性新业务频发爆发的情况. 即产品经理在实验他的新想法. 这种时候, 与其在原有的业务上修改, 然后发现并没有用户喜欢这个新功能, 最后新增逻辑被扔在业务里只会是个定时炸弹(无论是Bug还是安全隐患都会有). 不如直接写个新的业务, 将需要的部分复制出来进行重复. 这样不需要的时候就可以直接下线. 也不会影响到原有的业务. 变更成本比在原有代码上开发要低很多.
  • 这种模式实施完毕后, 再次修改的情况不适合重构, 更适合重写. 只要业务够小, 那么重写的成本就会无限低. 重写引入新Bug的几率也会相应下降.
  • 这种模式适合维护人员频繁变更的情况. 比如公司人手不够, 又或者人员更迭频繁, 那么在原有代码基础上修改就需要理解原有代码的组织模式, 而新增则直接新写一个就可以了.
  • 实施反模式的另外目的在于消除业务间的调用, 如果实现完毕反模式仍然会调用源业务, 那么就说明实施的不彻底.

是他先动手的

动手

好的, 抽象的概念到此为止. 让我们来看看上面这么多都讲了些什么:

  • 我们先说了不同大小的服务的的特征
  • 然后我们又讲了如果要将服务弄小的话, 该怎么拆分, 什么情况下适合拆分
  • 最后我们又讲了一种拆分到非常小的, 每次修改成本非常低的最小业务单元模式

好的, 我们通过对服务大小和服务间依赖模式的分析得出了一种每次编写成本低, 对周边影响低, 当然性能也低的模式 -- 最小业务单元模式.

再来看我们的目前的情况, 人手不够(没时间处理纵向复杂度), 人员水平不高(没能力处理纵向复杂度). 所以很适合使用这种横向复杂度暴涨但纵向复杂度不高的模式.

那么性能问题怎么解决? 对于总体性能问题, 这里就是WEB业务的先天优势了, 只要部署副本, 总体性能就会按照副本数量线性提升. 而对于单次请求性能问题, 我们知道, 提升性能有两种途径, 做减法, 即减少逻辑自然性能就提升了. 和 集中化, 即跟中台一样, 将性能敏感的部分集中化以提升性能.

最终, 我们终于得出了解决当前和未来一段时间人员问题的最终方案 -- 使用最小业务单元模式重新编写所有业务, 并在性能不够的核心部分使用传统的集中化模式以提升性能.

阿基里斯按住了乌龟

重构绝不是一蹴而就的, 一次性将业务停掉把新系统写好然后一次性把流量都切换过去简直是自杀行为. 有序的, 尽量不影响现有流量来进行切换, 成功的概率才会增加. 所以, 切换的流程正好与从0开始编写一个大型业务相反, 先切换最上层的边缘业务, 然后逐渐向内进行, 最后切换核心, 是比较好的实践方式.

大家都知到阿基里斯和乌龟的故事, 而业务重构过程也类似这个感觉. 在重构的过程中, 如何保证旧有业务代码不再继续膨胀是重构成功的关键之一, 否则旧代码一直膨胀, 新业务永远也没有上线的机会.

这种情况很简单, 把原有业务代码变为只读就可以了. 我与业务团队制定了规则, 从一个时间点开始, 不再向旧有平台新增代码. 新增业务一律直接从头开始写新的服务.

重构业务如同给鱼换鱼缸, 需要4样东西参与整个过程, 鱼缸(原有系统), 鱼(用户流量), 鱼网(桥接器), 新鱼缸(新系统). 既然不能继续在原有系统上继续增加代码, 那么我们该如何在重构的过程中, 让新的系统访问残留在旧系统的功能和数据呢? 答案就是渔网(桥接器)了.

选老平台与新平台的桥接器是很复杂的, 需要在所有访问流量的收束点(网关部分), 来依据规则转发流量, 这样才能做到改动最小, 覆盖所有业务. 而原有 ServerLess 平台的SDK则是个很好的桥接器, 所有的客户端都需要SDK才能跟服务端进行通信. 我们将它改造了一下, 新平台按照最小业务单元模式拆分出来的业务既读取它自身的数据库, 也通过改造后的SDK读取遗留在 ServerLess 平台上的原有数据. 同样, 原有 ServerLess 上的业务需要访问新平台上的业务, 也通过改造后的SDK来转发这部分流量到新的平台. 这样每次迁移完毕一个业务, 只要在SDK里面做出修改, 就能把流量导向新的平台了. 全部切换完毕后, 再单独找时间移除SDK, 换成原生的URI调用即可.

这里其实就是 ServerLess 平台设计上的局限之一, 自己的域名到 ServerLess 平台是 CNAME 过去的, URI自己完全无法控制. 而在自己建设的平台, 存在web网关, 再次迁移只要在WEB网关上做修改即可, 业务是完全无感知的. 而 ServerLess 平台无法操作WEB网关, 因此只能做出这种hack. 不得不说SDK是开源的给了我们迁移的机会. 要是闭源的. 只能要么做风险极高的一次性迁移, 要么在上面再适配一层自己的WEB网关才能达到同样的效果.

显而易见, SDK访问两个平台会成为性能瓶颈. 所以我直接在新平台实现了一个集中式的流量代理网关 api-proxy, 用 openresty 实现了流量转发, 并内嵌了个用 lua-resty-lrucache 实现的内部缓存, 来解决数据耦合过程耗时的问题. 这个代理网关能在 8core16G 的机器配置下实现接近80KQPS(平均请求耗时20ms, 缓存命中的情况下)的性能, 这就是我们在上面讲过的, 在性能不够的核心部分使用传统的集中化模式以提升性能.

Ghost Dubbing

数据库的切换比较复杂, 对于文章, 评论这种数据, 我们找用户在线比较少的时间直接复制数据库切换流量就可以了. 但是在复制用户数据上我们遇到了问题. 用户密码是加密的, 而且我们不知道是如何加密的 (ServerLess平台的内部功能, 具体实现未知).

如何切换这部分数据呢? 我们做了这样的设计.

未注册用户, 很简单, 直接调用迁移移植后的新用户中心接口注册就可以了.

已注册用户, 用户登录时调用 api-proxy 模拟用户登录, 用户一旦在旧平台登录成功, 证明用户的用户名和密码是合法的, 直接将用户输入的这个密码输入新平台的更新用户密码逻辑, 完成本地数据库密码的更新. 一旦检测到本地用户存在密码, 就不会在调用 api-proxy, 而是直接进入新的用户中心完成用户登录过程. 这样我们就完成了在不知道原来加密方法的情况下, 重新将用户密码加密后结果存储到新数据库中的过程.

非活跃用户, 一直调用 api-proxy 来验证用户登录是完不成迁移的, 毕竟我们最后要下线 api-proxy. 于是我们设定了一个2周的过渡期, 2周过后, 我们直接下线 api-proxy. 非活跃用户将直接进入密码重置流程, 通过发送重置链接到用户注册邮箱完成密码的更新.

部署模式和局部优化

最后我们将一些不便于灰度切换的部分统一整理了下, 准备了一个特定的时间来进行整体切换. 上线过程很简单, 我们提前发了公告, 然后在 SDK 和 api-proxy 去掉了向 ServerLess 平台的转发逻辑, 直接全部切换到新环境中. 由于测试进行的很充分, 基本没遇到适配错误. 但出现了性能问题.

切换后整体延迟显著上升, 主站甚至在高峰期出现了打不开的情况. 这就是之前说的由于拆分过细导致的性能问题了. 不过解决方案也早就准备好了. 我们服务的部署模式是单机整体部署的, 即线上每台机器都有整站的所有服务. 因此提升性能直接在公有云新开一台机器, 将服务全都部署到上面, 最后在WEB反向代理添加新增的机器就可以了.

在没有容器化平台之前, 想要迅速简单扩容, 单机整体部署是相当不错的实践. 架构足够简单. 每台机器都是幂等的. 不会出现某台机器离线导致部分业务不可用的问题. 但缺点也是显著的. 因为单机的性能是有极限的. 因此当业务非常多的时候, 就会面临单机性能不够的情况, 虽然增加机器数量可以缓解, 但如果存在CPU密集的业务, 那么这种部署模式势必会与别的业务共享主频, 造成延迟上升. 根据我们的实践, 掘金有 100+ 微服务 repo. 也就是说, 这些repo每台机微服务器上都有. 我们的测试数据极限大概在每核心 8 repo 左右. 即一台16核心的机器, 大概能承受我们分割粒度的 repo 128 个.

为了减少接口延迟, 我们还在 api-proxy 上做了一些优化. 由于服务是最小业务单元模式的. 因此业务关联数据较多的时候, 通常需要调用的底层存储接口也会较多. 这会严重影响性能. 尤其是数据有上下文依赖的情况. 用户通知就是个例子, 比如某用户接到了别人点赞他文章的通知, 那么这个通知信息要显示文章的摘要(读取文章系统), 显示点赞用户(读取用户信息), 显示点赞数量(点赞系统), 这就要调用3个接口. 为此 api-proxy 设计了一个全局的缓存. 将这些数据提前用 builder 聚合到一起, 需要数据的系统直接读取缓存, 并将其中不需要的字段过滤掉, 只保留需要的字段即可. 这是一种典型的用空间换时间的思路.

结尾

掘金的重构和切换持续了接近6个月时间, 从2016-12到2017-05结束. 重构之初有我和2个前端工程师, 2个后端工程师, 2个iOS工程师, 2个Android工程师. 其中2016-12至2017-02这几个月是并行进行的, 即同时有新业务开发和重构在进行. 17年03月份工程师流失非常严重, 前后端只剩下我和另外一个前端工程师, iOS工程师全部离职, Android 工程师感情深厚留了下来. 所以这个月有大部分时间都在招聘新工程师, 并且在03月底招聘到了3个后端工程师. 实际上, 每年发完年终奖都是工程师流失最严重的时刻, 但我们由于一些其他问题导致流失的太多了. 说起来, 我们在工程师培养上可谓硕果累累. 这些工程师在离职后都去了一线大厂. 不得不说小公司从各方面上来讲都太难了. 这也是我不得不放下骄傲, 将架构尽可能变得简单的原因.

2017-04至2017-05这两个月停止了新业务的开发, 所有时间都在重构和迁移. 在迁移的最后阶段可以说所有团队的压力都是非常大的. 产品团队处于"没有新功能就无法吸引用户新增的滑稽又尴尬的情况", 而开发团队则处于帕累托曲线的最尾部, 越是接近完成, 就会发现越多零碎的小细节在等待着还原和完善. 这种时刻就是作为领导发挥作用的时刻了. 我最后直接砍掉了一些旧有业务, 先完成切换, 然后找时间再完成这部分遗留问题. 实际最后到我离开掘金, 这部分业务也没有被要求重新实现.

我听过无数个类似 "如果当初我知道这么难我绝对不会做" 的故事. 在重构期间我自己甚至把每周的周六都无偿付出用来推进进度(公司周六正常休息), 希望能尽早切换完毕. 但切换完毕后, 评价工作成果成为了难题. 因为短时间内都不会遇到性能瓶颈了, 我也没有时间来写这样一篇大长文来从原理讲述为什么要重构, 要怎样重构, 重构的过程, 重构的收益. 也许只有在旧有架构崩溃的那一刻, 用魔法瞬间修复, 才能显现出摧枯拉朽的效果. 但魔法是不存在的, 创业公司也没时间用来抒发感慨.

强大的架构不如钱, 钱不是万能的. 因此强大的架构不是万能的(手动滑稽). 架构没有优劣之分, 只有是否适合团队和业务之分. 谨慎重构, 评估先行.

我们下一篇再见.