编辑推荐: |
本文来自于infoq,本文通过使用Semian和Toxiproxy等工具,我们让单体应用具备了更高的可靠性和弹性。
|
|
背景
Shopify是一个面向中小型企业的多渠道商务平台,帮助用户创建商店并通过线上的网店或社交媒体以及线下的POS机随时随地销售产品。Shopify为60万商家提供服务,在高峰期每秒处理8万个请求。
在为有抱负的商家提供在线商店服务的同时,Shopify还是超级碗、Kylie化妆品、Justin
Bieber和Kanye West等名人的商品销售站点。从工程的角度来看,应对这些“闪购”是个巨大挑战,因为它们的流量是不可预测的。
我是Shopify服务模式团队的一名高级工程师。我们的团队负责平台的分片、可扩展性和可靠性等方面的问题。我们为开发可扩展的软件提供指南和API,因此Shopify的其他开发者就成了我们的用户。我们的团队座右铭是“把开发人员从可扩展性中解放出来”。
Shopify的工程团队
在2015年之前,我们有一个运营和性能团队。大概就在这个时候,我们决定组建一个生产工程团队并将之前的运营团队合并在一起。新团队负责构建和维护通用基础设施,让其他产品开发团队能够正常运行他们的代码。
生产工程团队和其他产品开发团队共同负责最终的用户应用程序,保证它们能够持续运行。这意味着所有技术角色都需要关注监控和事件响应,在出现问题时提供必要的技能来恢复服务。
最初的架构
2004年,Shopify创始人兼首席执行官Tobi Lütke开始为滑雪板产品建立网店。他对市面上已有的电子商务产品不是太满意,所以决定使用Ruby
on Rails构建自己的SaaS平台。
那时,Rails的版本还不到1.0,整个框架还只是通过电子邮件发送的.zip存档文件。Tobi加入了Rails作者David
Heinemeier Hansson(DHH)的行列,成为Ruby on Rails的贡献者,同时一边开发Shopify。Shopify现在是世界上最大最古老的Rails应用程序之一。它从未被重写过,仍然使用原始代码库,不过在过去十年中已经成熟了很多。Tobi当初提交代码的历史记录依然保留在版本控制系统中。
押注Rails极大地影响了我们的思考方式,同时也让我们能够快速交付产品。虽然框架的某些部分难以扩展(例如ActiveRecord回调和代码结构),但大多数人仍然赞同Tobi的观点——能够让Shopify从车库创业公司变身上市公司的非Rails莫属。
Shopify的核心应用仍然是Rails单体,除此之外,我们还有数百个其他Rails应用。它们不是微服务,而是特定于领域的应用程序:物流(与各种物流供应商交互)、身份(Shopify网站的单点登录)和App
Store等等。管理百来个应用程序并让它们保持安全更新是一项非常艰巨的任务,因此我们开发了ServicesDB。ServicesDB是一个内部应用程序,可用于跟踪所有的生产服务,并帮助开发人员确保他们不会遗漏任何重要的内容。
ServicesDB为每个应用程序维护了一份检查清单:所有权、运行时间、日志、轮班待命、异常报告和gem安全更新。如果其中任何一个出现问题,ServicesDB就会在GitHub上提交一个问题,并通知应用程序的所有者来解决问题。我们还可以通过ServicesDB了解如下一些问题:“有多少个应用程序使用了Rails
4.2?有多少个应用程序使用了过时的gem版本?哪些应用程序正在调用这个服务?
当前的技术栈
从一开始,我们就使用MySQL作为关系数据库,使用memcached作为键值存储,使用Redis作为队列和后台作业存储。
2014年,我们无法再将所有数据都保存在单个MySQL实例中,即使升级更好的硬件也无济于事。我们决定使用分片,将所有数据分成几十个数据库分片。分片对我们来说很管用,因为Shopify的商家是彼此隔离的,所以我们可以将一部分商家放在一个分片中。如果商家之间需要共享数据,那就麻烦了,好在我们的业务没有这样的需求。
分片为我们解决了数据库容量问题,但很快我们就发现,我们的基础设施中存在单点故障问题。所有这些分片仍在使用单个Redis。有一次,Redis发生中断,导致整个Shopify瘫痪,我们后来称之为“Redismageddon(Redis世界末日)”。这次事故给我们上了重要的一课,就是要避免让所有的Shopify服务共享单个资源。
多年来,我们从分片转向了“pod”的概念。pod是一个完全独立的Shopify服务实例,它拥有自己的数据存储,如MySQL、Redis、memcached。pod实例可以在任何区域生成。这种方法帮我们消除了全局的中断事故。截止到现在,我们拥有超过一百个pod,并且从转向新架构以来,没有哪个重大的中断会影响到整个Shopify。现在的中断只会影响单个pod或区域。
当我们发展到数百个分片和pod,很显然,我们需要一个编排部署解决方案。今天,我们使用Docker、Kubernetes和Google
Kubernetes Engine为新的pod分配资源。在负载均衡器方面,我们使用了Nginx、Lua和OpenResty,我们可以通过脚本来编写负载均衡器。
Shopify Admin的客户端经历了一个漫长的旅程。我们最开始使用的是HTML模板、jQuery和prototype.js,并于2013年将其迁移到Batman.js,Batman是我们自己开发的单页面应用程序框架(SPA)。然后,经过重新评估,我们又回到了静态HTML和纯JavaScript。随着前端生态系统的不断成熟,我们觉得是时候重新思考新的解决方案。去年,我们开始将Shopify
Admin迁移到React和TypeScript。
从使用jQuery和Batman以来,很多事情都发生了变化。JavaScript的运行速度越来越快,我们可以轻松地在服务器上渲染应用程序,从而减少客户端的工作量。React的资源和开发工具比Batman要好很多。另一个非常显著的区别是,我们现在有一个更好的解决方案可以确保业务逻辑不会泄漏到客户端——GraphQL。Admin成为了另一个GraphQL客户端,遵循与移动应用相同的模式:没有数据持久化,不需要服务器处理需要在客户端之间共享的内容,以及极其高效的资源获取。
我们如何构建、测试和部署
Shopify单体大概有10万个单元测试,其中大部分涉及到繁重的ORM调用,因此运行得不是很快。为了保持快速的交付管道,我们在CI基础设施上进行了大量投入。我们使用BuildKite作为CI平台。BuildKite的独特之处在于,它负责编排构建并提供用户界面,而用户可以在自己的硬件上按照自己的方式运行测试。
我们使用数百个并行的CI工作线程来运行这10万个测试,耗时15-20分钟。并行测试加快了我们的交付过程,否则,单个构建可能需要数天时间。我们的数百名开发人员每天都要交付新的功能和改进,因此我们必须保持快速的持续集成管道。如果构建结果为绿色,就可以将变更部署到生产环境中。我们没有使用staging或金丝雀部署,在出现问题时我们主要依赖功能开关和快速回滚。
ShipIt是我们的部署工具,是Shopify持续交付的核心。ShipIt是一个编配器,负责运行和跟踪项目的部署脚本。它支持直接部署到Rubygems、Pip、Heroku和Capistrano。对我们来说,主要是kubernetes-deploy或Capistrano(用于遗留项目)。
我们使用稍微调整过的GitHub流程,功能开发在分支上进行,主分支作为生产发布的真实来源。PR准备就绪后,将其添加到ShipIt的合并队列中。合并队列的作用是控制代码合并到主分支的速度。在繁忙时段,会有很多开发人员想要合并PR,但我们又不希望同时往系统中引入太多变更。合并队列确保一次部署只包含5-10次代码提交,这样可以更轻松地识别问题,在部署后如果出现问题可以及时回滚。
我们开发了一个浏览器插件,让合并队列与GitHub上的合并按钮很好地结合在一起:
ShipIt和kubernetes-deploy都是开源的,很多公司也采用了我们的流程并且取得了成功。
后续的挑战
在设计Shopify的每一个系统时都必须考虑到伸缩性,同时还要让人感觉是在开发经典的Rails应用程序。我们为此付出了令人难以置信的工作量。对于进行数据库迁移的开发人员来说,整个过程看起来就像迁移其他Rails应用程序一样,但实际上,系统将迁移作业异步应用到100多个数据库分片上,停机时间为零。我们的基础设施的其他方面也是类似,从CI、测试到部署。
在生产工程方面,我们付出了很多努力将基础设施迁移到Kubernetes。我们不得不对一些方法和设计决策做出评估,因为它们还没有为云环境做好准备。与此同时,我们在Kubernetes上的投入开始获得回报。因为之前我花了好几天写Chef手册,现在只需要再对Kubernetes的YAML配置文件做一些修改就可以了。我希望我们的Kubernetes基金会能够日趋成熟,为我们提供更多扩展的可能性。
通过使用Semian和Toxiproxy等工具,我们让单体应用具备了更高的可靠性和弹性。与此同时,我们还管理着公司的其他一百个服务——其中大多数使用了Rails。我们借助ServicesDB来确保它们都使用了与单体相同的模式,让我们从大规模运行Rails应用程序中学到的经验教训发挥作用。
这些服务之间还需要以某种方式发生交互,至于使用何种方式由它们自行决定。有些服务使用了Kafka,有些则使用基于HTTP的REST
API。最近,我们一直在寻找适用于整个Shopify的RPC和服务网格解决方案。我希望在接下来的一年中,应用程序能够以具备弹性和可扩展性的方式进行通信。
|