常用的几种大数据架构剖析

数据分析工作虽然隐藏在业务系统背后,但是具有非常重要的作用,数据分析的结果对决策、业务发展有着举足轻重的作用。随着大数据技术的发展,数据挖掘、数据探索等专有名词曝光度越来越高,但是在类似于Hadoop系列的大数据分析系统大行其道之前,数据分析工作已经经历了长足的发展,尤其是以BI系统为主的数据分析,已经有了非常成熟和稳定的技术方案和生态系统,对于BI系统来说,大概的架构图如下:

可以看到在BI系统里面,核心的模块是Cube,Cube是一个更高层的业务模型抽象,在Cube之上可以进行多种操作,例如上钻、下钻、切片等操作。大部分BI系统都基于关系型数据库,关系型数据库使用SQL语句进行操作,但是SQL在多维操作和分析的表示能力上相对较弱,所以Cube有自己独有的查询语言MDX,MDX表达式具有更强的多维表现能力,所以以Cube为核心的分析系统基本占据着数据统计分析的半壁江山,大多数的数据库服务厂商直接提供了BI套装软件服务,轻易便可搭建出一套Olap分析系统。不过BI的问题也随着时间的推移逐渐显露出来:

  • BI系统更多的以分析业务数据产生的密度高、价值高的结构化数据为主,对于非结构化和半结构化数据的处理非常乏力,例如图片,文本,音频的存储,分析。
  • 由于数据仓库为结构化存储,在数据从其他系统进入数据仓库这个东西,我们通常叫做ETL过程,ETL动作和业务进行了强绑定,通常需要一个专门的ETL团队去和业务做衔接,决定如何进行数据的清洗和转换。
  • 随着异构数据源的增加,例如如果存在视频,文本,图片等数据源,要解析数据内容进入数据仓库,则需要非常复杂等ETL程序,从而导致ETL变得过于庞大和臃肿。
  • 当数据量过大的时候,性能会成为瓶颈,在TB/PB级别的数据量上表现出明显的吃力。
  • 数据库的范式等约束规则,着力于解决数据冗余的问题,是为了保障数据的一致性,但是对于数据仓库来说,我们并不需要对数据做修改和一致性的保障,原则上来说数据仓库的原始数据都是只读的,所以这些约束反而会成为影响性能的因素。
  • ETL动作对数据的预先假设和处理,导致机器学习部分获取到的数据为假设后的数据,因此效果不理想。例如如果需要使用数据仓库进行异常数据的挖掘,则在数据入库经过ETL的时候就需要明确定义需要提取的特征数据,否则无法结构化入库,然而大多数情况是需要基于异构数据才能提取出特征。

在一系列的问题下,以Hadoop体系为首的大数据分析平台逐渐表现出优异性,围绕Hadoop体系的生态圈也不断的变大,对于Hadoop系统来说,从根本上解决了传统数据仓库的瓶颈的问题,但是也带来一系列的问题:

  1. 从数据仓库升级到大数据架构,是不具备平滑演进的,基本等于推翻重做。
  2. 大数据下的分布式存储强调数据的只读性质,所以类似于Hive,HDFS这些存储方式都不支持update,HDFS的write操作也不支持并行,这些特性导致其具有一定的局限性。

基于大数据架构的数据分析平台侧重于从以下几个维度去解决传统数据仓库做数据分析面临的瓶颈:

  1. 分布式计算:分布式计算的思路是让多个节点并行计算,并且强调数据本地性,尽可能的减少数据的传输,例如Spark通过RDD的形式来表现数据的计算逻辑,可以在RDD上做一系列的优化,来减少数据的传输。
  2. 分布式存储:所谓的分布式存储,指的是将一个大文件拆成N份,每一份独立的放到一台机器上,这里就涉及到文件的副本,分片,以及管理等操作,分布式存储主要优化的动作都在这一块。
  3. 检索和存储的结合:在早期的大数据组件中,存储和计算相对比较单一,但是目前更多的方向是在存储上做更多的手脚,让查询和计算更加高效,对于计算来说高效不外乎就是查找数据快,读取数据快,所以目前的存储不单单的存储数据内容,同时会添加很多元信息,例如索引信息。像类似于parquet和carbondata都是这样的思想。

总的来说,目前围绕Hadoop体系的大数据架构大概有以下几种:

传统大数据架构

​之所以叫传统大数据架构,是因为其定位是为了解决传统BI的问题,简单来说,数据分析的业务没有发生任何变化,但是因为数据量、性能等问题导致系统无法正常使用,需要进行升级改造,那么此类架构便是为了解决这个问题。可以看到,其依然保留了ETL的动作,将数据经过ETL动作进入数据存储。

优点:简单,易懂,对于BI系统来说,基本思想没有发生变化,变化的仅仅是技术选型,用大数据架构替换掉BI的组件。

缺点:对于大数据来说,没有BI下如此完备的Cube架构,虽然目前有kylin,但是kylin的局限性非常明显,远远没有BI下的Cube的灵活度和稳定度,因此对业务支撑的灵活度不够,所以对于存在大量报表,或者复杂的钻取的场景,需要太多的手工定制化,同时该架构依旧以批处理为主,缺乏实时的支撑。

适用场景:数据分析需求依旧以BI场景为主,但是因为数据量、性能等问题无法满足日常使用。

流式架构

在传统大数据架构的基础上,流式架构非常激进,直接拔掉了批处理,数据全程以流的形式处理,所以在数据接入端没有了ETL,转而替换为数据通道。经过流处理加工后的数据,以消息的形式直接推送给了消费者。虽然有一个存储部分,但是该存储更多的以窗口的形式进行存储,所以该存储并非发生在数据湖,而是在外围系统。

优点:没有臃肿的ETL过程,数据的实效性非常高。

缺点:对于流式架构来说,不存在批处理,因此对于数据的重播和历史统计无法很好的支撑。对于离线分析仅仅支撑窗口之内的分析。

适用场景:预警,监控,对数据有有效期要求的情况。

Lambda架构

Lambda架构算是大数据系统里面举足轻重的架构,大多数架构基本都是Lambda架构或者基于其变种的架构。Lambda的数据通道分为两条分支:实时流和离线。实时流依照流式架构,保障了其实时性,而离线则以批处理方式为主,保障了最终一致性。什么意思呢?流式通道处理为保障实效性更多的以增量计算为主辅助参考,而批处理层则对数据进行全量运算,保障其最终的一致性,因此Lambda最外层有一个实时层和离线层合并的动作,此动作是Lambda里非常重要的一个动作,大概的合并思路如下:

优点:既有实时又有离线,对于数据分析场景涵盖的非常到位。

缺点:离线层和实时流虽然面临的场景不相同,但是其内部处理的逻辑却是相同,因此有大量冗余和重复的模块存在。

适用场景:同时存在实时和离线需求的情况。

Kappa架构

​ Kappa架构在Lambda 的基础上进行了优化,将实时和流部分进行了合并,将数据通道以消息队列进行替代。因此对于Kappa架构来说,依旧以流处理为主,但是数据却在数据湖层面进行了存储,当需要进行离线分析或者再次计算的时候,则将数据湖的数据再次经过消息队列重播一次则可。

