当前位置: 首页 > 科技观察

投稿-京东商品详情页响应“双11”大流量的技术实践

时间:2023-03-16 23:45:55 科技观察

来到京东打开商品页,通常会看到普通版、闪购、全球购等不同的页面风格,这里面会涉及到各种竖版模板页面的渲染。之前的解决方案是做静态的,但是做静态的一个很大的问题就是页面改版的时候需要重新全量重新生成新的静态页面。我们有数亿种产品。这么多的产品,需要跑很多天才能生成一个页面,有些突发情况是处理不了的。比如新的《广告法》需要清洗一些数据,后端清洗时间和成本来不及,所以很多时候都是从前端展示系统进行数据过滤。因此,需要一个非常灵活的前端显示架构来支持这个需求。首先,这是我们前端首屏的大致结构。第一屏有title,price,price,inventoryservice,servicesupport,extendedwarrantyservice等等,中心区的服务种类很多很多。而这么多的服务只是第一屏的一部分。如何在这个页面中集成这么多的服务,或者说在一个页面中把它做的非常非常好,是我们要解决的问题。而大家在副屏上看到的就是广告等等。这里会有品牌服务,因为京东有第三方商家,我们会提供广告位,叫商家模板。还有产品介绍、测评、咨询等多项服务,都在这个屏幕上。商品详情页涉及的服务商品详情页主要涉及以下服务:商品详情页HTML页面渲染价格服务促销服务库存状态/发货服务广告服务售前/秒杀服务评价服务试用服务推荐服务商品介绍服务对于一些类目相关的特殊服务,我们在详情页使用KV结构存储,但是是长尾的,即数据是离散数据。这样如果做一般的缓存,效率可能不会特别高,只会缓存一些热点,比如一些闪杀的产品,在缓存中才会有效。还有很多爬虫和一些软件会爬取我们的页面。如果您的缓存有问题,您的数据将很快从缓存中清除。因此,设计时应考虑离散数据问题。最早的时候,我们的商品详情页使用的是.NET技术,但是随着商品数量的增加,商品数据库结构设计复杂度的变化,我们后来生成静态页面,通过JAVA生成页面分片,比如产品介绍等一一送达。我们在这一层其实遇到过很多问题,比如这里会生成很多小文件。如果你的磁盘使用EXT3或其他小文件,它会被INODE限制。还有一个问题是,我们在生成这样的页面碎片时,如果页面整体风格发生变化,往往会涉及到全量的数据刷新。比如要支持单品闪购。对于这种情况,我们需要将所有闪购页面重新生成为静态页面。如果我们的业务变化很快,说这个页面不是我想要的,我们就需要重新生成静态页面,重新刷新。这对于几万种商品来说是没有问题的,但是现在我们的商品规模已经非常大了。在这种情况下,依赖系统可能会崩溃,因为你会调用很多依赖方。假设我们现在依赖二十个,每个页面需要调动二十多个源来获取相应的数据。后来我们发现了这个问题。其实最主要的是页面模板变化的速度不能满足我们的需求;另外,我们做静态页面的机械盘在遇到大流量的时候会非常非常慢。后来我们做成动态的,通过JAVAWorker把数据存到KV存储中。前端是Nginx+Lua,让模板全是动态数据。这个架构我们已经在线运行一年多了,整体性能非常稳定。平均响应时间在50毫秒以内,基本可以维持在30-40ms左右。对于这套设计,可以非常快速地响应变化的需求。我们有一个依赖于许多服务的异构产品详细信息页面系统。我们用它来抓取相关的数据源,同步worker会根据维度聚合数据。有产品维度和其他维度,比如产品介绍、分类、商家、品牌等。我们分别存储这些尺寸。例如展示商品详情页时,读取商品信息,商品相关信息:分类、商户、品牌等,然后渲染页面;并且只是把产品介绍读出来吐出来。其实这个的本质也是一个静态的思想,就是让数据静态化,而不是页面静态化。这样做的好处是可以随时更改页面模块。另外,你只需要保证数字被原子化即可。雾化就是你没有对它进行再加工,这样它就可以被重复使用和加工。商品详情页统一服务体系的建立商品详情页异步加载的服务比较多,所以我们建立了统一的服务体系。为什么要做这个系统?我们的目标是页面上访问的所有请求或服务都必须通过我们的系统。Monitor,监控每项服务的服务质量;随时通过我们自己的交换机做一些降级处理。比如推广速度慢,可以随时降级,保证后台服务不会被异常流量冲击。本系统前端采用Nginx+Lua。数据异构系统。像我们的库存,大家可能看到我们的库存和淘宝的库存不一样。因为京东有自营和第三方的,你看库存会显示有没有货,有没有预定,第三方可能会有运费的概念。第三方也有发货时效问题,比如你买了多少。几天内发货。对于这些数据,我们可以做异质性。异质之后,只靠自己,不靠别人。如果别人的服务有问题,抖动或者响应慢,对我们没有影响。核心设计思想?异类思想。我们按照自己的维度存储别人的数据,或者按照我们要消费的数据的格式存储。存储之后,我们只消费自己的数据,不依赖别人的数据。相当于别人的界面抖动怎么影响到我。像双十一一样,我们有一个集群。比如某个商品挂了,前端还能提供服务,但是数据不会更新。再比如双十一期间,有些产品没有更新,却需要闪杀。我们可以通过前端逻辑处理在系统中手动标注它们。?服务闭环的思想。假设我们在设计页面的时候有很多依赖别人的服务,出现问题的时候一定要第一个找到我们。找我们的时候,我们需要联系其他部门,会有沟通上的问题。如果我们能及早发现这个问题,并进行预先计划的解决方案,比如降级,如果库存有问题,第一时间告诉我们,我们可以降级为全部有货,让大家有货买,形成服务闭环。所有服务接入均通过我们的系统接入,发现问题及时降级。?维度存储。在存储数据的时候,我们是按照维度来存储的。然后我们根据使用方法获取。比如我们进行一个详情页,只需要获取两次,一次是获取商品信息,一次是获取业务分类等等。统一接入层和代理层统一入口,形成闭环。所有的访问都是通过我们的系统访问的,这让我很容易在出现问题时找到他们。做监控。比如这个接口响应慢,我可以监管我的依赖业务。还有缓存前端,前端有5-10秒的缓存,这个时间大家都能忍。我们把缓存放在前面,我们的Nginx+Lua,它的并发度很高。预缓存后,很多流量无法引导到你的业务层;也就是我们尽量让流量在前端处理,不到达我们的业务层。对于业务前端,比如库存打包,我们会在Nginx+Lua中做一些简单的处理。做一些简单的数据处理,比如一些非法导入的数据,都会在这一层被过滤掉。新版本测试。比如我们做了延保服务。我想知道它之前和之后是如何工作的。我需要对某些人使用A版本,对某些人使用B版本,这在我们的层面上是可以实现的。比如根据用户的ID,或者用户每次访问,都会使用UUID。而这里通过Nginx+Lua,部分程序是通过Lua编写的,AB测试由这里的程序控制。其他的引流、发布、流量切换等都在这一层完成。比如我们上网的时候,会有一些开关的概念。在Nginx+Lua层,我们会写代码。50%的用户会用到新版本,然后一步步慢慢添加,大部分流量控制在我们前端。做一些线上压测,通过Lua协程机制,将一个请求分成两个并发请求发送到后端,然后你做一些逻辑验证。降级开关前置,用于监控服务质量和限制电流。我们在实践的时候,会做服务隔离。为什么要隔离?这很简单。假设您在其中一个系统中进行http调用而忘记设置超时时间。这时候流量大的时候,http服务就会出现问题,可能会导致应用挂掉。所以我们在设计的时候,会对我们的业务进行分类,在一个应用中对业务进行分类:0级业务,1级业务;比如库存,需要库存的地方,没有这个业务,页面不会进行下一步,我们设置为0级服务;而如果没有延保服务,则不受影响,我们设置为1级。这里使用servlet3异步。通过异步,我们接收请求并将它们存储在隔离池中。然后将这些池的请求相互隔离。如果一个池子出现问题,不会影响另一个池子。.之前做的时候其实也遇到过。比如开发试用报告的时候,没有超时时间,我们的应用就挂了。部署和组隔离。比如我们有一个业务,这个业务可能很多人依赖我,我就可以分组。A部门调这个组,B部门调那个组。你为什么这么做?因为你不能保证每个人都会按照你的流程去做。比如压测没有告诉你,导致你不加流量等等。对于这种情况,我尽量分开。如果你这样做,它不会影响其他人。分组就是不同的部门呼叫不同的组,或者根据呼叫者的级别进行不同的分组。归根结底,假设一个应用涉及到的服务很多,但是这些服务又特别重要,比如价格可能一天上百亿,那么这个时候可以单独做一个服务。促销、库存等可以作为服务分离出来。如果前期没有问题,大家会更多的时候把它做成一个大项目。当一个大项目重启时,会出现抖动,抖动是针对所有服务的。所以我们需要拆分应用隔离。对于分布式缓存,使用最广泛的可能是Redis和Memcached。这里我们的前端Nginx会用到一致性哈希的概念,比如通过分类进行一致性哈希,这样就可以一致性地哈希到不同的Nginx实例上来提高攻击率。此外,一些错误数据或一些自下而上的数据不会被缓存。对于突发流量,我们使用更高效的缓存。最有效的方法就是把数据拿到你这边缓存,这样数据就在你的掌控之中。还有,如果你一套数据在一个机房,那么就没有跨机房了,整体效率可能会提高。这里用的最多的就是多级缓存,先做本地缓存,本地缓存没有key就去分布式。此外,我们还会做一些自动降级处理。对于一些不是特别重要的东西,我们会根据超时时间自动降级,比如第三方的发货时限。如果这些信息在几秒钟或几分钟内没有显示给用户,则不会影响他的购买,对于这种数据,我们会做一个,比如超过500毫秒或者200毫秒,就会自动降级,也就是不会输出这个数据。还有一些数据是不能降级的,比如价格。如果没有数据,页面可能是空的,我们不会缓存它。还有库存,我们不能做大缓存。另外,我们尽量减少回源量,就是使用一致性哈希。我们还将使用非阻塞锁和304响应。比如304响应适合一直点击刷新按钮,此时一些异步加载的数据不需要向服务器请求重新计算。这时候适合设置过期时间,比如10s,10s内全部返回304。还有一些恶意访问,只能多提高我们的抗恶意能力。比如我们通过KV存储数据,这样在KV***的情况下就不怕刷了,因为我们的流量是够用的,除非他们占满我们的带宽。再一个就是提高缓存效率,减少回源的影响。另外,我们会考虑将一些恶意流量分流给另外一个群体,也就是给一些恶意用户使用,即使能用,但是速度慢。N页之后的请求也有特殊处理。比如在访问一个列表的时候,像大家访问的前十页比较多,可以对后十页做一些特殊的处理,比如限速。比如这个服务是正常10毫秒出来的,我放在100毫秒,我们在Nginx上都是这样的,让他拖慢刷你的速度。还有一些数据是我们的底线,一个是静态的。比如我们将前几页的数据静态化。如果服务宕机了,我们可以把这个静态数据带给大家,这样大家就看不到503页面或者404页面的状态。也没有办法做缓存,也就是说我们没有降级的计划。对于降级,我们有两种:***,手动降级。比如一些库存,我们人工去监控这种服务,我们后台会有一个报警系统,比如多少毫秒会有一个报警,人工去控制。还有自动降级。刚才说了过段时间降级,流量大的时候会自动降级,因为你的系统扛不住流量,不然就挂了。我们这样做是为了让一些用户可以使用它,并降级一些。还有就是连接池的超时。如果不设置或者设置太大,一般访问没有问题,但是一旦出现异常情况,比如网络抖动或者其他情况,你的整个系统就可能挂掉。还有重试的时间和次数。什么时候重试,第一次访问已经挂了,然后第二次、第三次访问,其实这个请求是没用的。通过循序渐进或循序渐进的方式慢慢恢复。还有就是CDN回源,我们做了版本控制,现在测评也是版本控制,为什么要做版本控制?因为双十一的评价量非常非常大,如果直接追根究底,是处理不了的。所以我们现在对评估进行了版本化。有了版本号,这个页面就可以长期缓存,比如可以缓存一两天;如果没有版本号,只能缓存几分钟,然后返回源。对于这种方法,可以更有效地完成CDN缓存。爬虫不回源,不允许在后台服务。返回历史数据,非阻塞锁。这里将进行监控和报警。首先我们要知道系统的状态,还要应用实例的存活,调用次数,响应时间和可用率。如果来电量大,可能会有恶意的人刷你,需要提前提醒。如果这个down了,可能是你依赖的服务有问题,需要查看是哪些有问题。对于日志,我们看到更多的是Nginx的访问日志,更多的访问日志是IP或者其UA。通过阅读这些信息,你可以知道哪些是爬虫,哪些是恶意访问,哪些是正常的。流动。当出现问题时,你可以通过其他机制进行干预或拒绝,不让他问。还有应用日志,因为这里会写业务代码,所以可以看到。还有应用程序日志。对于应用来说,业务日志和异常日志比较多。其实我们在发现问题的时候更多的是通过日志发现的,有的是在开发中。记录日志的时候,没有任何意义,只有一个,出了问题,我们也不知道出了什么问题。所以,我们在内部的时候,要求一些日志记录的很清楚,什么问题,发生在什么地方,有什么异常都要记录下来。对于更重要的议程,请直接向警方报告。监控日志将使用呼叫量、响应时间和可用性。我们在搭建系统的时候,一定要进行压力测试。最好的是吞吐量压力测试,这取决于你系统的最大压力测试。为此,我们可以按一个URL。这种方法有一个很大的问题。如果是单个URL,那肯定是热点,热点压力意义不大。另一种被广泛使用的方法是复制真实的线上流量,然后直接在线压测。我们直接把线上流量引到一台进行压测,测试你的极限。还有页面埋点。测压时要考虑是读压还是写压,还是读写压。压力测试时,读写性能非常好。一旦读写混在一起,它就会在某个点上抖动,它的反应会非常非常慢。比如有人压测的时候,顺序很好。一旦是离散的(所谓离散就是有人访问1,有人访问2,访问没有先后顺序,这就是离散)。在压力测试的时候,你需要知道你的压力测试场景是什么样的?还有其他的,就是响应头记录服务器真实IP,前端JS瘦身,业务逻辑服务后端,接入层数据过滤,数据校验,缓存前端,部分业务逻辑前端,智能DNS,reduce跨机房调用提供数据刷新接口,更新或删除异常数据,并发提升性能。我们这里用的很多。一个产品页面拉取数据的时候,调整了十几二十个接口。这些接口都有规则,就是先获取产品,再获取其他的。这些接口可以并行调用。如果之前的调用需要1-2秒,我们通过并发改进了300-400毫秒。作者简介:张凯涛,京东高级Java工程师,2014年加入京东,主要负责商品详情页和统一服务架构及详情页开发。设计开发了多个访问量上亿的系统。工作之余喜欢写技术博客。有《跟我学 Spring》、《跟我学Spring MVC》、《跟我学Shiro》、《跟我学Nginx+Lua开发》等系列教程。目前,该博客的访问量超过460万。