14.深入分布式缓存:从原理到实践 --- 典型电商应用与缓存

	分布式系统的cap理论首先把分布式系统中的三个特性进行了如下归纳:
	1.一致性(C) : 在分布式系统中的所有数据备份,在同一时刻是否是同样的值(等同于所有节点访问同一份最新的数据副本)
	2.可用性(A) : 在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求(对数据更新具备高可用性)
	3.分区容错(P) : 以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就
	当前操作在C和A之间做出选择。

	电商领域是典型的要在cap做出权衡的业务领域。从参与者来说有用户,商户,平台运营人员;从基础领域模型看有商品,订单,库存,营销,物流等。
	用户的诉求是什么?买到好东西,支付方便,安全快捷。
	商户的诉求是什么?业务模式上快速解决回款;技术上解决对账清晰,数据准确。
	平台的诉求是什么?越来越多的用户,越来越多的品类,越来越好的商家。

1.电商类应用的挑战与特点
	电商类应用具有如下特点:
		1.稳定性决定服务能力
		2.高并发性场景
		3.业务发展性能也得发展
		4.产品快速试错

2.应用数据静态化架构高性能单页web应用
	在电商网站中,单页web是非常常见的一种形式,比如首页,广告页等都属于单页应用。这种页面是由模板+数据组成,传统的构建方式一般通过静态化实现。
  而这种方式的灵活性不是很好,比如页面模板部分变更了需要重新全部生成。因此最好能有一种实现方式是可以实时动态渲染的,以支持模板的多样性。另外
  也需要考虑如下问题:
	  	1.动态化模板渲染支持
	  	2.数据和模板的多版本化:生产版本,灰度版本和预发布版本
	  	3.版本回滚问题
	  	4.异常问题,假设渲染模板时遇到了异常情况如何处理
	  	5.灰度发布问题,比如切20%量给灰度版本
	  	6.预发布问题,目的是在正式版本测试数据和模板的正确性

  	1.整体架构
  		静态化单页如图14-1,直接将生成的静态页推送到相关服务器即可。使用这种方式要考虑文件操作的原子性,即从老版本切换到新版本如何做到文件操作原子化。
  	  而动态化方案的整体架构如图14-2,分为三大系统:CMS系统,控制系统和前端展示系统。

  	2.CMS系统
  		在cms系统中可以配置页面的模板和数据。模板动态在cms系统中维护,即模板不是一个静态文件,而是存储在cms中的一条数据,最终发布到'发布数据存储redis'
  	  中,前端展示系统从redis中获取该模板进行渲染,从而前端展示系统更换了模板也不需要重启,纯动态维护模板数据。
  	  	原始数据存储到'元数据存储mysql'中即可,比如频道页一般需要前端访问的url,分类,轮播图,商品楼层数据等,这些数据按照相应维度存储在cms系统中。cms
  	  系统提供发布到'发布数据存储redis'的控制。将cms系统中的原始数据和模板数据组装成聚合数据(json存储)同步到'发布数据存储redis',以便前端展示系统获取
  	  进行展示。此处提供3个按钮:正式版本,灰度版本和预发布版本。

  	  	cms目前存在几个问题:
  	  	1.用户访问诸如 http://channel.jd.com/fashion.html 怎么定位到对应的聚合数据呢?我们可以在cms元数据中定义url作为key,如果没有url,则用id
  	    作为key,或者自动生成一个url。
  	    2.多版本如何存储呢?使用redis的Hash结构存储即可,key作为url,字段按照维度存储:正式版本使用当前时间戳存储(这样前端系统可以根据时间戳排序然后获得
  	    最新的版本),预发布版本使用'predeploy'作为字段,灰度版本使用'abVersion'作为字段即可,这样就区分开了多版本。
  	    3.灰度版本如何控制呢?可以通过控制系统的开关来控制如何灰度
  	    4.如何访问预发布版本呢?比如在url参数总是带上predeploy=true,另外可以限定只有内网可以访问或者访问时带上访问密码,比如pwd=abc234
  	    5.模板变更的历史数据校验问题?比如模板变更了,但是使用历史数据渲染该模板会出现问题,即模板总是要兼容历史数据的;此处的方案不存在这个问题,因为每次存储
  	    的是当时的模板快照,即数据快照和模板快照推送到'发布数据存储redis'中。

  	3.前端展示系统
  		前端展示系统可以获取当前url,使用url作为key首先从本机'发布数据存储redis'获取数据。如果没有数据或者异常则从主'发布数据存储redis'获取。如果主'发布数据
  	  存储redis'也发生了异常,那么会直接调用cms系统暴露的api直接从元数据存储mysql中获取数据进行处理。
  	  	前端展示系统的伪代码如下:
  	  	--1.加载lua模块库
  	  	local template=require("resty.template")
  	  	template.load=function(s) return s end

  	  	--2.动态获取模板
  	  	local myTemplate="<html>{*title*}</html>"

  	  	--3.动态获取数据
  	  	local data={title="iphone6s"}

  	  	--4.渲染模板
  	  	local func=template.compile{myTemplate}
  	  	local content=func(data)

  	  	--5.通过ngx api 输出内容
  	  	ngx.say(content)

  	  	从上述代码可知,模板和数据都是动态获取的,然后使用动态获取的模板和数据进行渲染。
  	  	此处假设最新版本的模板和数据都有问题怎么办?可以从流程上避免:
  	  	a)首先进行预发布版本发布,测试人员严重没问题后进行下一步
  	  	b)接着发布灰度版本,在灰度的时候自动去掉cdn功能(即不设置页面的缓存时间),发布验证
  	  	c)最后发布正式版本,正式版本发布的前5分钟内是不设置页面缓存的,这样就可以防止发版时遇到问题,但是若问题版本已经在cdn上,问题会影响到全部用户,且无法快速回滚。

  	4.控制系统
  		控制系统是用于版本降级和灰度发布的,当然也可以把这个功能放在cms系统中。
  		a)版本降级:假设当前线上的版本遇到问题了,想要快速切换到上个版本,可以使用控制系统实现,选择其中一个历史版本然后通知给前端展示系统,使用url和当前版本的字段即可,
  		这样前端展示系统就可以自动切换到选中的那个版本;当问题修复后,再删除该降级配置切换回最新版本。
  		b)灰度发布:在控制系统控制哪些url需要灰度发布和灰度发布的比例,与版本降级类似,将相关的数据推送到前端展示系统即可,当不想灰度发布时删除相关数据。

  		1.数据和模板动态化
  			我们将数据和模板都进行动态化存储,这样可以在cms进行数据和模板的变更;实现了前端和后端人员的分离;前端开发人员进行cms数据配置和模板开发,而后端开发人员只进行
  		  系统的维护。另外,因为模板的动态存储,每次发布新的模板不需要重启前端展示系统,后端开发人员更好的得到了解放。
  		  	模板和数据可以是一对多的关系,即一个模板可以被多个数据使用。假设模板发生变更后,我们可以批量推送模板关联的数据,首先进行预发布版本的发布,由测试人员进行验证,
  		  验证没问题发布正式版本。

  		2.多版本机制
  			我们将数据和模板分为多版本后,可以实现:
  				1.预发布版本:更容易让测试人员在实际环境进行验证
  				2.灰度版本:只需要简单的开关,就可以进行ab测试
  				3.正式版本:存储多个历史正式版本,假设最新的正式版本出现问题,可以非常快速的切换回之前的版本