优点:Kappa架构解决了Lambda架构里面的冗余部分,以数据可重播的超凡脱俗的思想进行了设计,整个架构非常简洁。

缺点:虽然Kappa架构看起来简洁,但是施难度相对较高,尤其是对于数据重播部分。

适用场景:和Lambda类似,改架构是针对Lambda的优化。

Unified架构

​ 以上的种种架构都围绕海量数据处理为主,Unifield架构则更激进,将机器学习和数据处理揉为一体,从核心上来说,Unifield依旧以Lambda为主,不过对其进行了改造,在流处理层新增了机器学习层。可以看到数据在经过数据通道进入数据湖后,新增了模型训练部分,并且将其在流式层进行使用。同时流式层不单使用模型,也包含着对模型的持续训练。

优点:Unifield架构提供了一套数据分析和机器学习结合的架构方案,非常好的解决了机器学习如何与数据平台进行结合的问题。

缺点:Unifield架构实施复杂度更高,对于机器学习架构来说,从软件包到硬件部署都和数据分析平台有着非常大的差别,因此在实施过程中的难度系数更高。

适用场景:有着大量数据需要分析,同时对机器学习方便又有着非常大的需求或者有规划。

总结

以上几种架构为目前数据处理领域使用比较多的几种架构,当然还有非常多其他架构,不过其思想都会或多或少的类似。数据领域和机器学习领域会持续发展,以上几种思想或许终究也会变得过时。


更多精彩洞见,请关注微信公众号:思特沃克

Share

CSS-in-JS,向Web组件化再迈一大步

简介

CSS-in-JS是什么,看到这个词就能大概猜到是在JavaScript里写CSS,那为什么要在JavaScript里写CSS呢,像之前一样写在css文件里哪里不好么?

在介绍这个概念之前,先来回顾一下在日常编写CSS代码时都有哪些痛点:

  • 全局污染 – CSS的选择器是全局生效的,所以在class名称比较简单时,容易引起全局选择器冲突,导致样式互相影响。
  • 命名混乱 – 因为怕全局污染,所以日常起class名称时会尽量加长,这样不容易重复,但当项目由多人维护时,很容易导致命名风格不统一。
  • 样式重用困难 – 有时虽然知道项目上已有一些相似的样式,但因为怕互相影响,不敢重用。
  • 代码冗余 – 由于样式重用的困难性等问题,导致代码冗余。

进化史介绍

在CSS的进化历史上,出现过各种各样的框架致力于解决以上的问题:

  • SASS, LESS – 提供了变量、简单函数、运算、继承等,扩展性、重用性都有了很大的提升,解决了一些样式重用冗余的问题,但是对于命名混乱问题的效果不大。
  • BEM (.block__element–modifier) – 比较流行的class命名规则,部分解决了命名混乱和全局污染的问题,但class定义起来还是不太方便,比较冗长,而且和第三方库的命名还是有可能冲突。
  • CSS Modules – 模块化CSS,将CSS文件以模块的形式引入到JavaScript里,基本上解决了全局污染、命名混乱、样式重用和冗余的问题,但CSS有嵌套结构的限制(只能一层),也无法方便的在CSS和JavaScript之间共享变量。

可以看一个简单的CSS Modules例子了解一下:

生成的dom结构如下图,基于css文件中的class名称生成了唯一的class名称,样式会定义到生成的class上。

styles打印出来如下图,定义了css中的class名字和生成的唯一class名字的对应关系。

可以看出,以上框架都解决了不少痛点,但也还是各有一些不足,当然CSS-in-JS也并不是完美的解决了所有问题,我们先来详细介绍一下。

流行框架介绍

现在随着组件化概念的流行,对从组件层面维护CSS样式的需求日益增大,CSS-in-JS就是在组件内部使用JavaScript对CSS进行了抽象,可以对其声明和加以维护。这样不仅降低了编写CSS样式带来的风险,也让开发变得更加轻松。它和CSS Modules的区别是不再需要CSS样式文件。

来看一下几个流行的CSS-in-JS框架六个月内的下载趋势

我们来看看几个下载量靠前的框架的风格是什么样的:

styled-components

先来看看下载量最高的styled-component的代码风格:

从上图可以看出,Title和Wrapper都是框架包装好的component,可以直接在react的jsx语法中使用,在包装component的时候还定义了标签分别是h1和section。此段代码产生的html dom如下图所示:

可以看到section和h1上分别生成了唯一的class名称,样式也对应的定义在生成的class上了。

这样就可以解决命名混乱和全局污染的问题。组件相关的代码都在一起,可以统一查看,也可以方便的重用样式。

glamorous

再来看看glamorous,这个框架是PayPal开发的。(前两个logo看下来,恍惚间感觉进了化妆品专柜)。

和styled-component不同的是,glamorous的样式直接以attribute的形式定义在了dom上,之后虽然也为其生成了class名称及样式,但这种以attribute定义的方式对伪类选择符(如 :hover)支持的不好,会带来一些不方便,而且需要再记住一套attributes名称和值与真正的css样式代码的对应关系。

JSS

和上面两个框架类似,jss也是会定义styles对象,并附到component上,最后生成的dom也是会有生成的唯一class名称,并有对应的样式,但样式并不是真正的css语法,而是对象的属性和值,这样也是对伪类选择符支持的不好,而且也需要记住属性和css样式代码之间的对应关系。

Radium

Radium在定义样式对象上看似和其他相似,但在生成dom结构的时候并没有生成唯一的class名称,而是直接把样式放到了style属性上,这样会带来诸如可读性差、CSS权重过大、不支持伪类选择符等问题。

测试

下面再来看一个styled-component提供的基于jest的测试框架:

jest-styled-components

这个框架主要是通过生成Snapshot并比较的方式来保证component样式的每次更改都会被检测到,并且样式是期望的样式。这样就又降低了重构CSS样式带来的风险。

优劣势总结

看了这些框架后,可以发现CSS-in-JS的优势还是挺多的:

  • 因为有了生成的唯一class名称,避免了全局污染的问题
  • 唯一的class名称也解决了命名规则混乱的问题
  • JavaScript和CSS之间可以变量共享,比如一些基础的颜色和尺寸,这样再当需要在JavaScript里计算一些高度的时候,可以取到和dom相关的一些padding,margin数值,统一管理
  • 只生成页面需要用到的代码,缩减了最终包的大小,提升了性能
  • CSS的单元测试增加了样式重构的安全性

但是CSS-in-JS也存在着一些不足和争议:

  • 有些观点觉得JS和CSS的关系没这么近,把CSS写进JS里引入了新的一套依赖,增加了复杂度,新人加入项目后需要学习的东西就更多了,也让学习曲线更加陡了
  • 对前端框架确实有些依赖性,更适合于组件化的框架,如React等
  • Debug的时候需要花更多的功夫才能找到对应的样式代码
  • 覆盖第三方插件样式时会有权重不够的问题
  • Lint工具对于JavaScript内部的CSS代码样式支持的还不够

