其他分享
首页 > 其他分享> > 创业一年来经历的技术风雨|中生代技术分享第十七期

创业一年来经历的技术风雨|中生代技术分享第十七期

作者:互联网

创业一年来经历的技术风雨|中生代技术分享第十七期

张逸 中生代技术

创业一年来经历的技术风雨|中生代技术分享第十七期

虽然说是技术风雨,其实不过是一种文艺范儿的描述而已,没有这么夸张,只是在创业的这一年中,整个产品研发过程给了我许多前所未有的体验和启示,所以想借助这个机会和群里的朋友一起交流,分享自己的一些收获。

这次分享打算从三个方面开始讲解,分别是:

1
产品的架构以及技术选型
2
技术团队的管理
3
研发团队总结的技术实践

1

产品的架构以及技术选型


产品的架构

首先来说说产品的架构。首先声明这不是广告,我们公司产品的商业版本也还未完全成型,所以本次交流仅仅是技术上的。

整体架构

产品代号为Mort,是基于大数据平台的商业智能(BI)产品。我们的产品架构如下所示:

创业一年来经历的技术风雨|中生代技术分享第十七期
我们选择了Spark作为我们的大数据分析平台。基于目前的应用场景,主要使用了Spark SQL,目前使用的版本为Spark 1.5.0。会在7月推出商业版本之前同步升级到Spark的最新版本。

在研发期间,我们从Spark 1.4升级到1.5,经过性能测评的Benchmark,确实有显著的提高。1.6版本在内存管理方面有明显的改善,Execution Memory与Store Memory的比例可以动态分配,但在测试我们自己的产品时发现内存并非主要的性能瓶颈,而是CPU,故而暂时没有考虑升级1.6。

从第一次升级Spark的性能测评,以及观察这一年来Spark版本的演进,我们对Spark的未来充满信心,尤其是Tungsten项目计划,会在内存管理、代码生成以及缓存管理等多方面都会有所提高,对于我们产品而言,算是“坐享其成”了。

由于我们要分析的维度和指标是由客户指定的,这就需要数据分析的聚合操作是灵活可定制的。因此在我们产品中写了一个简单的语法Parser,用以组装Spark SQL的SQL语句,执行分析,最后将DataFrame转换为我们期待的数据结构返回给前端。

但是,这种设计方案其实牵涉到两层解析的性能损耗,一个是我们自己的语法Parser,另一个是Spark SQL提供的Parser(通过它将其解析为DataFrame的API调用)。所以在将来我们会调整方案,直接将客户定制的聚合操作解析为对DataFrame的API调用(可能会使用新版本Spark的DataSet)。

微服务架构

我们的产品需要支持多种数据源,对数据源的访问是由另外一个standalone的服务CData来完成的,通过它可以隔离这种数据源的多样性。这相当于一个简单的微服务架构,目前仅提供两个服务,一个服务用于数据分析,一个服务用于对客户数据源的处理: 创业一年来经历的技术风雨|中生代技术分享第十七期
在未来,产品不止限于现有的两个服务,例如我正在考虑将定期的邮件导出服务独立出来,保证该服务的独立性,避免受到其他功能执行的影响,因为这个功能一旦失败,可能会对客户的业务产生重要影响。

然而,我们还是在理智地控制服务的粒度。我们不希望因为盲目地追求微服务架构,而带来运维上的成本。

元数据架构

我们的产品需要存储元数据(Metadata),用以支持Report、Dashboard以及数据分析,主要的数据模型结果如图所示: 创业一年来经历的技术风雨|中生代技术分享第十七期
针对元数据的处理逻辑,我们将之分为职责清晰的三层架构。自上而下分别为REST路由层、应用服务层和元数据资源库层。

REST路由层和应用服务层需要接收和返回的消息非常相似,甚至在某些场景中,消息结构完全相同,但我们仍然定义了两套消息体系(皆被定义为Case Class)。逻辑层与消息之间的关系如下图所示: 创业一年来经历的技术风雨|中生代技术分享第十七期

在REST服务框架方面,我们选择使用了Spray。选择这个框架的原因:一方面它足够轻量级,是一个纯粹的Web服务端框架;另一方面是它基于AKKA的消息处理架构在并发处理方面能够满足我们的要求。

技术选型

语言的选型

