尘世间的上帝之国

View Original

我的2019技术之路小结

时间飞逝,19年即将进入尾声,子曰:温故而知新,作为一个在技术之路上孜孜不倦地奋斗的一线软件工程师,今天就来回顾总结下19年的技术之路,以作承上启下之用。

首先向不甚知情的读者简单介绍下笔者现在的工作:我目前就职于纽约的成长型创业公司Squarespace,公司的主营业务是零代码自主建站(个人网站或电商网站),域名,邮件营销工具。同行业的可参考对比的公司包括Wix(6B), Shopify(45B), Godaddy(11B), MailChimp(5B估值)。我主要在做电商网站建站那块业务,上至前端的购物车应用、结算应用、商家产品管理、订单管理,下至后端的订单系统和支付系统,都有所涉及。在小公司工作的好处之一就是粥多僧少,只要肯干活有的是项目,能学到的东西自然也就比较多。

19年做的项目基本都是后端的项目,最主要的两个大项目是 1)构建第二代订单系统以提高系统容错性 2)全链路实现Stripe SCA来迎合欧洲新出的关于支付两步验证的法规。

构建第二代订单系统

在Google Search工作时,我有个印象是比较核心的infra系统基本每过个两三年都要重做一次,因为数据量增长实在太大或者是老系统的性能出现明显业务瓶颈。现在业界很火的一个话题是微服务架构(Microservices),采用这种系统架构的好处很明显,在一个几百个人的工程团队里分工明确,责任明确,开发测试部署效率都比在单体应用时代有了显著的提升(毕竟能做到像Google那样所有人都在google3一个monorepo里写代码并快速部署子系统迭代的公司寥寥无几,对infra的要求太高,小公司没能力也没精力去做)。但微服务并非万能钥匙,凡事有利必有弊,其中一个大弊端就是整体系统稳定性降低,如果没有做好容错的话,很容易出现单点故障导致线上事故。

在单体应用时代,假设我们只有一个系统X,X的稳定性是99.99%, 也就是说10000分钟里最多宕机1分钟。但在微服务架构下,我们把X拆成了三个各自分工相对松耦合的微服务A, B, C, 三个子系统的稳定性假设也是99.99%, 但从整体来看系统的稳定性变成了 99.99% * 99.99% * 99.99% = 99.97%!这里还有个隐藏假设是该系统的功能依赖A, B, C三个系统同时可用。

现在我们来把A, B, C具象化下,场景为电商网站的订单提交请求处理。 A为库存更新,B为支付,C为创建订单,依次进行,A和B必须同时成功完成C才能开始。A、B、C很自然地可以用Domain Driven Design的设计思路用三个微服务来实现其API,并在上游调用系统S中调用S -> (A ->B -> C)。这时假设B支付服务出故障了,原因是依赖的外部支付系统Stripe出故障了,那段时间的所有订单都不能创建,而且已经完成的库存更新必须得撤销(Rollback)来保证一致性。注意A、B、C为三个独立系统,可由不同的编程语言和不同的数据库来实现,无法通过底层关系型数据库的事务性(transaction)来实现强一致性(Strong consistency)。假设C订单服务出故障了,原因是底层的数据库数据遇到性能瓶颈开始挂断连接,理论上S可以重试几次,但重试只能解决偶然性的系统问题,不能解决持续性的系统问题,毕竟用户在前端页面等着呢,你不能让他们等个十分钟,估计好多人都或刷新或放弃关闭页面了。按照之前的逻辑,S此时要撤销B的支付和A的库存更新,但撤销一个完成的支付说易行难,尤其是其实现是使用外部支付系统,比如Stripe,PayPal,WorldPay等,中间会有不同的手续费产生。所以实际操作里会发现如果C出故障了,整个订单请求被挂断,但是用户的钱却被扣了!这时平台或者商家不得不手动联系买家处理这些没有订单的支付记录。

以上简述就是我们上一代的订单系统碰到的诸多头疼的问题之一。因为这些问题,在过去两年我们平台发生了大大小小的事故不下十次,每次都是又费人力又费财力,搞得所有人(工程师、产品经理、director甚至是SVP)焦头烂额。

