十亿级流量的搜索前端,是怎么作架构升级的?

图片

导读:前端发展飞速,从最开始的静态页面到 JavaScript,再从 PC 端到移动端,随着大前端的复杂度不断提高,不少公司开始先后端分离,剥离出前、后端架构设计。那咱们来看看,前端架构设计是什么?曾经很是简单的前端架构发展到如今有哪些问题,遇到前端代码体量巨大、跨团队协做效率、代码耦合、技术栈落后等问题又该怎么解决?
前端

1、什么是前端架构?

前端架构这一词,相信不少人的定义都不太同样;按照拆词的解释来看,我理解为“前端”+“架构”。前端是指,Web 端的前台页面,包括网页的内容、样式、脚本等,这三者一般封装在组件中,多是模板引擎的文件模块,也多是 MVVM 框架里的组件。“架构”就更好理解了,架构一词来自建筑行业,能够理解是房屋的总体结构、框架。结合前端和架构的概念,“前端架构”能够理解为,Web 页面组件的抽象和组织方式。
后端

又由于各个公司的业务不一样,每一个公司的前端架构发展都不同,这里,我会拿百度移动端经典的搜索场景来给你们举例,但愿从百度的移动端架构演进过程当中,发现一些共性的问题。浏览器

2、百度移动端背景及问题
为何是以百度来举例?是由于百度是国内搜索引擎的领头人,而且,目前一直处于行业领先状态。据 statcounter 前瞻产业研究院在 2019 年中国搜索引擎行中能够知道,百度搜索占全世界搜索引擎市场份额12.3%,居第二位,仅次于谷歌。因此用百度来举例,更具备表明性。

言归正传,打开百度 App 你会发现,百度前端直接分为首页和搜索结果页,搜索结果页是搜索的主要入口,天天承载着十亿级流量。
安全

不只如此,搜索结果页承载着许多产品线的需求和下游模块的运行时,每一年内部的研发人员会提供五百多个产品需求,为十几个下游模块提供基础库和运行时。甚至还有后端协同,从图 1 咱们能够看出结果页的总体架构。前端框架

图片

图 1:百度搜索结果页的总体架构架构


针对总体的架构设计,有这些问题:app

  • 细分业务线众多,单个库代码庞大;框架

  • 平均每个月有 200+ 提交,3w+ 行代码;前后端分离

  • 80+ 开发者在同一个代码库中开发;ide

  • 没有人能彻底掌握模块总体技术。


因而,梳理出三个方面的问题:

1. 人员职责不清晰,单个模块同时承担了多个团队的职责

  • 框和 Tab:“所有”和垂类搜索共用;

  • 运营产品:***在结果页代码库里;

  • 其余:结果列表、用户反馈、搜索推荐、体验日志、速度日志、计费逻辑……


2. 代码耦合严重

  • 容易出错,代码逻辑脆弱;

  • 结构僵化,不易新增功能;

  • 依赖牢固,代码很难复用。


3. 技术栈落后

  • 页面没有组件化。没有 Vue、没有 React,还在用 Smarty 模板;

  • 没法支持 Node.js。Smarty 模板强依赖 PHP 环境;

  • 工具链落后。没有 TypeScript、没有 Jest。

这三个问题最终会影响到研发效率以及产品质量。那么百度又是怎么去具体作的呢?架构优化的目标只有两个,一是知足业务需求,二是技术上能对框架和工具灵活升级(也是为了持续的知足业务需求)。根据“知足业务需求”这一目标,百度内部是制定了三个层面的方向。(如图 2)

  • 底层基础层是贴近社区,由于据内部调研来看,造轮子的成本不高,可是维护这些轮子成本极高,若是想更快的迭代,仍是建议贴近社区,去用些开源的事情或者去贡献开源。主要是解决技术栈落后以及职责不清晰等问题。

  • 中间层是独立模块,主要是应对以前提到的职责不清晰的问题以及交付效率低等问题。主要是解决职责不清晰以及交付效率低等问题。

  • 顶层就是组件化,在独立模块的基础上去作组件化,加速业务的迭代。

图片图 2:业务需求的三个方向


3、怎么解决

根据这里提到的方向和目标,怎么结合百度本身的架构落地呢?首先,回顾下百度的架构,以下图 3 能够看到。

图片

图 3:百度搜索结果页的总体架构


1. 这里有两块日志,意味着同一套代码要在两个部分维护;除了重复以外,它们的差别会对后续的维护引入更高的成本;

2. 底层这个 HHVM+PHP 和社区更加拥抱 Node.js 会有冲突。