相信大家已经能够猜测到我们选择的语言,那就是Scala。巧合的是,在上周三,我的前同事吴雪峰(我们都称他为“吴大师”)正好“布道”了一把Scala。我们选择Scala的一个主因确实就是因为Spark。另一个原因呢?就是因为我确实不想再写Java了。

其实有时候我觉得语言的选型是没有什么道理的。除了特殊的应用场景,几乎所有的程序设计语言都能满足如今的软件开发需求。所以语言的纷争,有时候都成了宗教的纷争一般。

在我的团队,有熟悉Java的、有熟悉JavaScript包括NodeJS的,有熟悉Clojure的,当然也有熟悉Scala的。除了NodeJS,后端开发几乎都在JVM平台下。

我对语言选型的判断标准是:实用、高效、简洁、可维护。我对Java其实没有意见,但我始终认为即使是引入了Lambda以及Method Reference的Java 8在语法方面还是太冗长了。

Scala似乎从诞生开始,一直争议很大。早在2014年1月ThoughtWorks的Tech Radar中,就讲Scala列入了Adopt圈中,但却在其中特别标注了“the good parts”:创业一年来经历的技术风雨|中生代技术分享第十七期
在2016年Stack Overflow发布的开发人员调查结果中,我们也收获了一些信心。在最爱语言的调查中,Scala排在了第四名:创业一年来经历的技术风雨|中生代技术分享第十七期

在引领技术趋势的调查中,我们选用的React与Spark分列冠亚军: 创业一年来经历的技术风雨|中生代技术分享第十七期

在Top Paying Tech调查中,在美国学习Spark和Scala所值不菲,居然并列冠军: 创业一年来经历的技术风雨|中生代技术分享第十七期

其实有了微服务,在不影响代码维护性的情况下,使用多语言进行开发也成为了可能。或许在将来,我们产品的可能会用clojure或者Ruby来写DSL,用NodeJS负责元数据(以避免Spray + JSON4S不太好的Json对象序列化)。坦白说,我没有强烈的语言倾向性。

数据集的选型

我们还有一个最初的技术选型后来被认为是失败的选择。

CData服务需要将客户的数据源经过简单的ETL导入到系统中,我们称之为数据集(DataSet)。最初在进行技术选型时,我先后考虑过MySQL、Cassandra、HBase。后面两种都可以认为是列式存储的NoSQL数据库。团队中没有一个人有Cassandra的经验,至于HBase,在查询方面非常高效,但对聚合运算的支持明显不足,不适合我们的场景。再加上团队中有一位成员比较熟悉MySQL,最终决定使用MySQL。

然而,我们的产品需要支持大数据,当数据量上升到一定级别时,就需要系统很好地支持水平扩展,通过增加更多机器来满足性能上的需求。评估我们的架构,后端平台可以简单划分为三个层次:Web应用服务层(Spray + Nginix)、数据分析层(MESOS + Spark)以及存储层(主要用于存储分析数据DataSet,MySQL)。就会发现MySQL正是水平伸缩的最大障碍。

还好我们醒悟得早,在项目初期就否定了这个方案,而改为采用HDFS+Parquet。Parquet文件是一种列式数据存储结构,对于主要为分析型查询方式的BI数据操作,能够提供更好的查询性能。同时,Parquet文件存储的内容以二进制形式存放,相较于文本形式容量更小,可以节省更多的存储空间。

Spark SQL提供了对访问Parquet文件很好的集成。将Parquet文件存放到HDFS中,然后再通过Spark SQL访问,可以保证在存储层与数据分析层都能很好地支持分布式处理,从而保证系统的水平伸缩。当对大规模数据集进行分析处理时,可以通过水平增加更多的节点来满足高性能的实时查询要求。

我们曾经比较了Parquet方案与MySQL方案,在同等配置下前者的性能要远远优于后者,且Spark对Parquet的支持也要远远好于MySQL。

为了更好地提升性能,我们还计划在HDFS层之上引入Tachyon,充分发挥内存的优势,减少磁盘IO带来的性能损耗。

前端的技术选型

前端的技术选型则为React + Redux。选择React的原因很简单,一方面我们认为这种component方式的前端开发,可以极大地提高UI控件的重用,另一方面,我们认为React这种虚拟DOM的方式在性能上还是有一定优势的。此外,React的学习曲线也并不高,很容易上手。我们招了3个大学还未毕业的实习生,JS基础非常薄弱,在我们的培养下,一周后就可以慢慢开始完成React Component开发的小Story了。

