我们的技术实践

本文是我在中生代技术群分享的话题《创业一年经历的技术风雨》中的第三部分《研发团队总结的技术实践》。若要阅读第二部分《技术团队的管理》,请移步中生代技术群公众号。

与大多数团队相比,因为我们使用了小众的Scala,可以算得上是“捞偏门”了,所以总结的技术实践未必具有普适性,但对于同为Scala的友朋,或许值得借鉴一二。Scala社区发出的声音还是太小,有点孤独——“鹦其鸣也,求其友声”。

这些实践不是书本上的创作,而是在产品研发中逐渐演化而来,甚至一些实践会非常细节。不过,那个优秀的产品不是靠这些细节堆砌出来的呢?

Scala语言的技术实践

两年前我还在ThoughtWorks的时候,与同事杨云(大魔头)在一个Scala的大数据项目,利用工作之余,我结合了一些文档整理了一份Scala编码规范,放在了github上,链接地址为:https://github.com/agiledon/scala_coding_convention。

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

  • 写出来的代码尽可能有scala范儿,不要看着像Java代码
  • 不要用Scala中理解太费劲儿的语法,否则不利于维护

对于Scala编程,我们还总结了几条小原则:

  • 将业务尽量分布到小的trait中,然后通过object来组合
  • 多用函数或偏函数对逻辑进行抽象
  • 用隐式转换体现关注点分离,既保证了职责的单一性,又保证了API的流畅性
  • 用getOrElse来封装需要两个分支的模式匹配
  • 对于隐式参数或支持类型转换的隐式调用,应尽量让import语句离调用近一些;对于增加方法的隐式转换(相当于C#的扩展方法),则应将import放在文件头,保持调用代码的干净
  • 在一个模块中,尽量将隐式转换定义放到implicits命名空间下,除非是特别情况需要放到package object中
  • 在不影响可读性的情况下,且无需封装任何行为,可以考虑使用tuple,而非case class
  • 在合适的地方使用lazy关键字

AKKA的技术实践

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

几条AKKA实践的小原则:

  • actor接收的消息可以分为command和event两类。命名时,前者用动宾短语,表现为命令请求;后者则使用过去时态,体现fact的本质。
  • 产品需要支持多种数据源,不同数据源的处理逻辑放到不同的模块中,我们利用actor来解耦

以下是为AKKA的ActorRefFactory定义的工厂方法:

通过向自定义的工厂方法actorOf()传入Actor的名称来创建Actor:

  • 注意actor的sender不能离开当前的ActorContext
  • 采用类似Template Method模式的方式去扩展Actor
  • 或者以类似Decorator模式扩展Actor
  • 考虑建立符合项目要求的SupervisorStrategy
  • 尽量利用actor之间的协作来传递消息,这样就可以尽量使用tell而不是ask

Spark SQL的技术实践

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

以下是我们的一些总结:

  • 要学会使用Spark Web UI来帮助我们分析运行指标;另外,Spark本身提供了与Monitoring有关的REST接口,可以集成到自己的系统中;
  • 考虑在集群环境下使用Kryo serialization;
  • 让参与运算的数据与运算尽可能地近,在SparkConf中注意设置spark.locality值。注意,需要在不同的部署环境下修改不同的locality值;
  • 考虑Spark SQL与性能有关的配置项,例如spark.sql.inMemoryColumnarStorage.batchSize和spark.sql.shuffle.partitions;
  • Spark SQL自身对SQL执行定义了执行计划,而且从执行结果来看,对SQL执行的中间结果进行了缓存,提高了执行的性能。例如我针对相同量级的数据在相同环境下,连续执行了如下三条SQL语句:

第一次执行的SQL语句: SELECT UniqueCarrier,Origin,count(distinct(Year)) AS Year FROM airline GROUP BY UniqueCarrier,Origin 第二次执行的SQL语句: SELECT UniqueCarrier,Dest,count(distinct(Year)) AS Year FROM airline GROUP BY UniqueCarrier,Dest 第三次执行的SQL语句: SELECT Dest , Origin , count(distinct(Year)) AS Year FROM airline GROUP BY Dest , Origin

观察执行的结果如下所示:

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

  • 针对复杂的数据分析,要学会充分利用Spark提供的函数扩展机制:UDF((User Defined Function)与UDAF(User Defined Aggregation Function);详细内容,请阅读文章《Spark强大的函数扩展功能》。

React+Redux的技术实践

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

  • 遵循组件设计的原则,我们将React组件分为Component与Container两种,前者为纯组件。

组件设计的原则

  • 一个纯组件利用props接受所有它需要的数据,类似一个函数的入参,除此之外它不会被任何其它因素影响;
  • 一个纯组件通常没有内部状态。它用来渲染的数据完全来自于输入props,使用相同的props来渲染相同的纯组件多次,
  • 将得到相同的UI。不存在隐藏的内部状态导致渲染不同。
  • 在React中尽可能使用extends而不是mixin;
  • 对State进行范式化,不要定义嵌套的State结构,不同数据的相互引用都通过ID来查找。范式化的state可以更有效地利用Store里存储空间;
  • 如果不能更改后端返回的模型,可以考虑使用normalizr;但在我们的项目中,为了满足这一要求,我们专门修改了后端的API。因为采用了之前介绍的元数据架构,这个修改主要影响到了REST路由层和应用服务层的部分代码;
  • 遵循Redux的三大基本原则;

Redux的三大基本原则

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

在我们的项目中,将所有向后台发送异步请求的操作都封装到service中,action会调用这些服务。我们使用了redux-actions的createAction创建dispatch需要的消息:

在Reducer中,通过redux-actions的handleAction来处理action,避免使用丑陋的switch语句:

在Container组件中,如果Store里面的模型对象需要根据id进行filter或merge之类的操作,则交给selector对其进行封装。于是Container组件中就可以这样来调用:

  • 使用eslint来检查代码是否遵循ES编写规范;为了避免团队成员编写的代码不遵守这个规范,甚至可以在git push之前将lint检查加入到hook中:

echo "npm run lint" > .git/hooks/pre-push chmod +x .git/hooks/pre-push

Spray与REST的技术实践

我们的一些总结:

  • 站在资源(名词)的角度去思考REST服务,并遵循REST的规范;
  • 考虑GET、PUT、POST、DELETE的安全性与幂等性;
  • 必须为REST服务编写API文档,并即使更新;
  • 使用REST CLIENT对REST服务进行测试,而不能盲目地信任Spray提供的ScalatestRouteTest对客户端请求的模拟,因为这种模拟其实省略了对Json对象的序列化与反序列化;
  • 为核心的REST服务提供健康服务检查;
  • 在Spray中,尽量将自定义的HttpService定义为trait,这样更利于对它的测试;在自定义的HttpService中,采用cake pattern(使用Self Type)的方式将HttpService注入;
  • 我个人不太喜欢Spray以DSL方式编写REST服务,因为它可能让函数的嵌套层次太深;如果在一个HttpService(在我们的项目中,皆命名为Router)中,提供的服务较多,建议将各个REST动作都抽取为一个返回Route对象的私有函数,然后利用RouteConcatenation的~运算符拼接起来,以便于阅读:
  • Spray默认对Json序列化的支持是使用的是Json4s,为此Spray提供了Json4sSupport trait;如果需要支持更多自定义类型的Json序列化,需要重写隐式值json4sFormats;建议将这些隐式定义放到Object中,交由Router引用,而不是定义为trait去继承。因为并非Router都使用Json格式,由于trait定义的继承传递性,可能会导致未使用Json格式的Router出现错误;
  • Json4s可以支持Scala的大多数类型,包括Option等,但不能很好地支持Scala枚举以及复杂的嵌套递归结构,包括多态。这时需要自定义Serializer,具体做法可以参考我在知乎专栏上的文章,请通过点击「阅读原文」阅读。

整个技术分享内容包括产品的技术架构、技术选型与技术实践并非我一个人的体会,而是整个研发团队的知识荟萃,我只是将这些知识搬运过来介绍给大家罢了。所以还要谢谢我研发团队的兄弟们。

原文发布于微信公众号 - 逸言(YiYan_OneWord)

原文发表时间:2016-05-03

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏java一日一条

Jsoup代码解读之四-parser(上)

作为Java世界最好的HTML 解析库,Jsoup的parser实现非常具有代表性。这部分也是Jsoup最复杂的部分,需要一些数据结构、状态机乃至编译器的知识。...

871
来自专栏狮乐园

【译】Understanding SOLID Principles - Dependency Inversion

当我们在读书,或者在和一些别的开发者聊天的时候,可能会谈及或者听到术语SOILD。在这些讨论中,一些人会提及它的重要性,以及一个理想中的系统,应当包含它所包含的...

843
来自专栏Java架构

京东Java架构师讲解购物车的原理及Java实现

1)用户没登陆用户名和密码,添加商品, 关闭浏览器再打开后 不登录用户名和密码 问:购物车商品还在吗? 

3815
来自专栏美团技术团队

【你问我答】你与Java大牛的距离,只差这24个问题

点击上方“公众号”可以订阅哦 上周我们做了第一期“你问我答”活动,没想到有那么多读者进行了提问,受宠若惊。 问题比较多也比较杂,王锐老师很认真地给出了一些答案,...

42213
来自专栏java一日一条

有经验的Java开发者和架构师容易犯的10个错误(上)

首先允许我们问一个严肃的问题?为什么Java初学者能够方便的从网上找到相对应的开发建议呢?每当我去网上搜索想要的建议的时候,我总是能发现一 大堆是关于基本入门的...

672
来自专栏编程

使用JavaScript开发一个自修改代码

话说在25年前,我刚刚开始从事软件开发。在工作中,我遇到一个叫Dave的朋友,他曾在一家大型保险公司工作过几年,他的工作重点是开发支持一个名为“个人人寿保险”的...

2627
来自专栏Java学习网

最佳编码实践:搞砸代码的10种方法

 这是一篇提供有效、实用编程方法的程序箴言,作者Susan Harkins是世界最大的技术期刊出版社的主编,具有多年的实践经验;在这篇文章里她重申“最佳编码实践...

2644
来自专栏java一日一条

在Java 8下更好地利用枚举

在我们的云使用分析API中,返回了格式化过的分析数据(这里指生成分析图)。最近,我们添加了一个特性,允许用户选择时间段(最开始只可以按天选择)。问题是,代码中每...

971
来自专栏JAVA高级架构

两年Java程序员面试经

工作两年有余,本人第一份工作是在一家外包公司,第二份工作是在一家做SAAS平台的公司,第一家公司让我入门,进入了软件开发的行业,了解了一些基础的东西;第二家公司...

2432
来自专栏Jimoer

Java设计模式学习记录-责任链模式

 已经把五个创建型设计模式和七个结构型设计模式介绍完了,从这篇开始要介绍行为型设计模式了,第一个要介绍的行为型设计模式就是责任链模式(又称职责链模式)。

1132

扫码关注云+社区