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

京东的商品详情页服务闭环实践

时间:2023-03-13 21:40:40 科技观察

京东的商品详情页技术方案之前已经揭秘《构建需求响应式亿级商品详情页》本文已经为大家揭晓,接下来我将揭秘亿级流量下商品详情页的统一双十一活动的服务架构,这次双十一整个商品详情页面没有出现停服的情况,服务非常稳定。统一服务提供:促销广告词组合服务、库存状态/送达服务、延保服务、试用服务、推荐服务、图书相关服务、详情页优惠券服务、今日底购服务等服务支持;这些服务包括我们自己做服务实现,有的只是做代理或者接口合并输出到页面。将这些服务聚合成一个系统的目的是打造一个服务闭环,优化现有服务,为未来需求做准备。跟着围棋走自己的方向,不要被别人乱了阵脚。你在页面上看到的c.3.cn/c0.3.cn/c1.3.cn/cd.jd.com请求都是统一服务的入口。为什么需要统一服务?产品详情页虽然只有一页,但是依赖它的服务有很多。我们需要控制入口并统一管理。这样做的好处是:统一管理和监控,出现问题可以统一降级;可以将一些相关的接口组合输出,减少页面的异步加载请求;有了它,所有的入口都在我们的服务中,我们可以更好的监控和思考我们页面的服务,从而运筹帷幄,决胜千里。在设计一个高弹性的系统时,想一想出现问题时怎么办:能不能降级,不能降级怎么处理,会不会滚雪球问题,如何快速响应异常;系统核心逻辑的完成只是为了保证服务能够工作,如何让服务更好更高效,或者说异常情况下如何正常工作也是我们需要思考和解决的问题深度。整体架构的大致流程:1.请求首先进入Nginx,Nginx调用Lua进行一些前置逻辑处理。如果前置逻辑无效,则直接返回;然后查询本地缓存,如果***则直接返回数据;2、如果本地缓存不是***数据,则查询分布式Redis集群,如果是***数据,则直接返回;3、如果分布式Redis集群没有***,会调用Tomcat进行回源处理;然后将结果异步写入Redis集群,并返回。以上就是整个逻辑过程。可以看到我们在Nginx层做了大量的前置逻辑处理,以减轻后端的压力。另外,我们的Redis集群部署在机房,如下图:即数据会写入一个主集群,然后以主从的方式将数据复制到其他机房,每个机房读取自己的集群;在这里,每个机房没有单独的集群,保证机房之间没有交叉访问。这样做的目的是保证数据的一致性性。在这个新的架构中,我们可以看到Nginx+Lua已经是我们应用的一部分了。在实际使用中,我们也是将其开发为项目,部署为应用。一些架构思路和结论我们主要遵循以下原则来设计系统架构:两种读服务架构模式本地缓存多级缓存统一入口/服务闭环接入层引入前端业务逻辑后端前端interface服务器聚合服务隔离二读取服务架构模式1.读取分布式Redis数据架构,可以看到Nginx应用和Redis是分开部署的。这种方式是一般应用的部署方式,也是我们统一服务的部署方式。这里会有跨机和跨交换机。或者跨机柜读取Redis缓存,但是不会出现跨机房的情况,因为数据是通过主从复制到各个机房的。如果对性能要求不是很高,可以考虑这种架构,更容易维护。2、读取本地Redis数据结构,可以看到Nginx应用和Redis集群部署在同一台机器上,可以杜绝跨机器、跨交换机或跨机柜调用,甚至跨机房电话。如果本地Redis集群不安全,则回源到Tomcat集群取数据。此方法可能会受到TCP连接数的限制。考虑使用unix域套接字来减少本机TCP连接的数量。如果单机内存成为瓶颈(比如单机内存最大256GB),则需要路由机制进行分片。例如,根据产品尾号分片,Redis集群一般采用树状结构链接主从部署。本地缓存我们使用Nginx作为应用部署,所以我们使用了大量的Nginx共享字典作为本地缓存。在Nginx+Lua架构中,HttpLuaModule模块的shareddict作为本地缓存(reload不丢失)或者内存级别的ProxyCache,提高了缓存。性能并减少带宽消耗;此外,我们使用一致性哈希(如商品编号/类别)进行负载平衡,以在内部重写URL以提高效率。我们在缓存数据的时候,使用维度来存储缓存数据,增量获取无效的缓存数据(比如10条数据,3条没有安装到本地缓存,只需要取这3条);商家信息、店铺信息、商家评分、店铺表头、品牌信息、类目信息等维度;例如,如果我们在本地缓存30分钟,调用次数将减少近3倍。另外,我们采用一致性哈希+本地缓存,比如库存数据缓存5秒,通常的攻击率:本地缓存25%;分布式Redis28%;回源47%;普通秒杀活动攻击率:本地缓存58%;分布式Redis15%;返源27%;某服务使用一致性哈希后,攻击率提升10%;URL按照规则改写为缓存键,并随机化,即无论页面URL如何变化,都不要让它成为缓存失效的因素。多级缓存对于读服务,我们在设计上会采用多级缓存,尽量减少对后端服务的压力。在统一服务体系中,我们设计了四级缓存,如下:1.1.使用Nginx本地缓存,这个前端缓存的主要目的是为了抵御热点;根据场景设置缓存时间;1.2、如果Nginx本地缓存不完善,那么它会读取各个机房的分布式slaveRedis缓存集群,缓存主要是保存大量离散数据,抵抗大规模离散请求。例如,使用一致性哈希构建Redis集群。即使其中一台机器发生故障,也不会出现雪崩;1.3.,Nginx将返回Tomcat;Tomcat首先读取本地堆缓存,主要用于支持一次请求多次读取一条数据或与数据相关的数据;其他情况下攻击率很低,或者缓存一些规模小但使用频率很高的数据,比如分类、品牌数据;我们将堆缓存时间设置为Redis缓存时间的一半;1.4、如果Java堆缓存不完善,会读取主Redis集群,一般情况下,缓存攻击率很低,不到5%。读取缓存的目的是防止前端缓存失效后大量请求涌入,导致我们后端服务压力过大而雪崩;我们默认开启,虽然这个缓存会增加几毫秒的响应时间,但是它加厚了我们的防御屏障,让服务更加稳定可靠。可以在此处进行改进。比如我们设置一个阈值,超过这个阈值我们才会去读主Redis集群。例如,Guava有RateLimiterAPI来实现它。统一入口/服务闭环在《构建需求响应式亿级商品详情页》中,我们已经讲过数据异构闭环的好处。我们在统一服务上也是遵循这个设计原则。这里我们主要做两件事:1.数据异构,比如判断我们对库存状态依赖的包和配件进行了异构。未来商户运费等数据可以异构,减少接口依赖;2.服务闭环,单个产品页面使用的所有核心接口都接入统一服务;有的是检查library/cache然后做一些业务逻辑,有的是调用http接口然后进行简单的数据逻辑处理;有的是做一个简单的代理,监控接口服务质量。介绍Nginx接入层。在设计系统的时候,我们需要尽可能前置一些逻辑,以减轻后端核心逻辑的压力。此外,服务升级和服务降级之间的切换也非常方便。在接入层,我们做了以下事情:数据校验/过滤逻辑前端,缓存前端,业务逻辑降级切换前端AB测试灰度发布/流量切换监控服务质量限流数据校验/过滤逻辑前端我们的服务有两种类型接口类型:一种是与用户无关的接口,一种是与用户相关的接口;所以我们使用c.3.cn/c0.3.cn/c1.3.cn和cd.jd.com两种域名;当我们请求cd.jd.com时,会向服务器发送用户cookie信息;在我们的服务器上,请求头会被处理,所有与用户无关的数据都会通过参数传递,所有数据在访问层都会被丢弃请求头(保留gzip相关的头);而用户相关的会从cookie中提取用户信息,通过参数传给后台;即后端应用从不关心请求头和Cookie信息,所有信息都是通过参数传递传递的。请求进入接入层后,会对参数进行校验。如果参数验证无效,则直接拒绝该请求;我们对每个请求的参数都进行了最严格的数据校验处理,确保数据的有效性。如下图,我们过滤了关键参数,如果这些参数不合法,则直接拒绝请求。另外,我们会对请求的参数进行过滤,然后按照固定的模式重新组合URL,派发给后端应用。此时,URL上的参数是固定的、有序的,可以根据URL进行缓存。缓存前端我们把很多缓存前端放到了访问层来切掉热点数据的峰值,配合一致性哈希来提高缓存的效率。在缓存的时候,我们根据业务设置缓存池,减少相互影响,提高并发度。我们使用Lua读取本地缓存的共享字典。在业务逻辑前面,我们直接在接入层实现了一些业务逻辑。原因是当峰值出现问题时,可以在这一层做一些逻辑升级;我们后台是一个Java应用,修复逻辑的时候需要在线。应用程序上线后可能需要几十秒才能启动。应用重启后,Java应用的JIT问题会出现性能抖动问题,可能会导致服务因重启而一直无法启动的问题;而在Nginx中这样做,改完代码后推送到服务器后,只需要秒重启,不存在抖动的问题。这些逻辑都是在Lua中完成的。降级开关前端我们的降级开关分为几种:接入层开关和后端应用开关,主控开关和原子开关;将开关设置在接入层的目的是防止应用降级后流量无谓的打到后端;通用开关是将整个服务降级,例如库存服务默认为有货;而原子开关是对整个服务中的一个小服务进行降级,比如需要在库存服务中调用商户发货服务,如果只有商户发货服务有问题,此时只有商户运输服务可以降级。另外,我们还可以根据服务的重要程度,使用超时自动降级机制。我们使用init_by_lua_file来初始化switch数据,sharedictionary存储switch数据,提供switch切换的API(switch_get(“stock.api.not.call”)~=“1”)。可以实现:秒级切换、增量切换(可以根据机器组开启,不是全部开启)、功能切换、细粒度服务降级切换、非核心服务可以随时间自动降级。比如双十一的时候,我们的一些服务出现了问题。我们对大大小小的服务进行了降级,这些操作用户是察觉不到的。AB测试对于服务升级,最重要的是会做AB测试,然后根据AB测试的结果来检查是否实施新的服务;有了接入层,做这种AB测试就很容易了;无论是上网还是切换都很轻松。可以根据请求的信息在Lua中调用不同的服务或上游组来完成AB测试。灰度发布/流量切换对于一个灵活的系统来说,能够随时进行灰度发布和流量切换是非常重要的,比如验证新服务器是否稳定,或者验证新架构是否优于旧架构,有时候只有在线运行,才能看出有没有问题;我们可以在接入层通过配置或者写Lua代码来实现,灵活性非常好。您可以设置多个上游组,然后根据需要切换组。一个系统监控服务质量最重要的是双眼盯着系统,尽早发现问题。我们会在接入层代理请求,记录status、request_time、response_time来监控服务质量。比如根据调用次数,状态码是否为200,响应时间等来进行告警。限流我们系统中主要的限流逻辑是:对于大部分请求,根据IP请求数限流,对于登录用户,根据用户限流;读取缓存的请求不受限制,只命中后端系统。请求受到限制。也可以限制用户访问的频率,比如在ngx_lua中使用ngx.sleep休眠处理请求来减慢界面刷新的速度;或者植入cookietoken之类的,必须按流程接入。当然也可以向爬虫/刷卡数据请求返回虚假数据来降低影响。前端业务逻辑和前端JS尽量少一些业务逻辑和一些切换逻辑,因为前端JS一般都会推送到CDN。如果逻辑有问题,需要更新代码上线,推送到CDN,然后让每个边缘CDN节点失效;或者通过版本号机制修改服务端模板中的版本号上线。这两种方法都存在效率问题。紧急故障如果这样处理,故障也有可能恢复。因此,我们的观点是前端JS只展示数据,全部或大部分逻辑交给后端完成,即静态资源CSS/JSCDN,动态资源JSONP;前端JS瘦身,背后业务逻辑。双十一期间,我们的部分服务出现了问题,商品信息无法更新。这时候需要对闪购商品进行标注,所以我们在服务器端完成,整个过程几十秒就可以搞定,避免了产品无法秒杀的问题。但是如果在JS中完成的话,会花费很长时间,因为JS在客户端还是有缓存时间的,一般缓存时间都非常长。前端接口服务器在商品详情页聚合了很多服务,一个类似的服务需要请求多个不相关的服务接口,造成前端代码臃肿,判断逻辑很多;而我受不了这种情况,我想要的结果就是前端异步请求我的一个API,我准备好相关的数据发送,前端可以直接拿到数据显示;所有或大部分逻辑在服务器端而不是客户端完成;所以我们在接入层使用Lua协程机制,并发调用多个相关服务,最后将这些服务合并。例如,推荐服务:高级组合、推荐配件、优惠套餐;请求通过http://c.3.cn/recommend?methods=accessories,suit,combination&sku=1159330&cat=6728,6740,12408&lid=1&lim=6获取聚合数据,以便前端需要调用的接口三次只能吐出所有数据一次。我们用API封装了这种请求,如下图:比如库存服务,判断一个商品是否有货需要判断:1.主营商品的库存状态,2.包裹的库存状态主产品对应的子产品,以及主产品配件的库存情况和配套子产品的配件库存情况;套装产品是一种虚拟产品,是将多个产品绑定在一起进行销售的一种形式。如果这个逻辑在前端完成,需要多次调用库存服务,然后进行组合判断,这样前端代码会很复杂,所有涉及调用库存的服务都要判断;因此,我们将这些逻辑封装到服务端就完成了;前端请求http://c0.3.cn/stock?skuId=1856581&venderId=0&cat=9987,653,655&area=1_72_2840_0&buyNum=1&extraParam={%22originid%22:%221%22}&ch=1&callback=getStockCallback,然后服务器计算整个库存状态,前端不需要做任何调整。在服务端使用Lua协程并发调用库存,如下图:比如今天的抄底服务,库存、价格、促销等调用接口太多,所以我们也使用这种传输这些服务的机制在接入层被合并成一个大服务并对外暴露:http://c.3.cn/today?skuId=1264537&area=1_72_2840_0&promotionId=182369342&cat=737,752,760&callback=jQuery9364459&_=1444305642364。我们目前的合并主要包括:促销与广告合并、配送相关服务合并。以后这些服务会合并,在前端做一些特殊的处理,比如设置超时,超时后自动调用原子接口;如果接口吐出的数据状态码不正确,则重新请求原子接口获取相关数据。服务隔离服务隔离的目的是防止整个应用中的所有服务因为某些服务抖动而导致不可用。可以分为:应用内线程池隔离、部署/组隔离、应用隔离。应用内部线程池隔离,我们使用Servlet3异步,根据重要性级别为不同的请求分配线程池,这些线程池之间是相互隔离的,我们还提供了一个监控接口来发现问题及时动态调整,这个练习可以看?。部署/分组隔离是指为不同的消费者提供不同的分组,不同的分组之间互不影响,防止有人乱用同一个分组导致整个分组服务不可用。拆解应用隔离,如果一个服务调用量很大,那么我们可以把这个服务分离出来,做成一个应用,减少其他服务上线或者重启对应用的影响。【本文为专栏作者张凯涛原创文章,作者微信公众号:凯涛博客,id:kaitao-1234567】点此查看作者更多好文