最后

在ThoughtWorks最新一期的技术雷达(CSS-in-JS | Technology Radar | ThoughtWorks)里,它的等级是Assess,表示的是:“值得追求。重要的是理解如何建立这种能力。企业应该在风险可控的项目中尝试此技术。” 所以最后想说的是,虽然它还是有些不足和争议,在应用之前需要多角度衡量一下对项目的适合度。但它的优点也很多,确确实实解决了很多痛点,而且与web组件化的方向高度一致,希望大家在条件合适的情况下多多尝试,多多反馈,这样也能促进整个CSS编码体验的继续进化~


更多精彩洞见,请关注微信公众号:思特沃克

Share

浅谈敏捷离岸团队沟通

很多人说,要让团队敏捷, 先让团队坐在一起。没错,“坐在一起”这区区几个字,可以解决团队从沟通到信任到效率提升的不少问题。作为团队的业务分析师,我们很多时候都扮演着产品端和开发端的黏合剂,最理想的工作环境可能是坐在产品团队和交付团队中间;办公室就是大舞台,随时都能展现自己的十八般武艺,把“坐在一起”的效应发挥到极致。

然而,由于种种原因,我们不免会遇到跨文化、跨时区的离岸团队。当“坐在一起”的舒适圈被打破后,很多问题则接踵而来:“见都没见过的团队成员怎么建立信任?”、“人都找不到怎么高效沟通?”、“时区空间不一致怎么组织工作坊?”经历了一个离岸团队的从无到有,淌过了大大小小的坑,笔者将所见所得整理成本文,希望能对同样纠结于此的同行们有一点点裨益。

按需互访

“见都没见过的团队成员怎么建立信任?” 答案恐怕是“无法建立”。别怪我这回答太消极任性,对于远隔重洋甚至黑夜颠倒的离岸团队而言,留言和视频所带来的困惑和乏力往往多过友好度和安全感。尤其对于那些毫无离岸合作经验的客户来说,面对这份未知和“不可控”往往是自我保护甚至抗拒的。

合作双方的互访则是建立信任的捷径,通过这扇窗户,一方面可以了解双方的企业文化、工作习惯乃至个人的性格特点;另一方面能够利用面对面工作坊高效梳理规划,最大程度保证后续项目推进方向的正确性。在条件允许的情况下,在项目伊始就建立互访机制:

  1. 在项目的重要节点,例如合作初始、项目里程碑、检查点进行在岸交流;
  2. 设立双向固定互访周期,如每三个月/半年。

当有机会去到客户现场,该如何充分利用短短数周或是数日?从个人经验来看,你或许可以:

  1. 做好充分的行前计划:不要把它当成签证材料准备中的“例行公事”。海外出差费时费力,为了确保效果,必须提前沟通准备并了解项目干系人的工作/休假安排,重要会议提前发好会议邀请,最好以邮件形式确认你的计划。相信我,你一定不想像我一样,人都到了客户现场以为一切自有安排,才发现接头人马上要休长假,独留“一脸懵逼”的自己。
  2. 尽量结识多的人:哪怕你自认不是“交际花”,哪怕自己的英语不够好,哪怕ta算不上直接干系人甚至来自不相干的团队/部门;当你们未来有工作上的交集,有一面之缘也大大好过冰冷的邮件。
  3. 定期和离岸团队沟通:即使有完整的记录,出差回来一次性输出的效果恐怕也差强人意。就算再忙再紧张,也要定期与团队交流,沟通自己在客户现场的收获,了解团队的问题和想法,并及时反馈到客户现场的工作计划中。

当客户即将来访,该如何抓住深化合作的机会?

  1. 最重要的依然是计划:提前为客户来访的工作内容做好细致的安排准备,双方设置好来访预期和目标,最大程度利用好团队与客户成员共同工作的时间。
  2. 提升对客户的影响力:帮客户站得更高,看得更广。尽力帮助他们在来访过程中更好地感受我们的工作方式和文化氛围,甚至以我们为窗口,介绍目前国内市场上的先进案例和最佳实践。
  3. 为客户做好行前准备和安排:这包括签证准备、交通酒店及个人饮食过敏源等在内的各类生活细碎。很多客户并没有到访中国的经验,对这个陌生又熟悉的国家甚至存在很多误会和担心。我们可以简单准备一份包含城市介绍、物价水平等信息的行前小材料,让他们不至于在踏上飞机前对即将来访的城市一无所知。

搭建沟通框架

对于“坐在一起”的敏捷团队,沟通会在工作和相处中自然而然地发生。而当我们所处的是一个离岸团队,很多沟通问题则会因为物理位置、语言文化障碍、时差而被放大;最危险的可能是,你不知道你不知道;而某些沟通隔阂可能会在某些时刻产生致命的影响。在项目初始就定义好沟通的渠道和方式在这样的环境下显得尤为重要。简单来讲,我们最应该关注的是:谁和谁沟通,通过什么形式沟通,达成怎样的目标,要有怎样的沟通频次。

(在项目初始与客户达成对重要会议的一致理解)

(对团队成员之间的沟通渠道需要统一和确认)

巧用工具

“时区空间不一致怎么组织工作坊?”针对这个问题,恐怕不仅仅是需求分析相关的工作坊,离岸团队的种种限制对我们许多熟悉的组织技巧和习以为常的敏捷实践都提出了挑战。互访是类似昂贵的特效药,而真正要身体好,还离不开悉心的日常调养。合适的工具,则是这里的关键。

1.Always On:如果能有超过四个小时的工作时间重叠,Always on一定要有。实时视频能增加许多亲切感和趣味,拉近团队成员的距离;更能让包括站会、desk check在内的很多敏捷实践变得容易。有了Always On,信息不回抬头喊一喊,开会了朝屏幕招招手,方便直接,也算对得起客户给的“魔镜”名号。

2.远程协作编辑软件:市面上的远程协作软件让人眼花缭乱,在这里分享几个你也许正在寻找的:

  • Keynote-Collaborate Mode: Keynote算得上我们目前使用最为频繁的演示软件,而它的Collaborate Mode这项高能技巧好像并不是那么知名。协同编辑除了能在紧张的项目节奏中提高团队效率,还可以帮助在重要演示环节/工作坊中与客户进行快速确认。举个例子,如果有两人共同参加工作坊,一人作为组织者与客户交流,另一位则可以实时将产出记录到Keynote中,在工作坊结束前第一时间呈现给客户进行确认。

(只需点击标题右下方的Collaborate,输入你要协作编辑对象的iCloud邮箱,即可将当前文件分享给对方进行协同编辑)

  • RealtimeBoard: 如果让我列举“2017年度最好用工具”,RealtimeBoard一定榜上有名:它是我目前能找到的最好的sticker board,是组织远距离工作坊的最佳搭档。除了最常用的反馈会议、头脑风暴外,RealtimeBoard提供了许多针对不同场景的实用模板,例如用户故事地图, 产品演进路线。

