背景京东事件系统是一个可以在线编辑、实时编辑更新、发布新事件、提供外部页面访问服务的系统。地址是http://sale.jd.com/***.html。其时效性高、灵活性强等特点极受欢迎,已发展成为京东数个重要的流量入口之一。在最近几次大促中,系统承载的PV都在数亿以上。随着京东业务的快速发展,京东活动系统的压力会越来越大。迫切需要更高效、更稳定的系统架构来支撑业务的快速发展。本文主要讨论主动页面浏览的性能。活动页面浏览性能提升难点:活动之间差异很大,不像产品页面有固定的模式。每个页面可以提取的公共部分有限,复用性差;活动页面内容多样,业务多样。依赖大量对外业务接口,难以实现数据闭环。对外接口的性能和稳定性严重制约着活动页面的渲染速度和稳定性;在本系统下经过多年的开发实践,提出了“异步页面渲染和页面浏览”的思路。整页数据放在redis或硬盘中,页面浏览就是从redis或硬盘中抓取静态页面,并以此为导向对系统架构进行升级改造。通过近几个月的运作,各方面的表现都有了明显的提升。在分享“新架构”之前,我们先来看看我们现有的web系统的架构现状。Web架构的发展与现状*浏览服务以京东事件系统架构的演进为例。具体的业务逻辑这里就不画了,只是简单描述一下架构。我们将在消耗性能的地方添加缓存。这里在一些数据库查询操作中加入了redis缓存。而全页redis缓存是在页面上进行的:由于活动页面内容量大,页面渲染一次的成本非常高。这里,可以考虑缓存整个页面呈现的活动内容。下次请求来的时候,如果缓存中有值,就直接拿到缓存中返回。以上是对系统应用服务级架构演进的简要说明。为了减轻应用服务器的压力,可以在应用服务器前面加CDN和nginxproxy_cache,降低回源率。除了整体架构(老)的“浏览服务”之外,老架构还做了另外两大优化:“接口服务”和“静态服务”1.访问请求先到达浏览服务,返回整个页面框架到浏览器(cdn、nginx、redis等各级缓存);2、对于实时数据(如秒杀)、个性化数据(如登录、个人坐标),使用前端实时接口调用,前端接口服务;3.静态服务:静态资源分离,所有静态js和css访问静态服务;4、重点:浏览服务和界面服务分离。页面固定部分使用浏览服务,实时更改和个性化由前端接口服务实现。接口服务有两种,直接读取redis缓存和调用外部接口。这里可以使用nginx+lua(openresty)优化直接读取redis的接口,不再详细说明。本次分享主要是关于“浏览服务”的架构。新旧架构性能对比在说新架构之前,我们先来看看新旧架构下的性能对比。*旧架构浏览服务性能突破CDN缓存和nginx缓存,源站到应用服务器流量约20%-40%。这里的性能对比只是针对本源到应用服务器的部分。浏览方式TP99如下(物理机)TP99大约1000ms,而且抖动很大,内存占用接近70%,cpu45%左右。1000ms内没有缓存,有阻塞甚至挂掉的风险。*新架构浏览服务性能本次2016618采用新架构支持。浏览TP99如下(分为app端活动和PC端活动)。手机活动浏览TP99稳定在8ms,PC活动浏览TP99稳定在15ms左右。全天几乎是一条直线,没有性能抖动。在新架构的支持下,服务器(docker)的CPU性能如下:CPU消耗一直稳定在1%,几乎没有抖动。对比结果:新架构TP99从1000ms降低到15ms,cpu消耗从45%降低到1%,新架构的性能有了质的提升。为什么!!!现在让我们揭开新架构的面纱。新架构的探索*页面渲染和页面浏览是异步的。纵观之前的浏览服务架构,20%-40%的页面请求会重新渲染页面。渲染需要重新计算、查询、创建对象等,导致CPU和内存消耗增加。TP99性能下降。如果能保证每次请求都能拿到redis的全页缓存,这些性能问题就不存在了。即:页面渲染与页面浏览是异步的。*直接改造后的问题及解决方案理想情况下,如果页面数据发生变化,可以手动触发渲染(页面发布新内容),外部数据变化可以通过监听mq自动触发渲染。但是有些对外接口不支持mq,或者不能使用mq,比如活动页面投放了一个商品,商品名称发生了变化。为了解决这个问题,view项目每隔指定的时间向引擎发起一次重新渲染请求——***内容放入redis。新内容将在下次请求到来时可用。由于activity比较多,无法判断访问了哪些activity,所以不推荐使用timer。通过添加缓存key实现,处理逻辑如下。优点是只为有访问权限的活动定期重新启动渲染。新架构说明*组织架构(不包括业务)视图项目的职责:直接从缓存或硬盘中获取静态HTML返回,如果没有返回错误页面(文件系统的访问性能比较低,超过100ms级别,这里不使用);根据缓存key2是否过期,判断是否重新向引擎发起渲染(如果你的项目对外接口支持mq,则不需要该功能)。引擎工程师职责:渲染活动页面,并将结果放在硬盘和redis上。发布项目,mq职责:当页面发生变化时,重新向引擎发起渲染。具体的页面逻辑这里不再赘述。Engine项目的工作是当页面内容发生变化时,重新渲染页面,并将整个页面内容放入redis,或者推送到硬盘。*View项目架构(redis版)View项目的工作是根据链接从redis中获取页面内容并返回。*查看工程架构(硬盘版)与Redis版相比优点:访问简单,性能好,尤其是在大量页面的情况下,没有性能抖动。单个dockertps达到700;缺点:严重依赖京东的redis服务。如果redis服务出现问题,所有页面将无法访问。硬盘版优点:不依赖任何其他外部服务,只要应用服务不挂,网络正常,就可以稳定外部服务;当页面数量少时,性能优越。单个dockertps达到2000;缺点:在页面数据量大的情况下(系统所有活动页面都有xxG左右),磁盘io消耗增加(这里使用javaio,如果使用nginx+lua(OpenResty),io消耗应该是控制在10%以内)。解决方案所有页面的访问和存储均采用urlhash方式,所有页面均分给各个应用服务器;使用nginx+lua(OpenResty)来使用nginx异步io代替javaio。*Openresty+硬盘版现在使用nginx+lua(OpenResty)作为应用服务,其高并发处理能力、高性能、高稳定性越来越受到青睐。通过上面的解释,viewproject是没有任何业务逻辑的。可以很方便的用lua实现从redis或者硬盘获取页面,实现更高效的web服务。通过测试对比,view项目读取本地硬盘比redis快(同样的页面,读取redis为15ms,硬盘为8ms)。因此,我选择使用硬盘和redis进行最新版本架构的备份,在硬盘无法读取的情况下读取redis。这里前端处理器的urlhash是自己实现的逻辑,引擎工程可以使用同样的规则推送到viewserver的硬盘。具体逻辑这里不详述。以后有时间我单独做一个分享。优点:具备硬盘版的所有优点,同时去掉tomcat,直接使用nginx的高并发和io处理能力;各种性能和稳定性达到最佳。缺点:硬盘坏了,影响访问;方法监控和日志打印需要用lua脚本重写。总结不管是redis版,硬盘版,还是openresty+硬盘版,其基础都是页面渲染和页面浏览是异步的。优点:所有业务逻辑剥离到引擎工程,新建视图工程理论上永远不需要上线;容灾多样化(redis、硬盘、文件系统),更简单。当对外接口或服务出现问题时,切断引擎项目渲染,无需更新redis和硬盘;新的视图项目与业务逻辑完全隔离,不依赖于外部接口和服务。推广期间,即使对外接口出现新的性能问题或对外服务宕机,视图项目也不会受到丝毫影响。正常访问;性能提升数百倍,从1000ms提升到10ms左右。详见之前的性能截图;稳定性:只要viewserver的网络还正常,理论上是可以不挂机使用的;可以大大节省服务器资源。按照这个架构,4+20+30=54个docker足以支持10亿级PV。(4个nginxproxy_cache,20个视图,30个引擎)作者:钱天行,2012年初加入京东,一直在京东审核、搭配采购、jshop活动系统等从事系统研发和架构工作项目。目前主要负责jshop活动系统架构升级,jshop数据中心计算架构设计。对构建高并发Web架构和高性能实时大数据运营有一定见解。在加入公司之前,他在传统电信行业有5年的开发和架构经验。【本文来自专栏作者张凯涛微信公众号(凯涛博客),公众号id:kaitao-1234567】
