Strikingly 团队2017技术展望

为了应对多客户端(Web, iOS, Android)的挑战,2016 年我们在团队层面和技术栈上做了很大胆的尝试:我们把前端团队和移动端合并了,组成了客户端团队。这个团队采用同一套基于 React 体系的技术栈去构建 Web、iOS 和 Android 应用,可以跨端复用很多业务逻辑代码。这次变革让我们体会到了统一技术栈在开发效率、团队协作和知识共享等方面带来的提升。

实际合并的过程是2016年5月开始的,在此之前我们做了很久的铺垫准备。整个过程分为三个阶段:

第一阶段:React Web

熟悉我们团队的人应该知道我们在过去两年累积了很多使用 React 开发大型 Web 应用的经验。我们的 Web 应用是一个建站平台(上线了,上线了sxl.cn | 简单易用・专业美观http://www.sxl.cn), 整个网站编辑器就是一个交互非常丰富的大型单页应用 (Single Page Application)。React 以及其社区的几个核心思想:组件化、单向数据流、纯函数 UI、不可变数据,大大简化了构建这种大型 Web 应用的过程。我们也想把这种开发体验通过 React Native 带到移动端开发。于是在16年3月份,我们进入了第二阶段。

第二阶段:React Native

在16年5月,我们开始使用 React Native 构建“上线了” iOS 应用。这款应用可以让用户直接通过手机建立网站,管理商城订单和留言,用户不再需要电脑进行这些操作。在这个应用的开发过程中,我们把 native 和 web 可以共用的代码,包括业务逻辑和通用的工具辅助类代码从原来的 React Web 代码里抽出来,放到 common 目录下方便 native 和 web 复用。整个项目前后花了3个月左右就把 iOS 和安卓应用写完了。iOS 和 Android 代码重用达到了90%,其中也包括不少 Web 端也可以共用的代码。

第三阶段:跨平台开发

基于前两个阶段的铺垫,我们有了比较深厚的 React 开发经验,也写了很多跨平台的业务逻辑。这些业务代码只要接上不同的视图层就可以开发出在不同平台上的应用了:对于 Web 应用,视图层就是 React.js,对于移动端应用就是 React Native。两者的开发体验非常类似的,我们甚至可以让同一位开发者去开发 Web,iOS 和安卓的应用。

因此,第三阶段我们从团队结构上重新进行了分配。之前,对于每一个功能模块(feature),在不同平台上都需要不同的开发者去完成。

而现在,我们可以根据功能模块去分配。一位全端开发者独立把 Web、iOS 和安卓平台都做出来。

三个阶段总共用了两年时间。从最初听到 Facebook 的工程师畅想着通过 React 可以开发不同平台应用到现在我们团队已经实现了这个目标,我很庆幸这是客户端工程师一个美好的时代。17年,我们将基于这个团队结构和技术架构,更快速的做出新功能模块!

(我司 CTO 在 JSConf 2016 做了一次关于全端团队搭建过程的分享,有兴趣的可以在这看视频

服务端架构的思考

GraphQL

在构建大型前端应用时,客户端和后端工程师通过 API 的方式进行合作,API 就是双方通信的协议。现在主流的 API 设计范式是 RESTful API,然而在实践中,我们发现 RESTful 在一些真实业务逻辑的需求下不是很适用。为此我们往往需要构建自定义 API 节点,而这违背了 RESTful 的设计理念。

我们认为 Facebook 的 GraphQL 是目前最接近完美的解决方法。服务端只需要定义好业务逻辑中设计的数据类型系统,客户端工程师就可以使用 GraphQL 自定义查询的数据及其结构,大大地提升了 API 的灵活性。关于 GraphQL 可以具体解决哪些问题,可以读一下我司 CTO 前年在 CSDN 上发布的文章,在这就不仔细阐述了。

自2015年7月推出 GraphQL 规范到今天,除了 Facebook 之外社区里不乏有些先锋开始陆续使用起来。其中知名的包括有 GitHub,Pinterest,Coursera 和其他。Meteor 这家公司也是先锋中的其中一员。Meteor 作为一家在 Web 应用领域一直有一些超前做法的公司,在2016年全力投入了大量资源打造基于 GraphQL 生态系统的 Apollo。Facebook 本身也投入了一整个团队把内部的旧 GraphQL 系统升级到对标社区规范的新系统,并发布了几个在实战中使用到的工具。

鉴于 GraphQL 目前在社区生态上已经比较完善了,2017年我们将开始使用 GraphQL 渐渐替换掉内部已有的 RESTful API。

构建复杂的 Rails 应用

Strikingly 服务端代码主要是基于 Ruby on Rails 开发的。Rails 的设计哲学是 おまかせ(Omakase),它提倡和遵循“惯例重于配置”(Convention over configuration)的理念,并提供了丰富的工具链,让 Web 开发人员非常容易上手。Rails 的惯例大多是 Web 开发领域多年总结下来的最佳实践,即使是新手,也能够在短时间内开发出安全,健壮的 Web 应用,这个对于初创企业来说是非常有帮助的。

但是当应用的逻辑开始变得复杂的时候,Rails 就开始显得力不从心了,它所提供的惯例和最佳实践没有办法再很好地指导开发人员写出具备高可维护性的代码。

要解决这问题,我们需要重新审视 Rails 在 Web 应用开发中的定位。Rails 只是一个 Web 框架,它不是一个应用开发框架,不能也不应该负责 Web 应用中领域相关的部分。我们不妨从 Java 社区借鉴 POJO (Plain Old Java Object) ,引入 PORO (Plain Old Ruby Object) 的概念。

在我们总结的设计模式中,一个 PORO 对象就是一个普通的 Ruby 对象,它的 initialize 方法除了提供其他 PORO 对象的依赖注入(DI,Dependency Injection)之外不包含任何参数,对象本身也不保留任何状态。在应用中,一个 PORO 对象通过工厂类产生,工厂类负责完成依赖注入,在这个过程中,可能需要调用其他 PORO 的工厂类来产生对象。

为了解决 Strikingly/上线了应用中不同场景下的不同问题,我们使用了以下 5 类 PORO 对象:

  • Service 对象
  • Form 对象
  • Policy 对象
  • Query 对象
  • Adapter 对象

Service 对象的功能是描述领域相关逻辑中的操作或过程。比如一个典型的 Web 应用中支持用户注册、登录、登出等操作,有些应用可以支持用户付费升级和取消套餐等等,这些操作都是适合用 Service 对象来描述的。

Form 对象提供了介于用户界面上的表单和 Model 定义之间的一层封装。Rails 本身提供了简单易用的表单,但是 Rails 的表单跟相应的 Model 之间有非常强的耦合性,这样等于说把应用的 Model 层实现细节直接暴露给了用户,非常不灵活。Form 对象替代了真正的 Model 层来作为表单的 Model 层,把用户输入转换成真正的 Model 对象。

Policy 对象和 Query 对象相对比较简单,它们分别定义了封装权限检测逻辑和数据查询逻辑的对象。

Adapter 对象提供了介于应用内部领域相关接口和应用外部依赖接口之间的一层封装。应用内部领域相关的逻辑应该有自己固定的接口并不与外部依赖接口之间产生强耦合关系。比如 Strikingly 提供域名的购买和管理服务,这个服务提供了域名查询、购买、验证、续费、取消等操作,这些操作都是域名这个领域内的“标准操作”,并不依赖于我们的上级域名提供商。我们应该允许上级域名提供商修改它们的API,允许切换到另一个上级域名提供商,甚至支持多个上级域名提供商。在有 Adapter 这一层封装之后,我们可以做以上这些更改而不需要更改任何域名领域内的操作代码,需要修改的仅仅是 Adapter 对象的代码而已。

今后随着系统的复杂性进一步增加,我们可能会使用更多的 PORO 对象类型来解决新的问题。我们相信,通过这种方式,可以有效地降低我们系统内部模块之间的耦合性,使得代码的可维护性大大增强,也更方便我们编写高效的测试代码。

关于这一部分的详细内容可以参考我们团队的资深 Rails 工程师 Florian Dutey 在 RubyConf Taiwan 2016 上的演讲 “Large scale Rails applications”。

微服务架构的演进方向

PORO 对象和依赖注入可以很大程度解决单个应用中业务复杂性造成的可维护性问题,但是应用规模、复杂度和用户数目的增加还带来了其他问题,对于这些问题,我们必须从系统整体架构上做调整来解决。

首当其冲的就是用户网站的高可用性。Strikingly 建站产品的特性决定了用户会对自己的网站有较高的可用性要求,对网站的管理平台和编辑器的可用性则没有很高的要求。这就需要从系统层面做切分,对用户网站所依赖的功能提供不同于管理平台和编辑器的可用性保证SLA(Service Level Agreement)。

其次,应用中的每个模块都有自己独特的流量特性,而单一应用则决定了比较单一的技术栈和通信、数据存储等方面服务的选择。例如用户网站的流量特性就跟管理平台和编辑器的流量特性完全不一样,每个用户网站的流量特性也不一样。即使在用户网站中,不同的功能模块因为功能的区别和使用率的不同,流量特性也不完全一样。如果能够对于每个模块采用各自最适合的技术方案,就可以最大化模块的性能。

再者,单体应用导致所有大大小小的改动都必须重新部署整个代码库,而为了保证新代码的正确性,部署之前需要对整个项目的前端和后端代码进行自动化测试,整个流程持续时间很长。每个小的改动都重新部署整个代码库代价太大,而如果降低部署的频率的话又会在一定程度上影响迭代效率。

解决这些问题的有效方案是将目前的单体应用合理地拆分成为多个微服务。例如,所有用户网站相关的服务都会被拆分出来作为微服务,并且每一类网站模块所依赖的服务都会成为单独的微服务。这样可以保证用户网站整体的高可用性,可以允许因为后台维护的原因出现某个服务短时间不可用,极大地降低网站整体上服务不可用的情况的出现。在微服务架构下我们可以为不同的服务选择不同的技术方案,并且各个服务可以分开部署。由于每个服务都非常小,测试和部署需要花费的时间非常短,可以极大地提升迭代效率。

向微服务架构演进是一项非常复杂的系统性工程,这也是为什么我们从几年前就开始思考引入微服务架构的可能性,但是因为当时的痛点还不大,并没有付诸实施。目前我们正在做关于微服务的元研究(meta research),也就是在研究为了迁移到微服务架构,有哪些具体技术方向的研究需要完成:

  • 服务发现
  • 通讯协议
  • 生命周期管理
  • 运行状况监控
  • 日志管理
  • 服务部署
  • 服务API设计最佳实践
  • 服务迁移过程
  • 数据管理和集成
  • 本地开发

如下图所示:

这张图是目前总结下来的微服务领域我们需要研究的具体技术方向,随着实践的深入可能会添加新的内容。2017年我们把微服务架构作为一个主要目标,并不是在这一年中需要完成微服务架构的迁移,而是在这一年中把大方向和一些重要的细节确定下来,完成必要的技术储备,并且完成几个最重要的微服务的迁移;而整个架构的迁移完成可能会需要持续2~3年的时间。我们在这个领域仍然比较缺乏经验,希望能够借此机会跟领域内有丰富经验的工程师多交流,更希望能够在这一年中找到能一起在未来2~3年内一起完成这个大工程的人才。

可靠的基础设施

Strikingly 最初是部署在 PaaS 平台 Heroku 上的,Heroku 负责分配和管理下层基础设施,我们只需要关注在应用本身。2014年我们从 Heroku 迁移出来。当时我们引入了刚刚兴起的 Docker 容器技术作为应用的打包部署方案,应该说是比较前卫的,但是在基础设施的管理上,包括 EC2,VPC,Subnet 的分配和设置等等,还是采用手动管理的方式,在计算资源需要伸缩的时候也是通过手动方式进行操作。这样的管理方式带来了不少问题。

首先,手动操作容易造成操作错误,尤其是在维护正在运行应用的基础设施的过程中,如果不小心关掉了某台服务器或者设置网络的时候规则设置错误,都可能造成服务中断,影响用户使用。

其次,手动操作效率比较低。我们除了生产环境之外,还有多个沙盒环境供线上测试使用。为了保证测试的有效性,这些沙盒环境都要做到尽量跟生产环境一致。对于一个运维工程师来说,手动创建完成并测试通过一个沙盒环境往往需要2~3天的时间,并且无法完全保证这个沙盒环境和生产环境的一致性。

再次,生产环境和沙盒环境的当前状态非常不透明,即使使用文档记录了环境创建的操作步骤和当前的状态,也很难保证文档和环境之间一直保持同步。这样我们就无法有效地了解当前的环境是否符合我们的预期,很可能会出现考虑不周全的情况。

为了解决这些问题,我们需要更有效的方式来管理我们的基础设施。

基础设施即代码(Infrastructure as Code)

我们选择的解决问题的方向是基础设施即代码(Infrastructure as Code)。所谓基础设计即代码,就是指开发和运维工程师不再使用手动方式或者过程式的脚本来分配管理基础设施,而是采用声明式的代码来定义基础设施,这里的代码既可以作为定义基础设施的文档,也可以运行来完成基础设施的配置,还可以作为自动化测试的spec来测试当前的基础设施配置是否符合预期。

基础设施即代码提供一种全新的方式来看待云计算时代的运维工作。传统的IDC时代,很多运维工作需要通过手工的方式来完成,手工操作的缺点在上文已经提过了。有一些自动化意识比较强的公司和个人,会采用过程式的脚本来自动化大部分的运维工作,确实减少了手工操作带来错误的可能性以及带来了效率的提升。但是复杂的脚本同样带来了不透明的问题:你怎么确定这个脚本运行的结果是符合预期的?如何测试脚本的正确性?如何保证脚本运行的幂等性?

基础设施即代码通过声明式的配置代码解决了这些问题。这些配置定义了我们所期望的状态,而运行这些配置的过程,则是不断地检测特定的计算资源是否符合定义,如果不符合,则通过调用云平台的API来操作使得该计算资源符合定义。

我们最初选择了 Ansible 作为配置基础设施和自动化部署的解决方案,基于 Ansible 设计和实现了一整套运维工具。这套工具帮助我们实现了两个重要的目标:

  • 任何一个工程师都能够简单地使用这套工具来部署/回滚,而不需要了解任何底层的实现细节
  • 可以高效地复制一套新的生产/沙盒环境而不需要太多的手动操作

第二点在我们准备在 AWS 中国区域推出“上线了”服务的时候起到了很大的作用,我们几乎一键完成了整个生产环境和几个沙盒环境的基础设施配置,并且照搬了整个应用部署/回滚的方案,节约了大量的时间。

随着系统复杂度的增加,我们渐渐发现 Ansible 虽然在实现自动化部署方面很好用,但在定义和配置基础设施上并不那么方便,不能完全解决上面提到的3个问题。经过研究和比较,我们选择了 Terraform 作为新的基础设施配置解决方案,并使用 Terraform 完全重写了所有的配置代码。现在我们可以在任何时候重复运行这些配置代码来把基础设施更新到最新定义,并且使用这些配置代码很快地创建新的沙盒环境来满足多个产品团队并行测试的需求。

容器的编排和集群管理

基础设施即代码很好地解决了基础设施的管理问题,但是并无法解决基础设施之上的服务的生命周期管理问题。在确定微服务架构的演进方向之前,我们已经做了一些分布式计算的尝试,即将一部分相对独立的模块拆分出来作为独立的服务存在。我们引入了 Docker 容器技术来管理服务的打包和部署,每个独立服务各自分配和管理自己的基础设施和计算资源。

这种方案决定了系统整体的基础设施复杂度将随着服务数目的增加而线性增长。对于每个独立的服务,我们都需要单独的配置文件来定义它的基础设施,并且需要对这些基础设计进行维护和监控。同时,我们对于每个服务预留了一定流量爆发性增长的弹性空间,但不同的服务之间无法共享这些预留的弹性计算资源,也造成了一定的浪费。

未来演进到微服务架构会需要我们有能力管理更多更细粒度的服务,而目前通过管理基础设施来管理服务的方式将面临非常大的局限性。我们需要一个方案来解耦基础设施和服务之间的直接关联。

具体来说,我们不仅仅需要容器来封装服务和它的运行环境,我们更需要一个容器调度、编排和集群管理方案,可以帮助我们管理下层的基础设施和计算资源,并作为资源池的形式提供给上层的服务容器消费。服务容器并不关心下层基础设施的管理和分配,基础设施也并不随着上层服务数目的变化而变化,只要基础设施提供了足够的计算资源给上层服务使用就可以。同时,这个方案中需要包含一套命令行工具甚至 Web 界面来让工程师更方便地创建和管理服务生命周期。简而言之,我们需要搭建一个简单易用的内部 PaaS 平台。

目前容器领域内比较成熟的容器调度编排方案有以下几个:

  • Docker 公司提供的 Docker Swarm
  • Google 开源的 Kubernetes
  • AWS 提供的 ECS 和 Blox Toolbox

这些主流的容器调度编排方案都支持 Docker 的容器标准,目前我们正在测试和评估这些不同的方案,预计在2017年完成 PaaS 平台的设计和实现。

自动化回归测试

为了在快节奏迭代部署的同时保证产品可用性的稳定,2016 年我们搭建了一套完整的自动化测试方案:从单元测试、集成测试、功能回归测试到 UI 回归测试。

我们 CI/CD 的流程是这样的:

单元测试和集成测试将会在 CI Tests 时跑。功能回归和 UI 回归测试就需要等待代码部署到沙盒环境后才会在 E2E (End-to-End) Tests 环节跑。在此之前,我们在单元测试和集成测试上已经下了很大工夫了,2016 年重点解决的问题是搭建功能回归和 UI 回归测试。

功能回归测试

功能回归测试我们选用了来自蚂蚁金服的一套基于 WebDriver 协议的测试框架 - Macaca.js。WebDriver 协议对各种设备的交互已经制定了 API 规范,Macaca.js 也严格遵守了这套协议并为 Web、iOS 和 Android 都实现了对应的驱动。这就意味着我们的 QA 工程师只需要学习 Macaca.js 就可以测试 Web,iOS,Android 的应用。

除了使用代码做自动化的功能回归测试,我们也用了 RainforestQA 众测(crowd testing)方案。在这个平台上,我们的 QA 工程师和产品团队只需要写好测试用例的步骤(steps),平台会自动分发给有测试经验的测试人员进行手动测试。

步骤定义:

步骤定义:

RainforestQA 是一个24小时都在运行的服务。相比于团队内部成员手动测试,这个方案平均把测试所需的时间缩短到原来的二十到三十分之一。也就是说,之前一位 QA 团队成员需要一天才能完成的测试,使用 RainforestQA 就可以在一小时内完成。

UI 回归测试

对于一款建站工具,在快速迭代的过程中,保证用户通过我们工具做出来的网站 UI 一致也是很重要的需求。我们采用了 UI 截图比对回归测试。在部署到沙盒环境上后,我们会做一些截图然后和上一次的截图(base image)做比对并高亮出两图之间的差别,只要截图有偏差就会报错并通知工程师和 QA 工程师进行排错。

在过去一年中,我们搭建了一套完整的从代码到部署的自动化流程,并完成了了一部分的功能回归测试用例。2017 年我们的目标是基于这套流程达到 90% 的功能回归测试覆盖率。

总结

总结一下,我们希望在2017年实现以下目标:

  • 借助一致的技术栈极大提升多客户端团队的产品开发效率,做出更多更好的产品特性
  • 引入 GraphQL 重构 API 层,提供更丰富灵活的 API
  • 遵循 PORO 和依赖注入的思路,充分解耦后端产品代码的复杂度
  • 向微服务方向转型,提供更高的系统可用性,优化系统的自动伸缩能力,提升开发效率
  • 基于已有的自动化测试流程,加入更多的测试用例,使得功能回归测试达到 90% 的覆盖率
  • 构建基于容器编排的内部 PaaS 平台,简化基础设施的管理复杂度以及应用服务的管理

Strikingly 崇尚用技术解决问题,以技术驱动产品,我们坚信,一个伟大的产品背后,一定有坚实的技术基石。这些技术目标的实现离不开一个自由,创新,充满黑客精神的技术团队。希望在2017年有更多的小伙伴能够加入我们,一起实现这些目标。欢迎大家加入和我们一起努力:Strikingly 2017 技术团队招聘

如果你想体验上线了,可以在腾讯云市场搜索上线了。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

上线了的专栏

1 篇文章1 人订阅

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏BestSDK

十大技巧快速提升原生APP开发性能

编辑导语 移动应用市场用户争夺战日益激烈,原来做APP拼想法拼创意拼是否抓住用户痛点。现在,精细化用户体验成为了一个APP能否留存用户的关键问题,一旦用户觉得体...

2289
来自专栏技术分享

微服务架构—自动化测试全链路设计

从 SOA 架构到现在大行其道的微服务架构,系统越拆越小,整体架构的复杂度也是直线上升,我们一直老生常谈的微服务架构下的技术难点及解决方案也日渐成熟(包括典型的...

1220
来自专栏ThoughtWorks

如约而至|2018年5月期技术雷达正式发布!

ThoughtWorks每年都会出品两期技术雷达,这是一份关于技术趋势的报告,由 ThoughtWorks 技术战略委员会(TAB)经由多番正式讨论给出,它以独...

881
来自专栏服务端思维

非功能性需求,不要成为项目的坑

顾名思义,用户在使用过程中易理解,易操作等。页面描述要清晰,最高境界就是“零手册”,不能让用户理解有歧义,此外,一致性也是很重要的,比如输入内容校验规则,页面展...

1043
来自专栏媒矿工厂

优化延迟的最佳视频传输方案(一)

流媒体服务逐渐成为全球媒体和娱乐业务的核心,根据目前市场的数据,由于增长率是传统电视的10倍,OTT视频已经占到了行业总收入的15%,预计到2022年将占据市场...

873
来自专栏较真的前端

Google I/O 2018 : Web 现状综述

1794
来自专栏程序你好

个人门户系统设计方案

1344
来自专栏杨建荣的学习笔记

运维开发流程梳理和思考

记得之前梳理过一个运维开发流程,也做了一些实践,从我的认识和理解来看,其实这更适合一个团队内的协作。

1353
来自专栏java一日一条

Java 程序员不容错过的开发趋势

当涉及到代码时,有很多热门话题,并且与时俱进总是潮流所向。如果你想知道如何分离糟粕和精华,那么我们已经准备就绪,只欠各位阅读下文的东风。

662
来自专栏SAP最佳业务实践

SAP最佳业务实践:半成品的计划与处理(234)-4成品生产1

image.png 创建产成品的销售订单输入销售订单条目 (109) 客户需要物料 F234-1 或 F234-2 或这两种物料,并将这一需求发送给工厂 100...

2965

扫码关注云+社区