(RealtimeBoard: 五花八门的实用模板)

(团队的第一次远程回顾会议)

3.即时通讯工具:每个人每天都要用到,自然必不可少。 在与海外客户的合作中,常见的主要有Skype, Lync(Skype for business), Slack,HipChat, Hangouts等。目前我们项目正在用的是Hipchat,比较突出的亮点是不用翻墙、可以与Jira, Github集成,缺点主要是记录保存时间较短。

4.项目资产管理工具:个人认为,离岸团队要比在岸团队更加注重文档,良好的文档整理能降低沟通成本,也让沟通有迹可循。关于用户故事/电子看板,常见的有Jira, Trello, Pivotal Tracker, Mingle;关于其他项目相关文档管理,一般使用Confluence, Google Drive, DropBox等。

文化互通

文化差异是每个海外合作团队所必须面对的。由于文化背景的不同,团队成员们有不同的语言体系、做事方式,继而对合作产生一些显而易见或者不知不觉的影响。它本身是一个中性词,甚至褒义词:两种截然不同的文化碰撞,让我们能够交到大洋彼岸的朋友,了解彼此的文化,是多么幸运的一件事。而它也有可能成为“问题”:因为缺乏了解,导致双方产生误解甚至不信任,影响健康的合作关系。

承认并沟通合作双方的文化差异永远不会太早,在项目开始前/初期就应该主动地向客户介绍我们的文化:这包括了大文化,即国与国之间的文化差异,例如中国人可能会相对内向,不说话并不代表没有关注讨论;也包括了小文化,即组织和团队的文化差异,例如我们会相对自管理,不存在传统的上下级观念。

我们总是在谈论良好的团队氛围对项目成功的重要性。良好的团队氛围永远不是从单纯的工作沟通中来的,而是必须来自于每一个活生生的人。当团队成员之间建立起个人关系,很多问题都可以迎刃而解。

(当遇到文化碰撞,团队里需要有人挺身而出)

以上寥寥几点来自笔者有限的项目实践,不免有所遗漏或有待商榷。但毫无疑问的是,离岸团队需要更用心和精巧地经营,而成功也往往离不开各个角色的配合与贡献。


更多精彩洞见,请关注微信公众号:思特沃克

Share

微服务下使用GraphQL构建BFF

微服务架构,这个在几年前还算比较前卫的技术在如今遍地开花。得益于开源社区的支持,我们可以轻松地利用 Spring Cloud 以及 Docker 容器化快速搭建一个微服务架构的原型。不管是成熟的互联网公司、创业公司还是个人开发者,对于微服务架构的接纳程度都相当高,微服务架构的广泛应用也自然促进了技术本身更好的发展以及更多的实践。本文将结合项目实践,剖析在微服务的背景下,如何通过前后端分离的方式开发移动应用。

对于微服务本身,我们可以参考 Martin Fowler 对 Microservice 的阐述。简单说来,微服务是一种架构风格。通过对特定业务领域的分析与建模,将复杂的应用分解成小而专一、耦合度低并且高度自治的一组服务。微服务中的每个服务都是很小的应用,这些应用服务相互独立并且可部署。微服务通过对复杂应用的拆分,达到简化应用的目的,而这些耦合度较低的服务则通过 API 形式进行通信,所以服务之间对外暴露的都是 API,不管是对资源的获取还是修改。

微服务架构的这种理念,和前后端分离的理念不谋而合,前端应用控制自己所有的 UI 层面的逻辑,而数据层面则通过对微服务系统的 API 调用完成。以 JSP (Java Server Pages) 为代表的前后端交互方式也逐渐退出历史舞台。前后端分离的迅速发展也得益于前端 Web 框架 (Angular, React 等) 的不断涌现,单页面应用(Single Page Application)迅速成为了一种前端开发标准范式。加之移动互联网的发展,不管是 Mobile Native 开发方式,还是 React Native / PhoneGap 之流代表的 Hybrid 应用开发方式,前后端分离让 Web 和移动应用成为了客户端。客户端只需要通过 API 进行资源的查询以及修改即可。

BFF 概况及演进

Backend for Frontends(以下简称BFF) 顾名思义,是为前端而存在的后端(服务)中间层。即传统的前后端分离应用中,前端应用直接调用后端服务,后端服务再根据相关的业务逻辑进行数据的增删查改等。那么引用了 BFF 之后,前端应用将直接和 BFF 通信,BFF 再和后端进行 API 通信,所以本质上来说,BFF 更像是一种“中间层”服务。下图看到没有BFF以及加入BFF的前后端项目上的主要区别。

1. 没有BFF 的前后端架构

在传统的前后端设计中,通常是 App 或者 Web 端直接访问后端服务,后台微服务之间相互调用,然后返回最终的结果给前端消费。对于客户端(特别是移动端)来说,过多的 HTTP 请求是很昂贵的,所以开发过程中,为了尽量减少请求的次数,前端一般会倾向于把有关联的数据通过一个 API 获取。在微服务模式下,意味着有时为了迎合客户端的需求,服务器常会做一些与UI有关的逻辑处理

2. 加入了BFF 的前后端架构

加入了BFF的前后端架构中,最大的区别就是前端(Mobile, Web) 不再直接访问后端微服务,而是通过 BFF 层进行访问。并且每种客户端都会有一个BFF服务。从微服务的角度来看,有了 BFF 之后,微服务之间的相互调用更少了。这是因为一些UI的逻辑在 BFF 层进行了处理。

BFF 和 API Gateway

从上文对 BFF 的了解来看,BFF 既然是前后端访问的中间层服务,那么 BFF 和 API Gateway 有什么区别呢?我们首先来看下 API Gateway 常见的实现方式。(API Gateway 的设计方式可能很多,这里只列举如下三种)

1. API Gateway 的第一种实现:一个 API Gateway 对所有客户端提供同一种 API

单个 API Gateway 实例,为多种客户端提供同一种API服务,这种情况下,API Gateway 不对客户端类型做区分。即所有 /api/users的处理都是一致的,API Gateway 不做任何的区分。如下图所示:

2. API Gateway 的第二种实现:一个 API Gateway 对每种客户端提供分别的 API

单个 API Gateway 实例,为多种客户端提供各自不同的API。比如对于 users 列表资源的访问,web 端和 App 端分别通过 /services/mobile/api/users, /services/web/api/users服务。API Gateway 根据不同的 API 判定来自于哪个客户端,然后分别进行处理,返回不同客户端所需的资源。

3. API Gateway 的第三种实现:多个 API Gateway 分别对每种客户端提供分别的 API

在这种实现下,针对每种类型的客户端,都会有一个单独的 API Gateway 响应其 API 请求。所以说 BFF 其实是 API Gateway 的其中一种实现模式。

GraphQL 与 REST

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.

GraphQL 作为一种 API 查询语句,于2015年被 Facebook 推出,主要是为了替代传统的 REST 模式,那么对于 GraphQL 和 REST 究竟有哪些异同点呢?我们可以通过下面的例子进行理解。