因此,百度同窗把目标架构调整为图 4 所示。

图片图 4:结果页的目标架构


图 4 中能够看到:

  1. 把日志、搜索框、相关搜索、性能打点等独立成单独的模块,有专门的同窗来独立维护和迭代;

  2. 在先后端之间加了一层渲染层;让业务代码和后端的逻辑分开;

  3. 在底层加了 Node.js 机制。

目标、方向都解决好以后,就得看如何实施。对于一个小体量的库来讲,从零构建架构就行;可是对于百度来讲,实施也是难点。不只要考虑平滑迁移、性能不退化,还要考虑长期可维护性、安全性、跨平台等。

前文也提到了,基本思路是按照基础设施、模块拆分、组件化的步骤执行;基础设施是业务模块划分的关键,完善的自动化和工具链是模块化的前提;模块化拆分能够为业务和团队提供更好的横向扩展能力;模块化的基础上,能够进一步在模块内部建设组件化方案来加速业务迭代。


在基础设施须要关注的事情包括:

  • TypeScript:大型项目必备,提早发现问题;也是跨平台的基础;

  • 持续集成:确保每次变动新增功能和修复问题的同时,不引入新的问题;

  • 单元测试:在重构之初引入,帮助防退化和辅助设计。


模块化拆分须要关注的事情包括:

  • 识别和定义业务边界,把大一统的仓库分割成若干独立的小仓库;

  • 在子模块内建设自动化机制,独立地选型、开发、上线。


注意:

模块化拆分不是技术问题,而是业务问题。只有根据业务和产品进行垂直划分,才有可能达到解耦和独立迭代的目的。不然只是形式上拆分耦合的代码,会形成更大的维护和沟通成本。

因为组件是业务模块内部的选型,组件化的方案相对比较自由。只须要不严重影响性能,且可以平滑过渡便可。


4、落地方案

1. 模块化

具体的落地方案,咱们也用一张图(图5)来表示。能够看到它分为服务端和浏览器端两部分。

  • 服务端关心的问题是业务模块的划分以及运行时的组合;

  • 浏览器端关心的问题是依赖的解决以及如何支持组件化方案。

图片图 5:具体的落地方案


2. 服务端

百度是把整个大模块拆分红多个独立业务模块,最终页面由模块组合而成。这要求业务模块具备统一的接口,即上图所示的 Molecule 接口,它定义了模块如何渲染、有哪些依赖等信息。由于渲染过程封装在了模块内部,因此整个架构能够支持多语言、多框架。

相信你也发现,Molecule 和微服务很是类似。它们的关键区别在于,微服务的服务之间经过 IPC 互相操做,且每一个服务能够独立伸缩、独立部署;而 Molecule 的各模块存在于同一个进程里。虽然有这样的区别,Molecule 仍然能够实现和微服务近乎相同的特性,如图 6 所示。

图片△图 6:Molecule 和微服务的比较

图 7 展现的是一个具体的业务模块的服务端入口文件,其中 ToptipController 是实现了由 Molecule 提供的控制器接口;这个接口要求提供一个渲染函数,接受一个字典类型的数据,返回渲染以后的页面内容。由调用方决定如何组装页面。

图片△图 7:具体的业务模块的服务端入口文件


如上是业务模块提供方的接口。此外 Molecule 机制还为调用方(组装最终页面的那一侧)提供了方便的接口,能够在须要引入子模块的地方,传入子模块名称和参数便可在运行时渲染出来。整个机制的原理很简单,但实际使用中可能还须要引入命名空间、考虑模块版本等问题。


3. 客户端

那么客户端如何运行起来呢?咱们也须要把每一个模块的浏览器端组件运行起来,困难在于组件之间的依赖和代码共享。这些组件可能位于不一样的代码库并属于不一样的业务,因此咱们须要一个很是松散的依赖方式。

这里咱们引入的是一个依赖注入的容器(图 8),总的来讲,框架逻辑和通用工具都封装成具体的Service提供给业务模块使用,每一个业务模块则须要定义它依赖于哪些Service。

图片△图 8:客户端设计

图 9 形象地描述了组件、Service 和容器间的关系。


图片△图 9:组件、Service 和容器之间的关系


其中蓝色表明具体的Service,其余颜色表示独立的业务模块。运行时容器会负责解决每一个业务模块的依赖,并把这些业务模块组装起来,最终获得可交互的 Web 页面。

注意:

业务模块之间是独立的,一个业务模块没法依赖于其余业务模块,只能依赖于通用 Service。所以若是存在业务模块之间的产品逻辑耦合,可能须要一个通用 Service 做为媒介,好比容器里提供一个起事件总线做用的 EventService。