3.应用多级缓存模式支撑海量读服务
	1.多级缓存介绍
		所谓的多级缓存,即在整个系统架构的不同系统层级进行数据缓存,以提升访问效率。整体流程如下14-3:
		1.首先接入nginx将请求负载均衡到应用nginx,此处常用的负载均衡算法是轮询或者一致性哈希,轮询可以使服务器的请求更加均衡,而一致性哈希可以提升应用nginx的缓存命中率。
		相对于轮询,一致性哈希会存在单机热点问题,一种解决办法是热点直接推送到接入层nginx,另一种办法是设置一个阈值,当超过阈值,改为轮询。
		2.接着应用nginx读取本地缓存,如果本地缓存命中则直接返回。应用nging本地缓存可以提升整体的吞吐量,降低后端的压力,尤其应对热点问题非常有效。本地缓存可以使用
		Lua Shared Dict,Nginx Proxy Cache(磁盘/内存),Local Redis实现
		3.如果nginx本地缓存没有命中,则会读取相应的分布式缓存(如redis缓存,另外可以考虑主从架构来提升性能和吞吐量),如果分布式缓存命中则直接返回相应的数据(并回写到nginx
		本地缓存)
		4.如果分布式缓存也没有命中,则会回源到tomcat集群,在回源到tomcat集群时也可以使用轮询或一致性哈希作为负载均衡算法
		5.在tomcat应用中,首先读取本地堆缓存,如果有则直接返回(并会写到主redis集群)
		6.作为可选部分,如果步骤4没有命中可以再尝试一次读取主redis集群,目的是防止从redis集群有问题时的流量冲击
		7.如果所有的缓存都没有命中,只能查询db或相关服务获取数据并返回
		8.步骤7返回的数据异步写到主redis集群,此处可能有多个tomcat实例同时写主redis集群,造成数据混乱。

		应用整体分了3部分的缓存:应用nginx本地缓存,分布式缓存,tomcat堆缓存。每一层缓存都用来解决相关的问题,如应用nginx本地缓存用来解决热点缓存问题,分布式缓存用来减少
	  访问回源率,tomcat堆缓存用于防止缓存失效/崩溃之后的冲击。

	2.如何缓存数据
		1.过期与不过期
			对于缓存的数据我们可以考虑不过期缓存和带过期时间缓存,什么场景应该选择哪种模式则需要根据业务和数据量等因素来决定。
			a)不过期缓存场景一般思路如下:
				业务逻辑:1.开始事务;2.执行sql;3.提交事务;4.写缓存
				使用cache-aside 模式,首先写数据库,如果成功,则写缓存。这种场景下存在事务成功,缓存写失败但无法回滚事务的情况。另外,不要把写缓存放在事务中,尤其写分布式
			  缓存,因为网络抖动可能导致写缓存响应时间很慢,引起数据库事务阻塞。如果对缓存数据一致性要求不是很高,数据量也不是很大,则可以考虑定期全量同步缓存。
			  	也有提到以下思路:先删缓存,然后执行数据库事务;不过这种操作对于如商品这种查询非常频繁的业务不适用,因为你在删缓存的同时,已经有另外一个系统读缓存了,此时事务
			  还没提交。当然对于如用户维度的业务是可以考虑的。
			  	不过为了更好的解决以上多个事务的问题,可以考虑使用订阅数据库日志的架构,如使用canal订阅mysql的binlog实现缓存同步。
			  	对于长尾访问的数据,大多数数据访问频繁都很高的场景,若缓存空间足够则可以考虑不过期缓存,如用户,分类,商品,价格,当缓存满了可以考虑LRU机制驱逐老的缓存数据。
			b)过期缓存机制,即采用懒加载,一般用于缓存其他系统的数据(无法订阅变更消息或者成本很高),缓存空间有限,低频热点缓存等场景。常见的步骤是:首先读取缓存,如果不命中
			则查询数据,然后异步写入缓存并设置过期缓存,设置过期时间,下次读取将命中缓存。热点数据经常使用,即在应用系统上缓存比较短的时间。这种缓存可能存在一段时间的数据
			不一致,需要根据场景来决定如何设置过期时间。如库存数据可以在前端应用上缓存几秒钟,短时间的不一致是可以忍受的。

		2.维度化缓存与增量缓存
			对于电商系统,一个商品可能拆成诸如基础属性,图片列表,上下架,规格参数,商品介绍等;如果商品变了,要把这些数据都更新一遍,那么整个更新成本(接口调用量和带宽)很高。
		  因此最好将数据进行维度化并增量更新(只更新变更的部分)。尤其是上下架这种只是一个状态变更,但是每天频繁调用的,维度化后能减少服务很大的压力。
		  	按照不同维度接收MQ进行更新。

		3.大value缓存
			要警惕缓存中的大value,尤其是使用redis的时候。遇到这种情况时可以考虑使用多线程实现的缓存(如memcached)来缓存大value;或者对大value进行压缩;或者将value拆分为
		  多个小value,客户端再进行查询,聚合。

		4.热点缓存
			对于那些访问非常频繁的热点缓存,如果每次都去远程缓存系统中获取,可能会因为访问量太大导致远程缓存系统请求太多,负载过高或者带宽过高等问题,最终可能会导致缓存响应慢,
		  使客户端超时。一种解决办法是通过挂更多的从缓存,客户端通过负载均衡机制读取从缓存系统数据。不过也可以在客户端所在的应用/代理层本地存储一份,从而避免访问远程缓存,即使
		  像库存这种数据,在有些应用系统中也可以进行几秒的本地缓存,从而降低远程系统的压力。

	3.分布式缓存与应用负载均衡	
		此处说的分布式缓存一般采用分片实现,即将数据分散到多个实例或多台服务器。算法一般采用取模和一致性哈希。如之前说的做不过期缓存机制可以考虑取模机制,扩容时一般是新建一个
	  集群;而对于可以丢失的缓存数据则可以采用一致性哈希,即使其中一个实例出问题只是丢失一小部分,对于分片实现可以考虑客户端实现,或者使用如Twemproxy中间件进行代理(分片对
	  客户端是透明的)。如果使用redis可以考虑使用redis-cluster分布式集群方案。
	  	应用负载均衡一般采用轮询和一致性哈希,一致性哈希可以根据应用请求的url或者url参数将相同的请求转发到同一个节点;而轮询即将请求均匀的转发到每一个服务器。
	  	整体流程如下:
	  		1.首先请求进入接入层nginx
	  		2.根据负载均衡算法将请求转发给应用nginx
	  		3.如果应用nginx本地缓存命中,则直接返回数据,否则读取分布式缓存或者回源到tomcat集群。
	  	轮询的优点是:应用nginx的请求更加均匀,使得每个服务器负载基本均衡,不会因为热点问题导致其中一台服务器负载过重。
	  	轮询的缺点是:随着应用nginx服务器的增加,缓存的命中率会下降。
	  	一致性哈希的优点是:相同的请求都会转发到同一台服务器,命中率不会因为服务器的增加而降低。
	  	一致性哈希的缺点是:因为相同的请求都会转发到同一台服务器,因此可能造成某台服务器负载过重。
	  	那么如何选择:
	  		a)负载低的时候选择一致性哈希,比如普通商品访问
	  		b)热点请求时降级一致性哈希为轮询,比如京东首页的商品访问
	  		当然,某些场景是将热点数据直接推送到接入层nginx,直接响应给用户,比如秒杀。

	4.热点数据与更新缓存
		1.打击全量缓存+主从
			如图14-7所示,所有缓存都存储在应用本地,回源之后把数据更新到主redis集群,然后通过主从复制到其他从redis集群。缓存的更新可以采用懒加载或者订阅消息进行同步。
		2.分布式缓存+应用本地热点
			对于分布式缓存,我们需要在nginx+lua 应用中进行应用缓存来减少redis集群的访问冲击,即首先查询应用本地缓存,如果命中则直接返回,如果没有命中则接着查询redis
		  集群,回源到tomcat,然后将数据缓存到应用本地。
		  	此处到应用nginx的负载机制:正常情况下才有一致性哈希,如果某个请求类型访问量突破了一定阈值,则自动降级为轮询机制。另外对于一些秒杀活动之类的热点我们是可以
		  提前知道的,可以把相关的数据预先推送到接入层nginx并将负载均衡机制降级为轮询。

		另外可以考虑建立实时热点发现系统来发现热点:
			1.接入层nginx将请求转发给应用nginx
			2.应用nginx首先读取本地缓存,如果命中则直接返回,不命中会读取分布式缓存,回源到tomcat集群
			3.应用nginx会将请求上报给实时热点发现系统(如使用udp上报请求,或者将请求写到kafka,或者使用flume订阅本地nginx日志),上报给实时热点发现系统后,它将进行
		    热点统计(可以考虑storm实时计算)
		    4.根据设计的阈值将热点数据推送到应用nginx本地缓存。
		因为做了本地缓存,因此对数据一致性需要我们去考虑,即何时失效或更新缓存:
			1.如果可以订阅数据变更消息,那么可以订阅变更消息进行缓存更新
			2.如果无法订阅或者成本比较高,并且对短暂的数据一致性要求不严格(比如在商品详情页看到的库存,可以短暂不一致,只要保证下单时一致即可),那么可以设置合理的过期时间,
			过期后再查询更新数据。
			3.如果是秒杀之类的,可以订阅活动开启消息,将相关数据提取推送到前端应用,并将负载机制降级为轮询
			4.建立实时热点发现系统来对热点进行统一推送和更新

	5.更新缓存与原子性
		如果多个应用同时操作一份数据可能造成缓存是脏数据,解决的办法有:
			1.更细数据时使用更新时间戳或者版本对比,如果使用redis可以利用其单线程机制进行原子化更新
			2.使用如canal订阅数据库binlog,此时把mysql看成发布者,binlog是发布的内容,canal看成消费者,canal订阅binlog然后更新到redis。
			3.将更新请求按照相应的规则分散到多个队列,然后每个队列进行单线程更新,更新时拉取最新的数据保存
			4.用分布式锁,更新前获取相关的锁。

	6.缓存崩溃与快速恢复
		当我们使用分布式缓存的时候,应该考虑如何对应其中一部分缓存实例宕机的情况。当数据可丢失的情况,我们可以选择一致性哈希。

		1.取模
			对于取模机制如果其中一个实例故障,如果摘除此实例将导致大量缓存不命中,瞬间大流量可能导致后端db/服务出现问题。对于这种情况可以采用主从机制来避免实例故障的
		  问题,即其中一个实例故障可以用从/主顶上来。但是取模机制下如果增加一个节点将导致大量缓存不命中,所以一般是建立另一个集群,然后把数据迁移到新集群,然后把流量
		  迁移过去。

		2.一致性哈希
			对于一致性哈希机制如果其中一个实例故障,摘除此实例只影响一致性哈希的部分缓存不命中,不会导致瞬间大量回源到后端db/服务,但是也会产生一定影响。

		3.快速恢复
			1.主从机制,做好冗余
			2.如果因为缓存导致应用可用性已经下降可以考虑:部分用户降级,然后慢慢减少降级量后台通过worker预热缓存数据。
			也就是说如果整个缓存集群故障,而且没有备份,那么只能去慢慢将缓存重建。为了让部分用户还是可用的,可以根据系统的承受能力,通过降级方案让一部分用户先用起来,
		  将这些用户的相关缓存重建。另外根据后台worker进行缓存数据的预热。