按照 REST 的设计标准来看,所有的访问都是基于对资源的访问(增删查改)。如果对系统中 users 资源的访问,REST 可能通过下面的方式访问:

Request:

GET http://localhost/api/users

Response:

[
  {
    "id": 1,
    "name": "abc",
    "avatar": "http://cdn.image.com/image_avatar1"
  },
  ...
]
  • 对于同样的请求如果用 GraphQL 来访问,过程如下:

Request:

POST http://localhost/graphql

Body:

query {users { id, name, avatar } }

Response:

{
  "data": {
    "users": [
      {
        "id": 1,
        "name": "abc",
        "avatar": "http://cdn.image.com/image_avatar1"
      },
      ...
    ]
  }
}

关于 GraphQL 更详细的用法,我们可以通过查看文档以及其他文章更加详细的去了解。相比于 REST 风格,GraphQL 具有如下特性:

1. 定义数据模型:按需获取

GraphQL 在服务器实现端,需要定义不同的数据模型。前端的所有访问,最终都是通过 GraphQL 后端定义的数据模型来进行映射和解析。并且这种基于模型的定义,能够做到按需索取。比如对上文 /users 资源的获取,如果客户端只关心 user.id, user.name 信息。那么在客户端调用的时候,query 中只需要传入 users {id \n name}即可。后台定义模型,客户端只需要获取自己关心的数据即可。

2. 数据分层

查询一组users数据,可能需要获取 user.friends, user.friends.addr 等信息,所以针对 users 的本次查询,实际上分别涉及到对 user, frind, addr 三类数据。GraphQL 对分层数据的查询,大大减少了客户端请求次数。因为在 REST 模式下,可能意味着每次获取 user 数据之后,需要再次发送 API 去请求 friends 接口。而 GraphQL 通过数据分层,能够让客户端通过一个 API获取所有需要的数据。这也就是 GraphQL(图查询语句 Graph Query Language)名称的由来。

{
  user(id:1001) { // 第一层
    name,
    friends { // 第二层
      name,
      addr { // 第三层
        country,
        city
      }
    }
  }
}

3. 强类型

const Meeting = new GraphQLObjectType({
  name: 'Meeting',
  fields: () => ({
    meetingId: {type: new GraphQLNonNull(GraphQLString)},
    meetingStatus: {type: new GraphQLNonNull(GraphQLString), defaultValue: ''}
  })
})

GraphQL 的类型系统定义了包括 Int, Float, String, Boolean, ID, Object, List, Non-Null 等数据类型。所以在开发过程中,利用强大的强类型检查,能够大大节省开发的时间,同时也很方便前后端进行调试。

4. 协议而非存储

GraphQL 本身并不直接提供后端存储的能力,它不绑定任何的数据库或者存储引擎。它利用已有的代码和技术进行数据源的管理。比如作为在 BFF 层使用 GraphQL, 这一层的 BFF 并不需要任何的数据库或者存储媒介。GraphQL 只是解析客户端请求,知道客户端的“意图”之后,再通过对微服务API的访问获取到数据,对数据进行一系列的组装或者过滤。

5. 无须版本化

const PhotoType = new GraphQLObjectType({
  name: 'Photo',
  fields: () => ({
    photoId: {type: new GraphQLNonNull(GraphQLID)},
    file: {
      type: new GraphQLNonNull(FileType),
      deprecationReason: 'FileModel should be removed after offline app code merged.',
      resolve: (parent) => {
        return parent.file
      }
    },
    fileId: {type: new GraphQLNonNull(GraphQLID)}
  })
})

GraphQL 服务端能够通过添加 deprecationReason,自动将某个字段标注为弃用状态。并且基于 GraphQL 高度的可扩展性,如果不需要某个数据,那么只需要使用新的字段或者结构即可,老的弃用字段给老的客户端提供服务,所有新的客户端使用新的字段获取相关信息。并且考虑到所有的 graphql 请求,都是按照 POST /graphql 发送请求,所以在 GraphQL 中是无须进行版本化的。

GraphQL 和 REST

对于 GraphQL 和 REST 之间的对比,主要有如下不同:

1. 数据获取:REST 缺乏可扩展性, GraphQL 能够按需获取。GraphQL API 调用时,payload 是可以扩展的;

2. API 调用:REST 针对每种资源的操作都是一个 endpoint, GraphQL 只需要一个 endpoint( /graphql), 只是 post body 不一样;

3. 复杂数据请求:REST 对于嵌套的复杂数据需要多次调用,GraphQL 一次调用, 减少网络开销;

4. 错误码处理:REST 能够精确返回HTTP错误码,GraphQL 统一返回200,对错误信息进行包装;

5. 版本号:REST通过 v1/v2 实现,GraphQL 通过 Schema 扩展实现;

微服务 + GraphQL + BFF 实践

在微服务下基于 GraphQL 构建 BFF,我们在项目中已经开始了相关的实践。在我们项目对应的业务场景下,微服务后台有近 10 个微服务,客户端包括针对不同角色的4个 App 以及一个 Web 端。对于每种类型的 App,都有一个 BFF 与之对应。每种 BFF 只服务于这个 App。BFF 解析到客户端请求之后,会通过 BFF 端的服务发现,去对应的微服务后台通过 CQRS 的方式进行数据查询或修改。

1. BFF 端技术栈

我们使用 GraphQL-express 框架构建项目的 BFF 端,然后通过 Docker 进行部署。BFF 和微服务后台之间,还是通过 registrator 和 Consul 进行服务注册和发现。

  addRoutes () {
    this.express.use('/graphql', this.resolveFromRequestScopeAndHandle('GraphqlHandler'))
    this.serviceNames.forEach(serviceName => {
      this.express.use(`/api/${serviceName}`, this.routers.apiProxy.createRouter(serviceName))
    })
  }

在 BFF 的路由设置中,对于客户端的处理,主要有 /graphql/api/${serviceName}两部分。/graphql 处理的是所有 GraphQL 查询请求,同时我们在 BFF 端增加了 /api/${serviceName} 进行 API 透传,对于一些没有必要进行 GraphQL 封装的请求,可以直接通过透传访问到相关的微服务中。

2. 整体技术架构

整体来看,我们的前后端架构图如下,三个 App 客户端分别使用 GraphQL 的形式请求对应的 BFF。BFF 层再通过 Consul 服务发现和后端通信。

关于系统中的鉴权问题

用户登录后,App 直接访问 KeyCloak 服务获取到 id_token,然后通过 id_token 透传访问 auth-api 服务获取到 access_token, access_token 以 JWT (Json Web Token) 的形式放置到后续 http 请求的头信息中。

在我们这个系统中 BFF 层并不做鉴权服务,所有的鉴权过程全部由各自的微服务模块负责。BFF 只提供中转的功能。BFF 是否需要集成鉴权认证,主要看各系统自己的设计,并不是一个标准的实践。

3. GraphQL + BFF 实践

通过如下几个方面,可以思考基于 GraphQL 的 BFF 的一些更好的特质:

GraphQL 和 BFF 对业务点的关注

从业务上来看,PM App(使用者:物业经理)关注的是property,物业经理管理着一批房屋,所以需要知道所有房屋概况,对于每个房屋需要知道有没有对应的维修申请。所以 PM App BFF 在定义数据结构是,maintemamceRequestsproperty 的子属性。

同样类似的数据,Supplier App(使用者:房屋维修供应商)关注的是 maintenanceRequest(维修工单),所以在 Supplier App 获取的数据里,我们的主体是maintenanceRequest。维修供应商关注的是 workOrder.maintenanceRequest

所以不同的客户端,因为存在着不同的使用场景,所以对于同样的数据却有着不同的关注点。BFF is pary of Application。从这个角度来看,BFF 中定义的数据结构,就是客户端所真正关心的。BFF 就是为客户端而生,是客户端的一部分。需要说明的是,对于“业务的关注”并不是说,BFF会处理所有的业务逻辑,业务逻辑还是应该由微服务关心,BFF 关注的是客户端需要什么。

GraphQL 对版本化的支持

假设 BFF 端已经发布到生产环境,提供了 inspection 相关的 tenantslandlords 的查询。现在需要将图一的结构变更为图二的结构,但是为了不影响老用户的 API 访问,这时候我们的 BFF API 必须进行兼容。如果在 REST 中,可能会增加 api/v2/inspections进行 API 升级。但是在 BFF 中,为了向前兼容,我们可以使用图三的结构。这时候老的 APP 使用黄色区域的数据结构,而新的 APP 则使用蓝色区域定义的结构。

GraphQL Mutation 与 CQRS

mutation {
  area {
    create (input: {
      areaId:"111", 
      name:"test", 
    })
  }
}

如果你详细阅读了 GraphQL 的文档,可以发现 GraphQL 对 querymutation 进行了分离。所有的查询应该使用 query { ...},相应的 mutaition 需要使用 mutation { ... }。虽然看起来像是一个convention,但是 GraphQL 的这种设计和后端 API 的 读写职责分离(Command Query Responsibility Segregation)不谋而合。而实际上我们使用的时候也遵从这个规范。所以的 mutation 都会调用后台的 API,而后端的 API 对于资源的修改也是通过 SpringBoot EventListener 实现的 CQRS 模式。

如何做好测试

在引入了 BFF 的项目,我们的测试仍然使用金字塔原理,只是在客户端和后台之间,需要添加对 BFF 的测试。

  • Client 的 integration-test 关心的是 App 访问 BFF 的连通性,App 中所有访问 BFF 的请求都需要进行测试;
  • BFF 的 integration-test 测试的是 BFF 到微服务 API 的连通性,BFF 中依赖的所有 API 都应该有集成测试的保障;
  • API 的 integration-test 关注的是这个服务对外暴露的所有 API,通常测试所有的 Controller 中的 API;

结语

微服务下基于 GraphQL 构建 BFF 并不是银弹,也并不一定适合所有的项目,比如当你使用 GraphQL 之后,你可能得面临多次查询性能问题等,但这不妨碍它成为一个不错的尝试。你也的确看到 Facebook 早已经使用 GraphQL,而且 Github 也开放了 GraphQL 的API。而 BFF, 其实很多团队也都已经在实践了,在微服务下等特殊场景下,GraphQL + BFF 也许可以给你的项目带来惊喜。

参考资料

【注】部分图片来自网络


更多精彩洞见,请关注微信公众号:思特沃克

Share

Serverless的微服务架构案例

本文首发于GitChat《Serverless 风格微服务的持续交付(上):架构案例》,部分内容已做修改。文章聊天实录请见:“顾宇:构建Serverless 风格微服务实战解析(上)

一次微服务架构的奇遇

2016年12月初,当时我正在以一名 DevOps 咨询师的身份参与某客户的 DevOps 转型项目。这个项目是提升该部门在 AWS (Amazon Web Services)云计算平台上的 DevOps 能力。

自助服务的应用系统基于 Ruby on Rails 框架开发,前端部分采用 AngularJS 1.0,但是没有采用前后端分离的设计,页面代码仍然是通过 ERB 组合而成。移动端则采用 Cordova 开发。为了降低开发难度和工作量, 移动端的应用内容实际上是把 AngularJS 所生成的 Web 页面通过响应式样式的方式嵌入到移动端。但因为经常超时,所以这款 APP 体验并不好。

整套 Rails 应用部署在 AWS 上,并且通过网关和内部业务系统隔离。BOSS 系统采用 SOAP 对外暴露服务,并由另外一个部门负责。因此,云上的应用所做的业务是给用户展现一个使用友好的界面,并通过数据的转化和内部 BOSS 系统进行交互。系统架构如下图所示:

应用的交互流程如下

  1. 浏览器或者移动端通过域名(由 AWS Route 53托管)转向 CDN(采用 AWS Cloudfront)。
  2. CDN 根据请求的内容类别进行区分,静态文件(图片,JS,CSS 样式等),会转向 AWS S3 存储。动态请求会直接发给负载均衡器 (AWS Elastic Load Balancer)。
  3. 负载均衡器会根据各 EC2 计算实例的负载状态将请求转发到不同的实例上的 Ruby On Rails 应用上。每一个应用都是一个典型的 MVC Web 应用。
  4. EC2 上的应用会将一部分数据存储在关系型数据服务(AWS RDS,Relational Database Service)上,一部分存储在本地文件里。经过应用的处理,转换成 SOAP 请求通过 网关发送给 BOSS 系统处理。BOSS 系统处理完成后会返回对应的消息。

根据业务的需要,一部分数据会采用 AWS ElastiCache 的 Redis 服务作为缓存以优化业务响应速度。

团队痛点

这个应用经历了多年的开发,前后已经更换过很多技术人员。但是没有人对这个应用代码库有完整的的认识。因此,我们对整个团队和产品进行了一次痛点总结:

组织结构方面

运维团队成为瓶颈,60 个人左右的开发团队只有 4 名 Ops 支持。运维团队除了日常的事务以外,还要给开发团队提供各种支持。很多资源的使用权限被限制在这个团队里,就导致各种问题的解决进度进一步拖延。

随着业务的增长,需要基础设施代码库提供各种各样的能力。然而 Ops 团队的任何更改都会导致所有的开发团队停下手头的进度去修复更新所带来的各种问题。

应用架构方面

应用架构并没有达到前后端分离的效果,仍然需要同一个工程师编写前后端代码。这样的技术栈对于对于开发人员的要求很高,然而市场上缺乏合适的 RoR 工程师,导致维护成本进一步上升。经过了三个月,仍然很难招聘到合适的工程师。

多个团队在一个代码库上工作,新旧功能之间存在各种依赖点。加上 Ruby 的语言特性,使得代码中存在很多隐含的依赖点和类/方法覆盖,导致了开发进度缓慢。我们一共有 4 个团队在一个代码库上工作,3个团队在开发新的功能。1 个团队需要修复 Bug 和清理技术债,这一切都要同时进行。

