一、概况4月21日,有赞举办首届“有赞科技发展日”活动。作为分享讲师,分享了过去一年有赞在Node方面的工作。实践经验。但是由于分享时间有限,只能把最重要的内容分享给大家,所以这个周末花了几个小时,结合那次分享,完善了一些内容,写了这篇文章,希望可以带给你新的灵感。二、Node基础框架的迭代演进1、从Koa到Astroboy(一)Koa+中间件有赞最早比较完整的Node项目是公司内部的一个管理系统。本系统采用Node全栈开发,主要包括HR员工管理系统和小伙伴APP。和大多数公司一样,我们第一个Node项目也是直接使用Koa,然后集成一些开源的中间件,这样可以快速搭建项目。做这个项目半年,基本把Node该踩的坑都踩了,于是开始尝试在外部产品上使用Node。我们尝试改造的第一个项目是公司官网。一个简单的项目,基本上没有什么大的风险。(2)脚手架项目模板对于第二个项目,我们不可能按照前面的方法简单的使用Koa加一堆中间件来搭建项目。因为我们已经有了之前的经验,所以我们整理出了这一套解决方案,提取了一个项目模板。对于每一个新项目,只要克隆模板并更改配置,就可以快速构建一个新项目。(3)阿童木1.0项目增多后,这种方式的弊端很快显现出来,因为模板代码和业务代码是耦合在一起的。如果要更改模板生成的代码,只能手动更新每个项目。随着时间的推移,保持同步变得越来越困难,每个项目的目录结构和代码风格可能会变得非常不同。因此,框架代码和业务代码的解耦非常重要。所以我们在脚手架模板的基础上提炼出一个叫做Astroboy的框架。这个框架是在Koa的基础上进行封装的。这样每个项目都是基于这个框架开发的。如果框架更新了,项目也只需要改变框架的版本号即可。(4)阿童木2.0很多项目都开始使用Node了,新的问题也出现了,因为每个产品的业务场景不一样,对框架的要求也不一样。比如某个中间件可能是A产品需要的,但B产品可能根本不需要,此时的框架不支持定制。所以对于框架,又提出了新的挑战,所以在今年年初,对框架进行了一次大的重构。本次重构基于Astroboy1.0,增加了很多新特性,主要有:基于Koa2开发,性能卓越提供基于Astroboy的上层框架定制能力高度可扩展的插件机制渐进式开发优先提供定制能力基于Astroboy的上层框架,如下图,有赞BaseFramework是在Astroboy基础上定制的最基础的NodeWeb框架。这一层主要集成了有赞的一些最基础的服务,比如:天网系统接入,是有赞内部的日志和业务监控系统健康检查。运维监控系统每5秒检查一次系统服务可用性和全链路监控。对于一个HTTP请求,一般会调用多个后端接口,对应的后端接口还会调用其他接口,所以整个调用过程其实是一个树状结构。如果遇到性能问题,找出性能瓶颈是很重要的,全链路监控就是为了解决这个问题。Dubbo服务调用接入,这个可以看下面关于服务化的介绍。有了有赞基础框架,我们需要在上面开展业务。业务场景分为两种:对于一些简单单一的业务,直接继承有赞基础框架开发即可;如果是一些复杂的业务,可以先在有赞基础框架的基础上定制一个业务框架。比如有赞原来有一个庞大的PHP项目(我们的名字是Iron)。服务拆分之后,Node承担了原来的PHP部分,所以我们鑫先定制了一个业务级的框架,叫做IronBaseFramework,然后按照业务模块(交易、门店、用户、营销)拆分成多个子项目。二是支持外挂。为此,请参考下面的插件说明。2.框架的几个核心概念以上介绍了有赞Node基础框架的迭代演进过程。下面主要介绍一下铁臂阿童木2.0框架的几个核心概念(1)Application应用的概念很好理解。这里,应用程序可以理解为一个项目,它继承自框架,实例化后就是一个实例。应用程序也是由插件一个一个组成的。(2)FrameworkFrameworkAstroboy框架是在Koa2的基础上进行封装的,框架的概念这里不再过多介绍。(3)Plug-inPlugin是软件设计中一个非常重要的思想。Eclipse等许多软件都支持此功能。插件可以解耦我们的系统。不会互相影响,这样的特性对于大型项目来说非常重要。插件是Astroboy框架的核心实现。它是服务(Service)、中间件(Middleware)和工具函数库(Lib)的载体。它本质上是一个NPM包,但它是基于NPM包的。以上,进行了更深层次的抽象。一个基于Astroboy的应用,是由一个个Plugins组成的。插件是我们手中的积木,这些积木通过Astroboy的框架引擎组织在一起,形成一个系统。那么插件和普通的NPM包有什么区别呢?插件约定一个目录结构,这样每个插件看起来都差不多,这对团队协作非常重要。如果每个模块看起来都不一样,那么团队协作的成本就会很高。应用启动后,插件的代码会自动注入到整个应用中。你只需要在插件的配置文件中启用这个插件。插件可以包含哪些信息?插件元数据,包括插件名称、版本、描述等;服务(Service)、中间件(Middleware)、实用函数库(Lib)等;Koa内置对象扩展,包括Context、Application、Request、Response等;插件管理和安装插件,可以使用npminstall命令,例如:npminstall[<@scope>/]@启用插件。安装插件后,需要启用插件才能使插件生效。启用插件也很简单,配置plugin.default.js即可。如果不同环境插件配置不同,只需要修改对应??环境(plugin.${env}.js)的配置即可,其中env表示Node运行时的环境变量,例如:development,test、生产等如下代码所示:'astroboy-cookie':{enable:true,path:path.resolve(__dirname,'../plugins/astroboy-cookie')}enable这个插件可以设置为true,并且path代表插件的绝对路径,这个一般适用于还在快速迭代中的插件。如果插件已经很稳定了,你可以将插件打包发布为npm包,然后通过包声明你的插件,如下代码所示:'astroboy-cookie':{enable:true,package:'astroboy-cookie'}禁用插件,禁用插件更简单,设置enable为false即可。三、Node接入有赞服务化体系的过程1、为什么要做服务化?随着公司业务的发展,网站应用规模不断扩大,垂直应用越来越多。应用程序之间的交互是不可避免的。将核心业务抽取出来作为独立的服务,逐渐形成稳定的服务中心,让前端应用能够更快地响应不断变化的市场需求。这个时候用于提高业务重用和集成的分布式服务框架(RPC)是关键,所以这个时候分布式服务架构势在必行。2、技术栈的选择在介绍技术栈的选择之前,先说一下公司的一些技术背景。在公司成立初期,为了能够快速开发,快速将产品推向市场,我们选择了使用PHP语言。我想这也是大多数创业公司的选择。随着业务的发展,PHP处理复杂业务的难度越来越大。所以等到一定的时候,我们就开始做服务拆分,所以首先要考虑的是底层技术的选择。我们考虑以下几点:第一,这个技术的生态是否足够完善,即相关的开源软件1、工具是否成熟;二是能否快速招到需要的人才。3、服务化拆分后,各层职责是什么?对于Node层,我们的定位是一个很薄的中间层。Node层并没有过多处理业务逻辑,所有的业务逻辑都交给了Java。它只负责以下三件事:模板渲染:模板渲染是指HTML模板的渲染;业务安排:对于稍微复杂一点的页面,通常需要将多个接口返回的数据进行聚合才能展示完整的页面,所以这种情况下,Node需要对多个接口进行聚合,然后将合并后的数据返回给前端.接口转发:Java服务不会直接暴露在公网供前端使用,所以这种情况下需要Node来承担接口转发的角色。至于Java层,需要承担业务逻辑、缓存等复杂的操作,这里就不过多介绍了。4、Node是如何调用Java接口的?那么服务拆分之后,首先要解决的问题就是:Node如何调用Java提供的接口。首先我们想到的是HTTP的方式。这里说明一下,我们公司采用的分布式服务框架是阿里开源的Dubbo框架,Dubbo框架本身支持通过注解的方式生成RestfulAPI。因此,在早期,我们使用的是这个现成的解决方案。随着应用数量的增加,这种方式的弊端也逐渐显露出来。要点如下:如果一个接口需要暴露给Node,需要手动添加额外的注解。每增加一个应用,运维需要为每个应用配置一个域名,不同的环境需要配置不同的域名。因此,随着应用数量的增加,应用域名的管理也越来越难维护。相应的,node也需要维护一个很长的域名配置文件。由于Java直接提供了HTTP接口,所以性能会低于RPC。所以,我们做了一些研究,看看在其他公司使用Dubbo框架时,Node是如何调用Java的。如下图所示:首先,Java应用服务启动时,会向服务注册中心注册服务。这里的服务注册中心可能是ETCD,也可能是Zookeeper。然后,当Node应用程序启动时,它会首先从服务注册表中拉取服务。服务列表,然后Node会和Java服务建立TCP长连接。此外,Node还需要负责Hession的协议解析和负载均衡。不难发现,这样一来,Node的职责就比较重了,对Node开发的要求也会很高。因此,我们对这种方式进行了改进,如下图所示:我们在Node和Java之间增加了一个中间代理层Tether。Tether是一个用Go语言编写的本地代理。Tether会对外暴露一个HTTP服务,对于Node来说,只需要通过HTTP调用本地服务,其他与服务相关的服务发现、协议解析、负载均衡、长链的建立和维护都由Tether来处理。这样一来,Node层就非常轻量级了。那么,最终实现的时候,Node是如何调用Java服务的呢?显示以下代码:constService=require('../base/BaseService');classGoodsServiceextendsService{/***根据商品别名获取商品详情*@param{String}alias商品别名*/asyncgetGoodsDetailByAlias(alias){constresult=this.invoke('com.youzan.ic.service.GoodsService','getGoodsDetailByAlias',[别名]);返回结果;}}module.exports=GoodsService;对于Node,调用Java为其服务只需要注意三点:服务名:服务名由Java包名+类名组成,比如上面的com.youzan.ic.service.GoodsService方法名:Java类暴露的方法,如上代码所示一种根据商品别名查询商品详情的方法getGoodsDetailByAlias参数:参数为传递给Java的参数列表最后总结一下该方法的优点:第一个是简单易用,对前端开发非常友好,只需通过HTTP二是多语言接入成本低。后面如果有其他语言(Python、Ruby)也需要接入整个服务体系,就像Node一样,只需要调用本地的Tether服务即可。Tether暴露的HTTP服务就足够了,没有额外的开发成本。三是后期优化协议层更方便,因为Tether实际上是通过这种方式进行代理。如果后期需要优化协议层的性能,只需要优化Tether的性能即可。那么,看到这里,可能有人又会想,这里Node也是通过HTTP调用Java的,是不是性能上有问题?所以这里我们做了一些优化,如下代码所示:constAgent=require('agentkeepalive');module.exports=newAgent({maxSockets:100,maxFreeSockets:10,timeout:60000,freeSocketKeepAliveTimeout:30000,});这里引用了一个agentkeepalive包。在HTTP早期,每次HTTP请求都需要打开一个TCPSocket连接,使用一次后断开TCP连接。使用keep-alive可以改善这种状态,即一次可以在不断开的情况下,在一个TCP连接中连续发送多份数据。因此,通过使用keep-alive机制,可以减少TCP连接的建立次数。4.参考资料https://github.com/apache/inc...https://github.com/QianmiOpen...https://github.com/QianmiOpen...https://github.com/p412726700...https://zh.wikipedia.org/wiki...