4.构建需求响应式亿级商品详情页
	商品详情页架构,主要包括三部分:
		1.商品详情页系统 : 负责静态部分
		2.商品详情页同意服务系统 : 负责动态部分
		3.商品详情页动态服务系统 : 给内网其他系统提供一些数据服务

	1.商品详情页前端结构
		前端展示可以分为这么几个维度:商品维度(标题,图片,属性等),主商品维度(商品介绍,规格参数),分类维度,商家维度,店铺维度等,另外还有一些实时性要求比较高的如
	  实时价格,实时促销,广告词,配送至,预售等都是通过异步加载的。

	2.单品页技术架构发展
		单品页技术架构经历了4个阶段的发展:
		架构1.0
			IIS+C#+SQL Server ,最原始的架构,直接调用商品库获取响应的数据,扛不住时加一层memcached来缓存数据。

		架构2.0
			该方案采用了静态化技术,按照商品维度生成静态化html。
			主要思路:
				1.通过MQ得到变更通知
				2.通过java worker调用多个依赖系统生成详情页html
				3.通过rsync同步到其他机器
				4.通过nginx直接输出静态页
				5.接入层负责负载均衡
			该方案的缺点是:
				1.假设只有分类,面包屑变了,那么所有相关的商品都要重新刷
				2.随着商品数量的增加,rsync会成为瓶颈
				3.无法迅速响应一些页面需求变更,大部分都是通过JavaScript动态改变页面元素
				
				随着商品数量的增加,这种架构的存储容量达到了瓶颈,而且按照商品维度生成整个页面会存在如分类维度变更就要全部刷一遍这个分类下所有信息的问题,因此又改版了
			  一次按照尾号路由到多台机器。
			  主要思路:
			  	1.容量问题通过按照商品尾号做路由分散到多台机器,按照自营商品单独一台,第三方商品按照尾号分散到11台。
			  	2.按维度生成html片段,而不是一个大的html
			  	3.通过nginx SSI 合并片段并输出
			  	4.接入层负责负载均衡
			  	5.多机房部署也无法通过rsync同步,而是使用部署多套相同的架构来实现
			  该方案的缺点是:
			  	1.碎片文件太多,导致无法rsync
			  	2.机械盘做SSI合并时,高并发时性能差,此时我们还没尝试SSD
			  	3.模板如果要变更,数亿商品需要数天才能刷完
			  	4.到达容量瓶颈时,我们会删除一部分静态化商品,然后通过动态渲染输出,动态渲染输出在高峰时会导致依赖系统压力大,扛不住
			  	5.还是无法迅速响应一些业务需求
			  我们的痛点是:
			  	1.之前的架构的问题存在容量问题,很快就会出现无法全量静态化问题,所以还是需要动态渲染;不过对于全量静态化渲染还可以通过分布式文件系统解决,没尝试。
			  	2.最主要的问题是随着业务的发展,无法迅速满足变化的需求

		架构3.0
			我们要解决的问题:
				1.能迅速响应瞬变的需求和其他需求
				2.支持各种垂直化页面改版
				3.页面模块化
				4.ab测试
				5.高性能,水平扩容
				6.多机房多活,异地多活
			主要思路:
				1.数据变更还是通过mq
				2.数据异构worker得到通知,然后按照一些维度进行数据存储,存储到数据异构JIMDB集群(JIMDB:redis+持久化引擎),存储的数据都是未加工的原子化数据,如商品
				基本信息,商品扩展属性,商品其他一些信息,商品规格参数,分类,商家信息等。
				3.数据异构worker存储成功后,会发送一个mq给数据同步worker,数据同步worker也可以叫做数据聚合worker,按照相应的维度聚合数据存储到相应的JIMDB集群;三个
				维度信息:基本信息(基本信息+扩展属性等的一个聚合),商品介绍(PC版,移动版),其他信息(分类,商家等维度,数据量小,直接redis存储)
				4.前端展示分为2个:商品详情页和商品介绍,使用nginx+lua技术获取数据并渲染模板输出。
				另外我们目前的架构的目标不仅仅是为商品详情页提供数据,只要是key-value结构获取而非关系结构的我们都可以提供服务,我们叫做动态服务系统。该动态服务分为前端和
			  后端,即公网还是内网,如目前该动态服务为列表页,商品对比,微信单品页,总代等提供相应的数据来满足和支撑其业务。

	3.详情页架构设计原则
		1.数据闭环
			数据闭环即数据的自我管理,或者说是数据都在自己的系统里维护,不依赖于任何其他系统,去依赖化。这样的好处是别人抖动跟我没关系。
			a)数据异构是数据闭环的第一步,将各个依赖系统的数据拿过来,按照自己的要求存储起来
			b)数据原子化,数据异构的数据是原子化数据,这样未来我们可以对这些数据进行再加工再处理而想要变化的需求
			c)数据聚合,将多个原子化的数据聚合为一个大json数据,这样前端展示只需要一次get,当然要考虑系统架构,比如我们使用的redis改造,redis又是单线程系统,我们需要
			部署更多的redis来支持更高的并发,另外存储的值要尽可能的小。
			d)数据存储,我们使用JIMDB,redis持久化存储引擎,可以存储超过内存N倍的数据量,我们目前一些系统是redis+LMDB引擎的存储,是配合ssd进行存储;另外我们使用hash tag
			机制把相关的数据哈希到同一个分片,这样mget时不需要跨分片合并。
			我们目前的异构数据是键值结构的,用于按照商品维度查询,还有一套异构是关系结构的,用于关系查询使用。

		2.数据维度化
			对于数据应该按照维度和作用进行维度化,这样可以分离存储,进行更有效的存储和使用。

		3.拆分系统
			将系统拆分为多个子系统虽然增加了复杂度,但是可以得到更多的好处,比如数据异构系统存储的数据是原子化数据,这样可以按照一些维度对外提供服务;而数据同步系统存储的
		  是聚合数据,可以为前端展示提供高性能的读取。前端展示系统分离为商品详情页和商品介绍,可以减少互相影响;目前商品介绍系统还提供其他的一些服务,比如全站异步脚本服务。

		4.worker无状态化+任务化
			worker无状态化+任务化,可以帮助系统做水平扩展。
			1.数据异构和数据同步worker无状态化设计,这样可以水平扩展
			2.应用虽然是无状态的,但是配置文件是有状态的,每个机房一套配置,这样每个机房只读取当前机房数据
			3.任务多队列化,等待队列,排重队列,本地执行队列,失败队列
			4.队列优先级化,分为:普通队列,刷数据队列,高优先级队列,例如一些秒杀商品会走高优先级队列保证快速执行。
			5.副本队列,当上线后业务出现问题时,修正逻辑可以回放,从而修复数据;可以按照比例如固定大小队列或者小时队列设计
			6.在设计消息时,按照维度更新,比如商品信息变更和商品上下架分离,减少每次变更接口的调用量,通过聚合worker去做聚合。

		5.异步化+并发化
			我们系统使用了大量的异步化,通过异步化机制提升并发能力。首先使用了消息异步化进行系统解耦,通过消息通知变更,然后再调用相应接口获取相关数据;之前老系统使用同步
		  推送机制,这种方式系统是紧耦合的,出问题需要联系各个负责人重新推送还要考虑失败重试机制。数据更新异步化,更新缓存时同步调用服务,然后异步更新缓存。可使得任务并行化。
		  商品数据来源有多处,但是可以并发调用聚合,经过这种方式我们可以把原先串行1s的时间提示到300ms内。异步请求合并,一次请求调用就能拿到所有的数据。前端服务异步化/聚合,
		  实时价格,实时库存异步化,使用如线程或协程机制将多个可并发的服务聚合。异步化还有一个好处是可以对异步请求做合并,原来的n个调用可以合并为一次,还可以做请求的排重。

		6.多级缓存化
			1.浏览器缓存,当页面之间来回跳转时走local cache,或者打开页面时拿着Last-Modified去CDN验证是否过期,减少来回传输的数据量
			2.cdn缓存,用户去离自己最近的cdn节点拿数据,而不是全部回源到北京机房获取数据,提升访问性能
			3.服务端应用本地缓存,我们使用nginx+lua架构,使用HttpLuaModule模块的shared dict做本地缓存(reload不丢失)或内存级Proxy Cache,从而减少带宽。
			另外我们还可以使用一致性哈希(如商品编号/分类)做负载均衡内部对url重写提升命中率
			我们对mget做了优化,如取商品其他维度数据,分类,面包屑,商家等差不多8个维度数据,如果每次mget获取性能差而且数量很大,30kb以上;而这些数据缓存半小时也是没有
		  问题的,那么我们可以设计为先读local cache,然后把不命中的再回源到remote cache获取,这个优化减少了一半的remote cache 流量。
		    4.服务端分布式缓存,我们使用内存+ssd+JIMDB持久化存储

		7.动态化
			数据获取动态化,商品详情页:按维度获取数据,如商品基本数据,其他数据;而且还可以根据数据属性,按需做逻辑,比如虚拟商品需要自己定制的详情页,那么我们就可以
		  跳转走,比如全球购的需要走jd.hk域名,也是没问题。
		  	模板渲染实时化,支持随时变更模板需求。
		  	重启应用秒级化,使用nginx+lua架构,重启速度快,重启不丢失共享字段缓存数据;
		  	需求上线速度化,因为我们使用了nginx+lua架构,可以快速上线和重启应用,不会产生抖动;另外lua本身是一个脚本语言,我们也尝试把代码如何版本化存储,直接内部驱动
		  lua代码更新而不需要重启nginx。

		8.弹性化
			我们所有的应用业务都接入了docker容器,存储还是物理机。我们会制作一些基础镜像,把需要的软件打成镜像,这样就不用每次去运维那安装部署软件了。未来可以支持自动扩容,
		  比如按照cpu或带宽自动扩容机器。

		9.降级开关	
			推送服务器推送降级开关,开关集中化维护,然后通过推送机制推送到各个服务器。可降级的多级读服务为:前端数据集群 -> 数据异构集群 -> 动态服务(调用依赖系统),这样
		  就可以保证服务质量,假设前端数据集群坏了一个磁盘,还可以回源到数据异构集群获取数据。开关前置化,如nginx->tomcat,在nginx做开关,请求就到不了后端,减少后端压力。
		  	可降级的业务线程池隔离。我们可以把处理过程分解为一个个的事件。通过这种将请求划分事件的方式我们可以进行更多的控制。如,我们可以为不同的业务再建立不同的线程池
		  进行控制:即我们只依赖tomcat线程池进行请求的解析,对请求的处理可以交给我们自己的线程池去完成。这样tomcat线程池就不是我们的瓶颈了。通过使用这种异步化事件模型,
		  我们可以提高整体的吞吐量,不让慢速的A业务影响到其他业务。慢的还是慢,但是不影响其他业务。

		10.多机房多活
			应用无状态,通过在配置文件中配置各个资方的数据集群来完成数据读取。数据集群采用一主三从结构,防止当一个机房挂了,另一个机房压力大产生抖动。

		11.多种压测方案
			线下压测使用apache ab,apache Jmeter,这种方式是固定url压测,一般通过访问日志收集一些url进行压测,可以简单压测单机峰值吞吐量,但是不能作为最终的压测结果,
		  因为这种压测会存在热点问题。
		  	线上压测,可以使用Tcpcopy直接把线上流量导入到压测服务器,这种方式可以压测出机器的性能,还可以把流量放大,也可以使用nginx+lua协程机制把流量分发到多台压测服务器,
		  或者直接在页面埋点,让用户压测,此种压测方式可以不给用户返回内容。

	4.遇到的一些问题
		1.SSD性能差	
			使用SSD做KV存储时发现磁盘IO非常低。
			初步怀疑:线上系统用的是消费级硬盘。RAID卡设置,write back 和 write through策略。实验用的是dd压测,严格测试应该用FIO等工具。

		2.键值存储选型压测
			对于存储选型,尝试过LevelDB,RocksDB,BeansDB,LMDB,Riak等,最终选择LMDB。

		3.数据量大时JIMDB同步不动
			JIMDB 数据同步时要 dump 数据,ssd磁盘容量用了50%以上,dump到同一块磁盘容量不足。

		4.切换主从
			之前的架构是一主二从(主机房一主一从,备机房一从)切换到备机房时,只有一个主服务,读写压力大时有抖动,因此改造为一主三从。

		5.分片配置
			之前的架构分片逻辑分散到多个子系统的配置文件中,切换时需要很多系统,解决方案:
			1.引入Twemproxy中间件,我们使用本地部署的 Twemproxy 来维护分片逻辑
			2.使用自动部署系统推送配置和重启应用,重启之前暂停mq消费保证数据一致性
			3.用unix domain socket减少连接数和端口占用不释放启动不了服务的问题

		6.模板元数据存储HTML
			起初不确定lua做逻辑和渲染模板性能如何,就尽量减少for,if/else之类的逻辑;通过java worker组成html片段存储到jimdb,html片段会存储诸多问题,假设未来
		  变了也是需要全量刷出的,因此存储的内容最好就是元数据。因此通过线上不断压测,最终jimb只存储元数据,lua做逻辑和渲染,逻辑代码在3000行以上,模板代码1500以上,
		  其中包括大量for,if/else语句,目前渲染性能可以接受。

		7.库存接口访问量600w/分钟
			因为是详情页展示的数据,缓存几秒是可以接受的,因此开启nginx proxy cache 来解决该问题。目前用的是nginx+lua架构改造服务,数据过滤,url重写等在nginx层完成。
		  通过url重写+一致性哈希负载均衡,不怕随机url,一些服务提升了10%以上的缓存命中率。

		8.微信接口调用链暴增
			通过访问日志发现某ip频繁抓取,而且按照商品编号遍历,但是会有一些不存在的编号,解决方案是:
			1.读取kv存储的部分不限流
			2.回源到服务接口的进行请求限流,保证服务质量

		9.开启nginx proxy cache 性能不升反降
			开启nginx proxy cache 后,性能下降,而且过一段时间内存使用率达到98%,解决方案是:
			1.对于内存占有率高的问题是内核问题,内核使用lru机制,本身不是问题,不过可以修改内核参数:
				sysctl -w vm.extra_free_bytes=6436787
				sysctl -w vm.vsf_cache_pressure=10000
			2.使用proxy cache在机械盘上性能差可以通过tmpfs缓存或nginx共享字典缓存元数据,或者使用ssd,目前使用内存文件系统。

		10.'配送至'读服务因依赖太多,响应时间偏慢
			'配送至'服务每天有数十亿调用量,响应时间慢,解决方案是:
			1.串行获取变并发获取,这样一些服务可以并发调用
			2.预取依赖数据回传

		11.网络抖动时,返回502错误
			Twemproxy 配置的timeout时间太长,之前设置5s,而且没有分别对连接,读,写设置超时。后来我们减少了超时时间,内网设置150ms以内,当超时时访问动态服务。

		12.机器流量太大
			双11期间,服务器网卡流量到了400Mbps,cpu30%左右。原因是我们所有的压缩都在接入层完成,因此接入层不再传入相关请求头到应用,随着流量的增大,接入层压力过大。
		  因此我们把压缩下方到各个业务应用,添加了相应的请求头,nginx gzip压缩级别在2~4吞吐量最高,应用服务器流量降了差不多5倍。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值