技术债方面

代码库中有大量的重复 Cucumber 自动化测试,但是缺乏正确的并行测试策略,导致自动化测试会随机失败,持续集成服务器 (Jenkins)的 slave 节点本地难以创建,导致失败原因更加难以查找。如果走运的话,从提交代码到新的版本发布至少需要 45 分钟。如果不走运的话,两三天都无法完成一次成功的构建,真是依靠人品构建。

基础设施即代码(Infrastructure as Code)建立在一个混合的遗留的 Ruby 代码库上。这个代码库用来封装一些类似于 Packer 和 AWS CLI 这样的命令行工具,包含一些 CloudFormation 的转化能力。由于缺乏长期的规划和编码规范,加之人员变动十分频繁,使得代码库难以维护。

此外,基础设施代码库作为一个 gem 和应用程序代码库耦合在一起,运维团队有唯一的维护权限。因此很多基础设施上的问题开发团队无法解决,也不愿解决。

我参与过很多 Ruby 技术栈遗留系统的维护。在经历了这些 Ruby 项目之后,我发现 Ruby 是一个开发起来很爽但是维护起来很痛苦的技术栈。大部分的维护更改是由于 Ruby 的版本 和 Gem 的版本更新导致的。此外,由于 Ruby 比较灵活,人们都有自己的想法和使用习惯,因此代码库很难维护。

虽然团队已经有比较好的持续交付流程,但是 Ops 能力缺乏和应用架构带来的局限阻碍了整个产品的前进。因此,当务之急是能够通过 DevOps 提升团队的 Ops 能力,缓解 Ops 资源不足,削弱 DevOps 矛盾。

DevOps 组织转型中一般有两种方法:一种方法是提升 Dev 的 Ops 能力,另一种方法是降低 Ops 工作门槛。在时间资源很紧张的情况下,通过技术的改进,降低 Ops 的门槛是短期内收益最大的方法。

微服务触发点:并购带来的业务功能合并

在我加入项目之后,客户收购了另外一家业务相关的企业。因此原有的系统要同时承载两个业务。恰巧有个订单查询的业务需要让当前的团队完成这样一个需求:通过现有的订单查询功能同时查询两个系统的业务订单。

这个需求看起来很简单,只需要在现有系统中增加一个数据源,然后把输入的订单号进行转化就可以。但由于存在上述的痛点,完成这样一个简单的功能的代价是十分高昂的。几乎 70% 的工作量都和功能开发本身没有关系。

在开发的项目上进行 DevOps 转型就像在行进的汽车上换车轮,一不留心就会让所有团队停止工作。因此我建议通过设立并行的新团队来同时完成新功能的开发和 DevOps 转型的试点。

这是一个功能拆分和新功能拆分需求,刚好订单查询是原系统中一个比较独立和成熟的功能。为了避免影响原有各功能开发的进度。我们决定采用微服务架构来完成这个功能。

构建微服务的架构的策略

我们并不想重蹈之前应用架构的覆辙,我们要做到前后端分离。使得比较小的开发团队可以并行开发,只要协商好了接口之间的契约(Contract),未来开发完成之后会很好集成。

这让我想起了 Chris Richardson 提出了三种微服务架构策略,分别是:停止挖坑,前后端分离和提取微服务。

停止挖坑的意思是说:如果发现自己掉坑里,马上停止。

原先的单体应用对我们来说就是一个焦油坑,因此我们要停止在原来的代码库上继续工作。并且为新应用单独创建一个代码库。所以,我们拆分策略模式如下所示:

在我们的架构里,实现新的需求就要变动老的应用。我们的想法是:

  1. 构建出新的业务页面,生成微服务契约。
  2. 根据 API 契约构建出新的微服务。
  3. 部署 Web 前端到 S3 上,采用 S3 的 Static Web Hosting (静态 Web 服务) 发布。
  4. 部署后端微服务上线,并采用临时的域名和 CDN 加载点进行测试。
  5. 通过更新 CDN 把原应用的流量导向新的微服务。
  6. 删除旧的服务代码。

我们原本要在原有的应用上增加一个 API 用来访问以前应用的逻辑。但想想这实际上也是一种挖坑。在评估了业务的复杂性之后。我们发现这个功能如果全新开发只需要 2人2周(一个人月)的时间,这仅仅占我们预估工作量的20%不到。因此我们放弃了对遗留代码动工的念头。最终通过微服务直接访问后台系统,而不需要通过原有的应用。

在我们拆微服务的部分十分简单。对于后端来说说只需要修改 CDN 覆盖原先的访问源(Origin)以及保存在 route.rb 里的原功能访问点,就可以完成微服务的集成。

构建出新的业务页面,生成微服务契约

结合上面的应用痛点和思路,在构建微服务的技术选型时我们确定了以下方向:

  1. 前端框架要具备很好的 Responsive 扩展。
  2. 采用 Swagger 来描述 API 需要具备的行为。
  3. 过消费者驱动进行契约测试驱动微服务后端开发。
  4. 前端代码库和后端代码库分开。
  5. 前端代码框架要对持续交付友好。

因此我们选择了 React 作为前端技术栈并且用 yarn 管理依赖和任务。另外一个原因是我们能够通过 React-native 为未来构建新的应用做好准备。此外,我们引入了 AWS SDK 的 nodejs 版本。用编写一些常见的诸如构建、部署、配置等 AWS 相关的操作。并且通过 swagger 描述后端 API 的行为。这样,后端只需要满足这个 API 规范,就很容易做前后端集成。

部署前端部分到 S3 上

由于 AWS S3 服务自带 Static Web Hosting (静态页面服务) 功能,这就大大减少了我们构建基础环境所花费的时间。如果你还想着用 Nginx 和 Apache 作为静态内容的 Web 服务器,那么你还不够 CloudNative。

虽然AWS S3 服务曾经发生过故障,但 SLA 也比我们自己构建的 EC2 实例处理静态内容要强得多。此外还有以下优点:

  1. 拥有独立的 URL,很容易做很多 301 和 302 的重定向和改写操作。
  2. 和 CDN(CloudFront)集成很好。
  3. 很容易和持续集成工具集成。
  4. 最大的优点:比 EC2 便宜。

根据 API 契约构建出新的微服务

在构建微服务的最初,我们当时有两个选择:

采用 Sinatra (一个用来构建 API 的 Ruby gem) 构建一个微服务 ,这样可以复用原先 Rails 代码库的很多组件。换句话说,只需要 copy 一些代码,放到一个单独的代码库里,就可以完成功能。但也同样会面临之前 Ruby 技术栈带来的种种问题。

采用 Spring Boot 构建一个微服务,Java 作为成熟工程语言目前还是最好的选择,社区和实践都非常成熟。可以复用后台很多用来做 SOAP 处理的 JAR 包。另一方面是解决了 Ruby 技术栈带来的问题。

然而,这两个方案的都有一个共同的问题:需要通过 ruby 语言编写的基础设施工具构建一套运行微服务的基础设施。而这个基础设施的搭建,前前后后估计得需要至少 1个月,这还是在运维团队有人帮助的情况下的乐观估计。