最初,我们唯一的一位前端选择了使用CoffeeScript来开发React,但是在项目早期,我们还是忍痛去掉了这些代码,改为使用ES 6。毕竟随着ES 6乃至ES 7的普及,JS的标准已经变得越来越合理,CoffeeScript的生存空间似乎被压缩了。

在前端技术选型方面,我们经历了好几次演变。从CoffeeScript到ES 6,从Reflux到Redux,每次变化都在一定程度上增加了工作量。我在文章《技术选型的理想与现实》中讲述的就是这一事实。

在这篇文章中,我们选择了Reflux。然而现在,我们最终还是决定采用了Redux,不过我们也是到了最近才算真正用好的Redux。在后面的技术实践中,我会在介绍Redux的使用。

结论

技术负责人一个非常重要的能力要求就是——善于做出好的技术决策。选择技术时,并不能一味追求新技术,也不能以自我为中心,选择“我”认为好的技术。而应该根据产品的需求场景、可能的技术风险、团队成员能力,并通过分析未来的技术发展趋势综合地判断。
技术决策不可能一成不变,而需要与时俱进。如果发现决策错误,应该及时纠正,不要迟疑,更不要担心会影响自己的技术声誉。

2

技术团队的管理

技术团队的管理

下面进入第二个话题:技术团队的管理。

我曾经在我的微信公众号上写过一篇文章《我想要的研发团队》。在这篇文章中,我谈到了我理想中的研发团队特征:

对于那些追求卓越技术的团队成员而言,对于代码质量会有一种近乎偏执的追求,同时内含的Geek范儿,使得他们更乐于引入更新的技术以及更新的技术理念。于是矛盾出现:

如何权衡项目进度与代码质量之间的关系?
创业团队一个致命的问题就是要求产品研发要“快”,如果说还有另外一个要求,那就是要“更快”。像我们这样面向企业客户的产品,在竞争对手步步紧逼的情况下,出不来产品,就意味着赚不了钱,甚至于生存不下去。市场团队成员压力山大,自然就会把进度压力转移到技术负责人头上。

身为技术负责人的我,身为一位要求代码高质量的技术人的我,就需要在这二者之间权衡。我会尝试着去平衡这种矛盾,通过快速迭代、快速发布、明确划定MVP来缓解进度压力,进而为提升代码质量预留出时间与空间。换言之,在我能够承担的职责范围内,我可以放任那些具有自组织能力的Geek们放肆地玩技术!

然而,团队在对待进度与代码质量的态度上,没有发出一致的声音。于是,不幸地分成了两个阵营。不仅仅是进度的矛盾,在技术理念上也存在较大的分歧。正如各种语言粉喜欢互撕一般,在面对技术选型、代码重构等诸多方面,团队成员也开始了轻度的互撕。

为了这个,我没少费精力,直到现在没出大问题,估计还是我之前积攒的人品在发挥效果。

这就需要谈到我写的另一篇文章《团队文化的谜题》。在这篇文章我写道:

正所谓“江山易改,本性难移”,若要打造合理的团队文化,不建议去尝试改变团队成员的性格。若有人真与团队文化缺乏违和感,不如痛快地将其“踢”出团队。许多团队之所以僵化,就在于团队负责人舍不得壮士断腕,又或者对那种“食之无味弃之可惜”的鸡肋成员不能痛下“杀手”。当然,更为主动的方式是在组建团队时,挑选符合文化期望的成员。

我觉得我是未卜先知。

然而,知易行难,创业团队就这几个“宝贝”,开掉一个也不是件容易的事儿。何况任何事情没有绝对的“对”与“错”。站在管理者的角度来讲,我不是一个喜欢走极端的人。例如我们不能只追求进度,粗暴简单地把功能实现就完事儿,产品的外部质量与内部质量问题,其实都可能是“技术债”;但我们也不能太追求代码的内部质量,允许存在一些坏味道,只是为了快速实现。归根结底,还是成本与收益的考量!

这种矛盾在团队是公开的,但矛盾不是私怨,平日里大家一起吃饭,喝酒,倒也算是好哥们儿。何况,我认为你并不能指望你的工作同事都能成为你的朋友、兄弟!只要大家认可自己正在从事的事业,还保有创业的热情即可!

为了更好地消除不必要的矛盾,并及时改进团队的氛围以提高战斗力,我们会不定期的开展“回顾会议(Retro Meeting)”。例如在上一次的回顾会议中,我们针对技术决策的纷争,总结了几条原则:

3