从18年底开始,我们开始立项设计新一代的订单系统以期解决之前碰到的那些问题。上文也提到了,在一个异构的分布式微服务架构系统中,强一致性基本无望,只能退而求其次寻求最终一致性(Eventual consistency)。继续使用上文的例子,当订单服务C出故障时,我们挂起整个请求,等到C恢复时,我们恢复那个被挂起的请求重试C,此时订单最终能被创建成功,买家和卖家收到邮件确认订单创建成功。有心点的朋友也许会问,万一是B支付服务出故障呢,你如果重试多次不会导致买家被多次扣款么?确实会,不仅是B,其中的任何服务都会遇到这个问题,库存可以被更新两次,订单可以被创建两个。我们使用了幂等性(idempotency)来解决这个问题。所谓的幂等性,就是说一个操作被调用N次的结果和被调用1次的结果是相同的,用简单的数学表述为 f(x)= f(x)f(x)...f(x)。在这个最终一致性链上的所有操作必须实现幂等性。

此处我略过了很多设计细节,只是想粗略地提到一些关键的想法来帮助读者理解问题和解决思路。最终的系统实现就是一个高度定制化的、高可用性的、最终一致性的工作流引擎系统。新系统至少经历了三次Stripe大规模宕机,若干次PayPal的间歇性抽风(PS: PayPal的延时实在太感人), 还有若干次内部的数据库宕机和上下游服务错误。从19年六月份上线到现在差不多半年时间里,有超过一万个订单如果没有这个新系统就出问题或流失了,直接的GMV贡献可达$500K。

全链路实现Stripe SCA

SCA全称Strong Customer Authentication, 是欧盟19年新出的法规,要求欧洲的银行对高风险的支付请求进行两步验证(2FA), 比如短信验证或者保密问题验证。初衷当然是好的,想减少在线交易欺诈付款,比如盗用信用卡,只是这法规推行的过于仓促,要求欧洲所有银行和有在线支付处理功能的平台必须改进他们的系统来做到SCA合规,截止时间是2019年9月14日。

对于Squarespace来说,因为是美国的公司,不在该法规管束范围之内。但是我们有不少位于欧洲的用户,他们的网站必须要做到SCA合规, 作为SaaS平台自然也要提供这一服务来帮助我们的用户轻松做到SCA合规。然而由于前期的项目规划不当,这个项目在上半年根本没被提上日程,等大伙意识到火烧眉毛时已经是七月中旬,此时留给我们的时间只有不到两个月了。没办法,只能硬着头皮上呗。

要实现全平台的SCA, 必须在所有涉及买家支付信息的应用处切换到Stripe为SCA新设计的API,包括:

  • 在个人账户里添加新信用卡

  • 在线支付时使用新信用卡

  • 在线支付时使用新信用卡并保存到账户

  • 在线支付时使用之前账户里保存的信用卡

  • 订阅订单(subscription order)离线(off session)处理账户保存的信用卡

一旦涉及两步验证,其中有一环节至少是异步的,也就是买家在客户端验证他们身份结束后的事件。Stripe做了一层很好的封装,无论是异步事件驱动的订单系统还是经典的同步系统,都能比较容易地使用封装好的API。这里面最麻烦的一点是验证结束后的请求处理,忽略具体技术细节不讲,此时的后端系统有可能是有状态的,可以把验证前后两次请求看成一个会话,处理验证后的请求是需要获取初次请求时的部分数据和状态。

Stripe推出这套新API也是很仓促,包括支持的支付方式、技术文档、测试卡的使用说明等。当然也能理解,毕竟他们有更多的客户等着他们完成新的API来完成集成。

最后我们有惊无险地在截止日期前一天上线了SCA合规的订单系统和支付系统,皆大欢喜。上线后几天陆陆续续发现一些小bug,也都逐一修复。不过这一番快马加鞭,难免在代码层面留下不少技术债(tech debt), 甚至在用户体验上也有部分赶工瑕疵。这反而是最考验一个工程团队的环节。古语云”行百里者半九十“,上线对于一个项目来说仅仅是百里中的九十,剩下的十里要不要走、什么时候走、如何走都是值得每个追求卓越的工程团队该解决的问题。

话说回来,在我写这篇小结时我又看了眼Stripe的文档,欧盟已经将法规正式约束的日期推迟到2020年年底,为什么呢?原来欧洲没几个银行能顺利赶上原先那个截止日期,欧盟也只好妥协延迟🤷‍♀️

上面两个项目的成功上线给我带来了不少的个人成长,不仅是做系统、写代码、画大饼,还有团队的沟通、协调、人员调配,找到自己能发挥最大影响力的地方,然后踏踏实实地努力。最有成就感的时候莫过于听说公司估值翻倍的时候——虽然一路走来也没有经历过小型的初创公司那种没日没夜赶工的日子,但我知道这依然快速的成长背后,有我的一份努力。


See this gallery in the original post