所以,要找到一种降低环境构建和运维团队阻塞的方式避开传统的 EC2 搭建应用的方式。

这,只有 Lambda 可以做到!

基于上面的种种考量,我们选择了 Amazon API Gateway + Lambda 的组合。而 Amazon API Gateway + Lambda 还有额外好处:

  1. 支持用 Swagger 规范配置 API Gateway。也就是说,你只要导入前端的 Swagger 规范,就可以生成 API Gateway。
  2. 可以用数据构建 Mock API,这样就可以很大程度上实现消费者驱动契约开发。
  3. 通过 Amazon API Gateway 的 Stage 功能,我们无需构建 QA 环境,UAT 环境和 Staging 环境。只需要指定不同的 Stage,就可以完成对应的切换。
  4. Lambda 的发布生效时间很短,反馈很快。原先用 CloudFormation 构建的 API 基础设施需要至少 15 分钟,而 Lambda 的生效只需要短短几秒钟。
  5. Lambda 的编写很方便,可以采用在线的方式。虽然在线 IDE 并不很好用,但是真的也写不了几行代码。
  6. Lambda 自动根据请求自扩展,无需考虑负载均衡。

虽然有这么多优点,但不能忽略了关键性的问题:AWS Lambda 不一定适合你的应用场景!

很多对同步和强一致性的业务需求是无法满足的。所以,AWS Lambda 更适合能够异步处理的业务场景。此外,AWS Lambda 对消耗存储空间和 CPU 很多的场景支持不是很好,例如 AI 和 大数据。(PS: AWS 已经有专门的 AI 和大数据服务了,所以不需要和自己过不去)

对于我们的应用场景而言,上文中的 Ruby On Rails 应用中的主要功能(至少60% 以上)实际上只是一个数据转换适配器:把前端输入的数据进行加工,转换成对应的 SOAP 调用。因此,对于这样一个简单的场景而言,Amazon API Gateway + Lambda 完全满足需求!

部署后端微服务

选择了Amazon API Gateway + Lambda 后,后端的微服务部署看起来很简单:

  • 更新 Lambda 函数。
  • 更新 API 规范,并要求 API 绑定对应 Lambda 函数处理请求。

但是,这却不是很容易的一件事。我们将在下一篇文章《Serverless 风格微服务的持续交付》中对这方面踩过的坑详细介绍。

把原应用的请求导向新的微服务

这时候在 CDN 上给新的微服务配置 API Gateway 作为一个新的源(Origin),覆盖原先写在 route.rb 和 nginx.conf 里的 API 访问规则就可以了。CDN 会拦截访问请求,使得请求在 nginx 处理之前就会把对应的请求转发到 API Gateway。

当然,如果你想做灰度发布的话,就不能按上面这种方式搞了。CloudFront 和 ELB 负载均衡 并不具备带权转发功能。因此你需要通过 nginx 配置,按访问权重把 API Gateway 作为一个 upstream 里的一个 Server 就可以。

删除旧的服务代码

不要留着无用的遗留代码!

不要留着无用的遗留代码!

不要留着无用的遗留代码!

重要且最容易被忽略的事情要说三遍。斩草要除根,虽然我们可以保持代码不动。但是清理不再使用的遗留代码和自动化测试可以为其它团队减少很多不必要的工作量。

最终的架构

经过6个人两个月的开发(原计划8个人3个月),我们的 Serverless 微服务最终落地了。当然这中间有 60% 的时间是在探索全新的技术栈。如果熟练的话,估计 4 个人一个月就可以完成工作。

最后的架构如下图所示:

在上图中,请求仍然是先通过域名到 CDN (CloudFront),然后:

  1. CDN 根据请求点的不同,把页面请求转发至 S3 ,把 API 请求转发到 API Gateway。
  2. 前端的内容通过蓝绿部署被放到了不同的 S3 Bucket 里面,只需要改变 CDN 设置就可以完成对应内容的部署。虽然对于部署来说蓝绿 Bucket 乍看有一点多余,但这是为了能够在生产环境下做集成在线测试准备的。这样可以使环境不一致尽可能少。
  3. API Gateway 有自己作用的 VPC,很好的实现了网络级别的隔离。
  4. 通过 API Gateway 转发的 API 请求分成了三类,每一类都可以根据请求状况自扩展。
  5. 身份验证类:第一次访问会请求 ElastCache(Redis),如果 Token 失效或者不存在,则重新走一遍用户验证流程。
  6. 数据请求类:数据请求类会通过 Lambda 访问由其他团队开发的 Java 微服务,这类微服务是后台系统唯一的访问点。
  7. 操作审计类:请求会记录到 DynamoDB (一种时间序列数据库)中,用来跟踪异步请求的各种日志。
  8. API Gateway 自己有一些缓存,可以加速 API 的访问。
  9. 消息返回后,再有三类不同的请求的结果统一通过 API Gateway 返回给客户端。

Serverless 风格微服务架构的优点

由于没有 EC2 设施初始化的时间,我们减少了至少一个月的工作量,分别是:

  1. 初始化网络配置的时间。
  2. 构建 EC2 配置的时间。
  3. 构建反向代理和前端静态内容服务器的时间。
  4. 构建后端 API 应用基础设施的时间。
  5. 构建负载均衡的时间。
  6. 把上述内容用 Ruby 进行基础设施即代码化的时间。

如果要把 API Gateway 算作是基础设施初始化的时间来看。第一次初始化 API Gateway 用了一天,以后 API Gateway 结合持续交付流程每次修改仅仅需要几分钟,Serverless 风格的微服务大大降低了基础设施配置和运维门槛。

此外,对于团队来说,Amazon API Gateway + Lambda 的微服务还带来其它好处:

  1. 开发效率高,原先至少 45 分钟的开发反馈周期缩短为 5 分钟以内。
  2. 无关的代码量少,需要维护的代码量少。除了专注业务本身。上游和 API Gateway 的集成以及下游和后端服务的集成代码量很少。
  3. 应用维护成本低。代码仅仅几十行,且都为函数式,很容易测试。避免了代码库内部复杂性的增加。

此外,我们做了 Java 和 NodeJs 比较。在开发同样的功能下,NodeJS 的开发效率更高,原因是 Java 要把请求的 json 转化为对象,也要把返回的 json 转化为对象,而不像 nodejs 直接处理 json。此外, Java 需要引入一些其它 JAR 包作为依赖。在 AWS 场景下开发同样一个函数式微服务,nodejs 有 4 倍于 java 的开发效率提升。

最后

Serverless 风格的微服务虽然大大减少了开发工作量以及基础设施的开发维护工作量。但也带来了新的挑战:

  1. 大量函数的管理。
  2. SIT,UAT 环境的管理。
  3. 持续交付流水线的配置。
  4. 面对基础设施集成带来的测试。

这让我们重新思考了 Serverless 架构的微服务如何更好的进行持续交付。


更多精彩洞见,请关注微信公众号:思特沃克

Share