研发团队总结的技术实践

我们的技术实践

第三个话题是介绍我们团队总结的技术实践。与大多数团队相比,因为我们使用了小众的Scala,就可以算得上是“捞偏门”了,所以总结的技术实践未必适合大家。这些实践也是在产品研发中逐渐演化的,没有专门总结,一些实践可能非常细节,也就是说可能很low,需要大家多多担待!

关于Scala

先说说Scala。两年前我还在ThoughtWorks的时候,与同事杨云(大魔头)在一个Scala的大数据项目,利用工作之余,我结合了一些文档整理了一份Scala编码规范,放在了github上。大家可以去看看:

https://github.com/agiledon/scalacodingconvention

我们的产品后端全部由Scala进行开发。对于编写Scala代码,我的要求很低,只有两点:

我们产品用的AKKA并不是太深入,仅仅使用了AKKA的基本功能。主要用于处理前端发来的数据分析消息,相当于一个dispatcher,也承担了部分消息处理的职责,例如对消息包含的元数据进行解析,生成SQL语句会,发送给Spark的SqlContext。分析的结果则以Future的方式返回给Spray。

也有几条小原则:

创业一年来经历的技术风雨|中生代技术分享第十七期

创业一年来经历的技术风雨|中生代技术分享第十七期

关于Spark SQL

目前的产品特性还未用到更高级的Spark功能。针对一些特殊的客户,我们计划采用Spark Streaming来进行流处理,除此之外,核心的数据分析功能都是使用Spark SQL。

以下是我们的一些总结:

sql SELECT UniqueCarrier,Origin,count(distinct(Year)) AS Year FROM airline GROUP BY UniqueCarrier,Origin

第二次执行的SQL语句:

sql SELECT UniqueCarrier,Dest,count(distinct(Year)) AS Year FROM airline GROUP BY UniqueCarrier,Dest

第三次执行的SQL语句:

sql SELECT Dest , Origin , count(distinct(Year)) AS Year FROM airline GROUP BY Dest , Origin

观察执行的结果如下所示: 创业一年来经历的技术风雨|中生代技术分享第十七期

观察执行count操作的job,显然第一次执行SQL时的耗时最长,达到2s,而另外两个job执行的时间则不到一秒。

关于React + Redux

关于React + Redux的使用,我们一开始并没有用好它。不过,随着对React + Redux的逐渐熟悉,结合社区的一些实践,我们慢慢体会到了其中的一些好处,也摸索出一些好的实践。

组件设计的原则

  • 一个纯组件利用props接受所有它需要的数据,类似一个函数的入参,除此之外它不会被任何其它因素影响;
  • 一个纯组件通常没有内部状态。它用来渲染的数据完全来自于输入props,使用相同的props来渲染相同的纯组件多次,
  • 将得到相同的UI。不存在隐藏的内部状态导致渲染不同。

Redux的三大基本原则

  • 单一数据源
  • State 是只读的
  • 使用纯函数来执行修改

在我们的项目中,将所有向后台发送异步请求的操作都封装到service中,action会调用这些服务。我们使用了redux-actions的createAction创建dispatch需要的消息: 创业一年来经历的技术风雨|中生代技术分享第十七期
在Reducer中,通过redux-actions的handleAction来处理action,避免使用丑陋的switch语句:创业一年来经历的技术风雨|中生代技术分享第十七期
在Container组件中,如果Store里面的模型对象需要根据id进行filter或merge之类的操作,则交给selector对其进行封装。于是Container组件中就可以这样来调用:创业一年来经历的技术风雨|中生代技术分享第十七期

Spray与REST

我们的一些总结:

4

Q&A

Q1. 为什么选择了Scala?
A1: 这个其实在分享中我已经介绍了原因。主因还是因为Spark吧。使用Spark的语言包括Java、Python、R以及Scala。我们的团队成员都不熟悉R,个别熟悉Python。Java的语法比较Scala而言,还是太冗长了。而且Spark的源码是Scala!

Q2. 您说您将来的产品要用clojure,处于什么方面考虑的?
A2: 只是随便这么一说。但是产品在将来需要提供DSL支持函数表达式。目前还没有开始设计,可选的语言包括Scala,但是现在没有确定。提到了Clojure,只是因为我们有团队成员是Clojure粉[Smile]

标签:Scala,中生代,技术,SQL,Spark,团队,我们,第十七期
来源: https://blog.51cto.com/u_15127580/2729492