相关阅读:
吊炸天!74款APP完整源码!
2016移动端Android新技术综合预览--好文不多,这一篇就足够
有了这些免费无限次的API 接口,再也不愁没有服务器开发不了APP了,也可以自己开发小程序了
从 14 年双十二结束开始接手淘宝首页,到如今差不多 1 年半时间,不久前完成了首页相关工作的交接。期间经历了两次改版和一次从 PHP 到 Node 的迁移,还是颇有感受,下面给大家分享下。
文章好像有点长,列个大纲会比较好:
一、相关背景介绍
淘宝首页是淘宝的门面,承载着几乎淘系所有业务的入口,流量很大,量级单位为亿。近几年无线端崛起,业务重点开始向无线终端偏移(目前不能叫偏移,基本以无线为主了),所以淘宝 PC 端首页的流量也有削减,不过即便如此,它的日均 PV 依然相当高。
淘宝首页一向是内部平台和技术的试验田,它一直在变化着。最新的框架和系统都会找淘宝首页试点,可以试想下,如果某一项需要推动的升级或者优化措施在淘宝首页已经上线,并且拿到了良好的数据和稳定性,其他业务还有什么理由不去尝试和更迭呢?同时,去年一年身在淘宝前端的技术架构组,自然而然也会主动去 push 一些实验性的内容到业务上。
淘系的站点页面包括首页、其他频道页和活动页等,这些页面并不都由淘宝前端一行一行的代码码出来,业务如此之多,这种玩法即便人数 double 也忙不过来。事实上,大多数页面都是依托内部的搭建平台——运营或者前端通过模块搭建的方式——构建的,而前端 focus 的重点在于搭建平台的建设自身以及模块的通用性和复用率的保障,当然,还有一些工程化的东西。
使用搭建平台搭建的页面,前端只需要考虑组成页面的原子模块的开发,整体的渲染由搭建平台提供的统一脚本全权负责。而在淘宝首页上,考虑到页面模块数量巨多,加上还有少量跨部门、跨团队的沟通,渲染模型略微不同。
二、淘宝首页的整体变迁
背景中提到,淘宝首页依托于内部搭建平台,它的变迁自然也是跟着搭建系统的变化而变化的。
1. PHP 下的淘宝首页
接手淘宝首页不久,便遇到了一年一度的改版,那时它还运行在 PHP 环境中。这里需要说明的是,淘宝首页的所有代码完全由前端掌控,前端不会直接跟数据库打交道,其数据来源分为两部分。
数据来源
一是 运营填写的数据。 采用前端挖坑的形式,预留坑位让运营获取填写数据,如(伪代码):
上面的代码会产生一份 PHP 的模板和 info 字段对应的表单坑位,这个过程简称「挖坑」。
运营填写这些坑位就会产生这份 PHP 模板对应的数据,最后渲染出来就是一个完整的 HTML 片段(实时性渲染)。
├── data.json# 运营数据的来源
└── index.php# 装载运营数据的 PHP 模板
旧版搭建系统中就是通过这种方式构造一个子模块。我描述得十分简单,但作为一个平台它需要考虑的东西还有很多很多的,比如数据顺序的控制、定时发布、回滚机制、过滤机制、筛选机制、数据的同步、数据的更新、版本控制、权限控制、其他系统的引用等等。
二是 后端或者个性化平台提供的数据。 不同的业务有不同的诉求。一些业务有自己的后端,他们要求使用自己业务产出的数据;有的业务希望用户看到的内容不一样,千人千面,期望接入算法;一些业务跟卖家直接打交道,期望使用招商数据;而有些业务期望采用运营从数据池筛选出来的数据…总之,淘宝首页需要对接形形色色的系统,接口繁多。后面会提到对动态数据源的整合。
并且这些系统对应的域名是不一样的,JSONP 格式自然也就成了首选。但一些特殊的系统,比如广告,它的渲染并不是一个简单的 JSONP 请求,可能它还要干预整个广告的渲染流程,比如加载他们的 JS,把渲染的控制权交过去。
页面的架构
上面介绍了数据的来源和子模块的结构,那么整个页面又是如何构成的呢?模块的搭建分为两种,一种是可视化搭建,运营或者前端可以将开发好的模块(或者模块库中选取的模块)拖拽到容器内,形成一个页面,
当然,上图也只是一个模型,作为一个系统需要考虑的问题还有很多很多,如页面的布局、多终端适配、模块的临时隐藏、位置调整、皮肤选择、模块的复制等等。
也可以通过如下源码搭建的方式(伪代码):
通过模块 id 将模块引入,并且添加一些类似lazyload的标记,方便控制渲染节奏和数据入口。源码搭建和模块搭建的区别在于,前者更易于控制模块的结构以及模块的渲染顺序。
动态数据源
首页面对一大堆接口和平台,对接几十个业务方,接口是个很大的问题,由于后台系统的差异,基本没有办法统一数据源的格式,一旦运营哪天心血来潮要换一个他自己觉得用的更爽的或者数据更好的系统,前后端估计又得沟通和对接几次。所以出现了下面这张图:
平台具备数据源接入的能力,也就是说我们挖的坑不仅仅可以让运营填数据,还可以从各种数据源中直接导入数据,当然,这里需要进行一次数据字段的映射转换。后端提供的接口是这样的:
{
"data": [{
"item_name":"name",
"item_url":"http://xxx",
"item_pic":"http://xxx"
}]
}
前端约定的接口形式是:
{
"info": [{
"name":"name",
"url":"http://xxx"
}]
}
那么系统必须提供这种映射的绑定策略:
info/name ->data/item_name
info/url ->data/item_url
绑定之后,数据既可以同步输出,也可以异步输出,这些都是平台提供的能力。这个方案基本上解决了后端系统/接口变化的问题,并且减少了前后端之间的沟通成本。
不过这里需要注意的是,虽然页面上的接口都通过平台统一梳理了一次,这也意味着,页面所有的请求会先流经平台,然后分发到各个后端,平台的抗压能力要求很高。
2. PHP 到 Node 的变迁
淘宝首页日均请求的这个量级,不可能是十几二十台台服务器抗得住的,支撑它必须有一个服务集群。
每一个 CDN 节点上都具备 PHP 渲染的能力,当页面发布时,我们把该页面所有的模块和数据同步到全部 CDN 节点上,基本模式大概就是如此了。看起来还挺不错,但是经过一段时间的运维,很多安全、性能问题都慢慢浮现出来了:
性能问题。 每个 PHP 页面包含多个子模块,而子模块也有可能引用了其他的子模块,PHP 的include操作是存在消耗的,每一次引用都是一次磁盘 IO,一个渲染节点上跑了成千上万个类似淘宝首页的 PHP 页面,并发一高其效率可想而知。
// @邦彦 同学补充:php 的 include 操作是存在消耗,但是加载、执行的过程预热后,字节码直接进缓存,并不存在频繁磁盘 io 的情况。cdn php 性能差的问题主要是两个:1. php 版本过旧,5.4 和 7 的性能相差不只几倍;2. fast-cgi 模式在高并发的场景下和 node 相比没有任何优势。
推送机制问题。 文件同步(图中的sync动作)是一种比较恶心的机制,首先,时间上没法控制,一个文件同步到所有的节点,快则几秒钟,慢的话耗时会超过一两分钟;并且同步过程还有可能失败,健康检测的成本也是相当高的。发布比较紧凑时,需要同步的文件也很多,很容易造成队列堆积,加剧同步差的体验。
实时性强需求问题。 文件在推送之前,还可能经过一些前置系统,发布链路越长,线上生效时间越慢,慢的时候大约五分钟才生效,这样的延时对于实时性要求很高(如秒杀)的需求来说是完全不能接受的。
当然,还有很多其他问题,如运维成本增高、安全风险增高、PHP 资深人才储备不足等等。所以 PHP 渲染容器的命运,就是,被干掉。
上图改变了下玩法,服务集群为 Cache CDN,它只有静态文件处理能力,没有 PHP/Node 的渲染能力,所以处理效率高,性能也好,抗压能力相当强,并且扛不住的时候还可以花钱买服务,拓展 Cache 集群。
用户访问时,Nginx 转到 Cache CDN,如果命中缓存则直接返回,没有命中便回源到源站服务器。源站服务器是具备模块渲染能力的 Node 服务,它可以做很多事情:
控制 Cache 响应头,通过max-age和s-maxage控制页面在客户端的缓存时间以及在 Cache 上的缓存时间,这个缓存时间可以根据需求随时做调整,比如大促的时候调长一些
控制内外网环境,和 AB 测试状态
融合前端相关的工具链,比如检测、压缩、过滤等等
它的优势有很多,这里不一一列举了。这个模式中还添加了一层容灾,源站服务器每隔一段时间将数据推送到于 Cache 同机房的备份服务器,一旦源站挂了,还能够自动容灾到备份数据上。
模式的变化不仅在运维上有了突破,CDN 被攻击时的安全风险也低了很多,同时也省却了 sync 所需的各种检测机制,每年节约成本也是百万以上,优势还是相当明显。
3. Node,不一样的模式
上面 PHP 模块中,我们只说了 HTML 和数据部分,用心的读者应该已经发现,CSS 和 JS 这些静态资源都没提到,那页面是如何渲染的呢?
旧版 PHP 页面中,我们是直接引入了一个 CSS 和一个 JS,淘宝这边采用的是 git 版本迭代发布,这些静态资源都是直接放在一个 git 仓库中。也就是这样:
每次发布完 git 文件,再修改 PHP 的版本号,然后发布 PHP 代码。当然,也做了相关的优化,比如发布 git 时自动更新版本号等。
而新版搭建平台的页面渲染模式与 PHP 的模式不太一样。
一个模块的 CSS/JS 和模板放在一起,CSS/JS 与页面其他模块的静态资源是相互独立的,目的就是希望单个模块也能够完整的跑起来,更加利于模块的复用。
而模块的挖坑,也从模板中独立了出来,采用 JSON Schema 的形式定义数据格式,
.
├──index.css# 模块样式
├──index.js# 模块渲染脚本
├── schema.json# schema 配置
└──index.xtpl# 模块的模板
搭建平台通过这个 JSON Schema 解析成图一的坑位。那么一个模块的渲染就变成了index.xtpl和挖坑数据之间的拼装了。
模块之间相互独立隔离,所以会存在一定程度的冗余,不过模块解偶带来的收益要比这点冗余要多得多。事实上,我们是通过一个仓库去管理单个模块的。页面的渲染就比较简单了,源站 Node 容器会将所有的index.xtpl合并成一个page.xtpl,为减少页面请求,css 和 js 也会 combo 成一个文件,如上图所示的http://cdn/??mod1.css,mod2.css,mod3.css。
任何模块的更新,页面都会有感知,下次进入系统时,就会提示是否需要升级模块和页面。这里内容比较多,我不细说,感兴趣的可以找我私聊。
三、淘宝首页的性能优化
首页模块众多,如果一口气吐出来,DOM 数量必然超过 4k 个,其结果就是首屏时间极长。按照 TMS 的开发规范,每个 TMS 模块都包含一个index.js和index.css,最后展示出来两个 combo 的 js 和 css。首页加载的时候也不会一口气执行所有index.js,否则刚开始页面阻塞会十分严重。
页面的渲染逻辑
每次数据请求都缓存到本地,并且为每个接口都提供一个硬兜底。还有一种方案是「重试」,请求一次不成功那就请求第二次。这方面的讨论具体可以看看之前写的这篇文章:《大流量的下兜底容灾方案》。
对于同步渲染,它只需要页面模板和同步数据,两者中任一种存在错误,源站都会报错,此时回源返回的内容就是一个 error 页面,状态码为5xx。这个错误不一定是开发者造成的,有可能是系统链路出现同步异常或者断路问题。针对这种问题,我给淘宝首页做了一个镜像页:
一旦接口请求失败,或者接口走了容灾逻辑,或者模块渲染超过 5s,控制台都会有黄色警报,当然此时,也已经向服务器发送了警报统计。
2. 接口 Hub
接口 Hub 是对数据请求的管理工具,如下图所示:
关于Java和Android大牛频道
Java和Android大牛频道是一个数万人关注的探讨Java和Android开发的公众号,分享和原创最有价值的干货文章,让你成为这方面的大牛!
我们探讨android和Java开发最前沿的技术:android性能优化 ,插件化,跨平台,动态化,加固和反破解等,也讨论设计模式/软件架构等。由一群来自BAT的工程师组成的团队。
关注即送红包,回复:“百度” 、“阿里”、“腾讯” 有惊喜!!!关注后可用入微信群。群里都是来自百度阿里腾讯的大牛。
欢迎关注我们,一起讨论技术,扫描和长按下方的二维码可快速关注我们。或搜索微信公众号:JANiubility。