图10是业务模块的客户端代码示例。它的依赖经过构造函数来声明,运行时容器负责依赖的建立,而业务模块只须要关心依赖的使用。正是使用和建立操做的分离,使得业务模块之间、业务模块和页面框架之间能够解耦,能够独立地开发、独立地测试。

图片△图 10:业务模块的客户端代码示例


以上是模块拆分的总体方案,咱们回顾一下:在服务端经过一个叫作 Molecule 的接口来组合业务模块;在浏览器端经过一个 DI 容器来解决依赖关系,并启动全部业务模块。


4. 组件化

组件化方案直接影响业务开发的的效率,换句话说,组件化方案某种程度上决定了业务同窗写怎样的代码。组件化也能够帮助解决职责不清晰等问题。咱们选的组件化方案是 San,你也能够基于你的业务或偏好选则 Vue 或者 React。业务代码的迁移比较直观,就是从 Smarty 模板迁移到 San 组件,从 HTML 字符串拼接变成有业务语义的组件结构。

接下来重点关注组件化方案的两个关键技术问题,跨平台和页面性能。


1)跨平台

咱们有很是多的业务代码,有上千个模板、几十万行代码,这些代码须要迁移到组件化方案上来,并且要确保后端从 PHP 迁移到 Node.js 的整个过程当中,业务代码不须要从新开发。因此业务组件如何跨平台呢?关键在于抽象。

  • 高层语言:咱们业务代码须要使用一个足够高层的语言,这里咱们用的是 TypeScript,能够翻译到多个平台;

  • 依赖反转:咱们的高层的业务的模块不该该依赖于具体的底层模块,而是它只依赖于接口,这样才有可能在不一样的平台给它替换掉不一样的底层的实现;

  • 抽象接口:最后是 Molecule 这个接口的设计应该足够的简单;Molecule 接口不依赖底层实现,好比 PHP 的具体 API。

作到以上几点就能够完成平滑的过渡。这个过程当中又分为三个阶段(图 11)。

图片△图 11:平台过渡的三个阶段


2)页面性能

引入前端框架一般意味着体积增长,性能降低,而性能直接影响搜索收入,所以页面性能是项目成功的关键。若是性能会比模板引擎的性能差,那么这个项目极可能会夭折。如何去保证页面性能?着重介绍两个优化点。

  • 引入 ***:引入服务端渲染,首屏性能能够获得明显提高;

  • *** 优化:传统的 *** 上还须要进一步优化性能。

引入***。为了解释***的重要性,请看图12。浏览器加载页面分为四步:请求页面、请求外链资源、执行脚本、渲染组件。从图中的对比能够看出,CSR在前面三步的时候,用户都是看不到页面的;而引入***以后,在第二步用户就能看到请求回来的页面。***它最大的一个用途就是提高首屏时间。


图片△图 12:CSR和***的比较


*** 优化。只是引入 *** 还不能让性能达到预期,由于相比于模板引擎直接拼接字符串,*** 须要递归渲染组件,尤为是递归 VNode 比较耗时。对此 San *** 相比于 Vue/React *** 作了不少改进。

  • 去 VNode:编译期递归 VNode,运行时只作 HTML 拼接;

  • 编译期计算:尽量把工做移到编译期,减少运行时开销;

图 13 展现了最终的 San *** 和改造前的 Smarty 模板引擎的性能对比。

图片△图 13:最终的 San *** 和改造前的 Smarty 模板引擎的性能对比


能够看到 Smarty 和 San *** 在不一样的场景会有不一样的表现,由于它们的渲染方式很是不一样。最终搜索结果页的组件化的 *** 上线以后,线上实验效果显示比 Smarty 要快 10ms左右。这个已是一个很不错的效果了,咱们用组件化从性能上战胜了模版引擎。


5、结语

针对百度搜索引擎在架构演化中遇到的问题,相信在其余领域也会有一些共性的东西。经过百度的解决思路,但愿能对正在作前端架构的你有一些启发。

Harttle

百度资深研发工程师,北京大学物理学学士和计算机科学硕士。2016年加入百度,曾负责和参与百度搜索Web极速浏览框架、MIP开源项目的研发,目前负责搜索结果页和搜索推荐业务。LiquidJS 的做者,贡献于San、Realworld Apps、hightlight.js、ALE、HTML5 Standard等项目。

阅读原文:十亿级流量的搜索前端,是怎么作架构升级的

更多干货、内推福利,欢迎关注同名公众号「百度Geek说」~