基于支付场景下的微服务改造与性能优化

墨墨导读:本文节选自《高可用可伸缩微服务架构:基于Dubbo、Spring Cloud和Service Mesh》一书,程超 等著,由电子工业出版社博文视点出版,已得到受权。近年来微服务架构已经成为大规模分布式架构的主流技术,愈来愈多的公司已经或开始转型为微服务架构。本书不以某一种微服务框架的使用为主题,而是对整个微服务生态进行系统性的讲解,并结合工做中的大量实战案例为读者呈现一本读完便可实际上手应用的工具书。前端


1、支付场景的介绍



本章主要介绍基于支付场景下的微服务实践,微服务体现的真谛最终仍是要理解业务,只有深刻理解了业务才能结合领域来从新定义微服务,下面就简单介绍一下互联网支付。


常见的互联网支付的使用场景主要有如下几种。java


  • 刷卡支付:用户展现微信钱包内的“刷卡条码/二维码”给商户系统,扫描后直接完成支付,适用于线下面对面收银的场景,如超市、便利店等(被扫,线下)。数据库

  • 扫码支付:商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”来完成支付,适用于PC网站支付、实体店单品等场景(主扫,线上)。apache

  • 公众号支付:用户在微信中打开商户的H5页面,商户在H5页面经过调用微信支付提供的JSAPI接口调用微信支付模块来完成支付,适用于在公众号、朋友圈、聊天窗口等微信内完成支付的场景。后端

  • WAP支付:基于公众号基础开发的一种非微信内浏览器支付方式(须要单独申请支付权限),能够知足在微信外的手机H5页面进行微信支付的需求!简单来讲,就是经过PC、手机网页来实现下单支付(俗称H5支付)。浏览器

  • App支付:商户经过在移动端应用App中集成开放SDK调用微信支付模块来完成支付。缓存

  • 网关支付:用户须要开通网上银行后在线完成支付,主要对象是国内银行借记卡和信用卡,是银行系统为企业或我的提供的安全、快捷、稳定的支付服务。安全

  • 快捷支付:快捷支付指用户购买商品时,不需开通网银,只需提供银行卡卡号、户名、手机号码等信息,银行验证手机号码正确性后,第三方支付发送手机动态口令到用户手机号上,用户输入正确的手机动态口令便可完成支付。性能优化


在支付场景下实现微服务的最终目标:可以将单体支付系统按业务进行解耦,利用微服务生态来实施支付系统,而且可以保证系统的可靠性和并发能力,建设完整的运维体系以支撑日益庞大的微服务系统。服务器


2、支付业务建模和服务划分



咱们在第2章介绍了领域建模的相关知识,由此能够知道几个关键词:领域、子域、限界上下文。有些读者对领域、子域的概念比较容易理解,可是限界上下文就理解得比较模糊,这里再对这个关键词简单作一下介绍。


能够把限界上下文理解为:一个系统、一个应用、一个服务或一个组件,而它又存在于领域之中。举个生活中的例子:我天天上班都会坐地铁,从家里出发到单位须要换乘三次地铁,分别是5号线、8号线和2号线。那么地铁就能够理解为限界上下文,从5号线走到8号线这个过程就是领域事件,而为了到达目的地中间换乘地铁,这个过程叫做上下文切换。


再回到支付业务中,该如何根据业务和领域相关知识来划分服务呢?咱们以一个业务架构示例来说解,如图11-1所示。


当咱们在工做中遇到一个完整的业务场景时,首先须要识别出一共有哪些领域,根据大的领域再来划分子域,最后将具备相同领域或子域的限界上下文进行归类。正确识别出领域实际上是比较难的,须要设计人员前期对业务有大量的调研,有比较深刻的了解后才能识别领域。


从图11-1中能够看到整个业务架构图分两大部分,中间的是业务核心领域,两边的是支撑子域。


咱们重点介绍中间的部分,每一层就是一个领域,领域中又包括特定子域。


(1)对接业务层:主要是一些业务系统对接支付系统,包括电商业务、互金业务和一键支付三个限界上下文。

(2)统一接入网关层:主要功能是对请求入口进行加解密、分流、限流和准入控制等。


640?wx_fmt=png

图11-1


(3)产品服务层。

  • 收银台:包括两个限界上下文,分别是PC收银台和手机手银台。

  • 商户:包括四个限界上下文,分别是分帐、鉴权、担保和代扣。

  • 我的:包括两个限界上下文,分别是充值和提现。

(4)业务服务层:包括五个限界上下文,分别是交易服务、支付服务、退款服务、计费服务和风控服务。

(5)基础服务层。

  • 网关:包括三个限界上下文,分别是支付网关、鉴权和支付路由。

  • 资金处理平台:包括四个限界上下文—对帐、清结算、备付金和会计。


3、支付场景下微服务架构的详解与分析



使用微服务的核心是业务,没有业务进行支撑的微服务是“虚的”,但只有业务与微服务相结合的思想而没有微服务的架构体系也是没法将微服务落地的,因此本章重点介绍要作好微服务还须要完善哪些技术架构。


下面咱们将以一个实际工做中的案例为出发点,分析在中小公司中如何落地微服务。如图11-2所示,左半部分是微服务的业务架构,右半部分是微服务的基础技术架构。


640?wx_fmt=png

图11-2


3.1 业务架构分析


根据前面介绍的如何根据业务来划分领域能够看到,整个业务架构部分已经完成了领域的划分,咱们重点来看服务层。服务层是一个核心域里面包含了多个子域,每一个子域都是按功能进行划分的,好比支付中心子域里面包括支付服务、路由系统和银行渠道等限界上下文,这些限界上下文是一个服务,仍是一个系统呢?这就要结合康威定律来综合考量团队的规模,小公司创业初期研发人员少,能够将支付中心子域定义为限界上下文,里面包括三个独立模块,分别是支付服务模块、路由模块和银行渠道模块,待人员逐步增长到必定规模后,多个项目组同时修改一个支付中心限界上下文会致使互相影响的时候,就须要将支付中心上升为一个业务领域,而将以前的三个独立模块拆分为独立系统,由不一样的项目组分别接管,各自维护各自部署,如图11-3所示。


640?wx_fmt=png

图11-3


能够看出左边是未拆分前的结构,交易服务想要调用支付模块就必须统一调用支付系统,而后才能调用支付模块,而右边是通过拆分后的结构,这时交易服务能够直接调用支付服务系统、路由系统和银行渠道系统中的任意一个,固然从业务流来说确定要先调用支付服务系统。


而数据层是根据业务进行数据库的拆分,拆分原则与应用拆分相同,如图11-4所示。


640?wx_fmt=png

图11-4


能够看到业务、应用和数据库三者一体,物理上与其余业务隔离,不一样应用服务的数据库是不能直接访问的,只能经过服务调用进行访问。


3.2 技术平台详解


当咱们将整个支付业务根据微服务理念作了合理划分以后,业务架构的各层次就逐步清晰起来,而微服务架构的成功建设除了业务上面的划分,技术平台和运维体系的支撑也是很是重要的,图11-2的右半部分共分为三个层次,分别是统一平台业务层、微服务基础中间件层和自动化运维层。


1) 统一业务平台层

这一层主要是通用的平台业务系统,包括数据分析服务、商户运营服务、运维管控服务和进件报备服务,它们没法根据业务被归类到某一业务系统中,只能做为支撑域存在,因此放到统一业务平台层供全部业务线共同使用。


2)微服务基础中间件层

微服务自己是一个生态,为了支撑微服务这个庞大的体系,必须有不少基础中间件进行辅助才能使微服务平稳地运行。下面将根据笔者积累的实践经验对图中一些重要的组件进行技术选型方面的介绍,另外图中有不少组件在本书其余章节进行了详细介绍,这里就再也不作说明。


  • 微服务框架


目前市面上很是流行Spring Boot+Spring Cloud的微服务框架,这套框架确实是微服务的集大成者,涵盖的范围广,能够支持动态扩展和多种插件。可是做为公司的管理者来讲,并不能由于出了新的技术就马上将公司核心业务用新的技术进行更替,这样在生产上所带来的风险将会很是大。比较合理的作法是,若是公司或部门是新成立的,尚未作技术框架的选型,又想在公司内部推广微服务的时候,尝试使用Spring Boot和Spring Cloud框架,能够节省出公司或部门的不少时间来攻关前端业务,而不须要将更多精力放在如何进行微服务的建设上来。


目前不少互联网公司在生产过程当中使用的微服务框架并非Spring Boot和Spring Cloud,会使用如Dubbo、gRPC、Thrift等RPC框架进行服务治理,而公司内部本身研发出不少微服务的外围组件,好比APM监控系统、分库分表组件、统一配置中心、统必定时任务等。在这种状况下公司内部已经自建了比较完善的基础架构平台就不必总体更换为Spring Boot和Spring Cloud,不然代价极大,甚至会对公司的业务形成严重的后果。公司发展的策略通常都是以客户(用户)稳定优先,但公司技术也须要更新,能够先尝试在公司边缘业务中使用,达到承认后逐步推广,循环渐进。


笔者在进行微服务改造的过程当中其实是基于原有的Dubbo作的改进,将Duboo和Spring Boot相结合造成服务治理框架。


  • 消息服务


咱们在谈技术选型的时候,不能脱离业务空谈选型,每种消息中间件一定有其优势和不足,咱们能够根据自身的场景择优选择,下面笔者结合本身使用的两种类型的MQ简单说一下选型与使用场景。


RabbitMQ是使用Erlang编写的一个开源的消息队列,自己支持不少协议:AMQP、XMPP、SMTP、STOMP,也正是如此,使它变得很是重量级,更适合企业级的开发。RabbitMQ是AMQP协议领先的一个实现,它实现了代理(Broker)架构,意味着消息在发送到客户端以前能够在中央节点上排队。对路由(Routing)、负载均衡(Load balance)或数据持久化都有很好的支持。可是在集群中使用的时候,分区配置不当偶尔会有脑裂现象出现,总的来讲,在支付行业用RabbitMQ仍是很是多的。


Kafka是LinkedIn于2010年12月开发并开源的一个分布式MQ系统,如今是Apache的一个孵化项目,是一个高性能跨语言分布式Publish/Subscribe消息队列系统,其性能和效率在行业中是领先的,可是原先的版本通过大量测试,由于其主备Partition同步信息的机制问题,偶尔会形成数据丢失等问题,因此更多的应用场景仍是在大数据、监控等领域。


目前市面上有不少支付公司都在使用RabbitMQ做为消息中间件,虽然很“重”可是却具备支付行业的不丢消息、MQ相对稳定等特色。缺点则是不像ActiveMQ那样可使用Java实现定制化,好比想知道消息队列中有多少剩余消息没有消费,哪些通道获取过消息,共有多少条,是否能够手动或自动触发重试等,还有监控和统计信息,目前作得还不是太完善,只能知足基本功能的要求。


接下来咱们再来讲说消息队列在技术领域的使用场景。


(1)能够作延迟设计。

好比一些数据须要过五分钟后再使用,这时就须要使用延迟队列设计,好比在RabbitMQ中利用死信队列实现。

(2)异步处理。

主要应用在多任务执行的场景。

(3)应用解耦。

在大型微服务架构中,有一些无状态的服务常常考虑使用MQ作消息通知和转换。

(4)分布式事务最终一致性。

可使用基于消息中间件的队列作分布式事务的消息补偿,实现最终一致性。

(5)流量削峰。

通常在秒杀或团抢活动中使用普遍,能够经过队列控制秒杀的人数和商品,还能够缓解短期压垮应用系统的问题。

(6)日志处理。

咱们在作监控或日志采集的时候常常用队列来作消息的传输和暂存。


  • 统一配置中心


目前市面有不少种开源的统一配置中心组件可供使用,如携程开源的Apollo、阿里的Diamond、百度的Disconf,每种组件都各有特色,咱们在使用的过程当中还须要根据实际状况来综合考量。笔者公司目前采用的微服务架构是Spring Boot+Dubbo的方式,Apollo的架构使用了Spring Boot+SpringCloud的方式,在架构方式上正好能够无缝对接,同时Apollo能够解决同城双活方面的问题,因此从这些角度来看比较适合目前的场景。


  • 银行通道监控与切换


因为每家银行提供的业务及产品不一样,例如B2C、B2B、大额支付、银企直连、代收代付、快捷支付等,这些产品及服务并没有统一的接口,要使用这些产品服务,支付机构只能一家家银行进行接入,当对接的银行通道过多时,每条通道的稳定性就是支付工做中的重中之重,这是涉及用户支付是否成功的关键,也是支付机构支付成功率的重要指标,基于此,要有针对性地进行银行通道稳定性的监控与故障切换系统的建设,如图11-5所示。


640?wx_fmt=png

图11-5


图11-5是通道监控与切换系统的总体架构,经过在相应组件或应用上面增长Agent监控代理拦截通道的请求状况,通过Collector进行数据汇总,而后将通道评分数据发送给Redis集群,而支付路由系统在进行通道选取的时候会从Redis集群中获取通道的评分及通道相应的配置项进行综合评定从而选取合适的通道,另外采集全部的监控数据都会存放到InfluxDB中,经过Grafana进行预警展现,若是通道不可用则自动将通道关闭,同时通知研发部门进行问题排查。


4、从代码层面提高微服务架构的性能



不少架构变迁或演进方面的文章大可能是针对架构方面的介绍,不多有针对代码级别的性能优化介绍,这就比如盖楼同样,楼房的基础架子搭得很好,可是盖房的工人不够专业,有不少须要注意的地方忽略了,在往里面添砖加瓦的时候出了问题,后果就是房子常常漏雨、墙上有裂缝等各类问题出现,虽然不至于楼房塌陷,但楼房已经变成了危楼。


判断一个项目是否具备良好的设计须要从优秀的代码和高可用架构两个方面来衡量,如图11-6所示。


640?wx_fmt=png

图11-6


优秀的代码是要看程序的结构是否合理,程序中是否存在性能问题,依赖的第三方组件是否被正确使用等。而高可用架构是要看项目的可用性、扩展型,以及可以支持的并发能力。能够说一个良好的项目设计是由两部分组成的,缺一不可。


4.1 从代码和设计的角度看


在实战的过程当中,不一样的公司所研发的项目和场景也不同,下面主要以支付场景为出发点,从代码和设计的角度总结一些常见的问题。


1)数据库常常发生死锁现象


以MySQL数据库为例,select......for update语句是手工加锁(悲观锁)语句,是一种行级锁。一般状况下单独使用select语句不会对数据库数据加锁,而使用for update语句则能够在程序层面实现对数据的加锁保护,若是for update语句使用不当,则很是容易形成数据库死锁现象的发生,如表11-1所示。


640?wx_fmt=png


在上述事例中,会话B会抛出死锁异常,死锁的缘由就是A和B两个会话互相等待,出现这种问题其实就是咱们在项目中混杂了大量的事务+for update语句而且使用不当所形成的。


MySQL数据库锁主要有三种基本锁。


  • Record Lock:单个行记录的锁。

  • Gap Lock:间隙锁,锁定一个范围,但不包括记录自己。

  • Gap Lock+Record Lock(next-key lock):锁定一个范围,而且也锁定记录自己。


当for update语句和gap lock、next-key lock锁相混合使用,又没有注意用法的时候,就很是容易出现死锁的状况。


2)数据库事务占用时间过长


先看一段伪代码:



 
 
public void test() {	
    Transaction.begin //事务开启	
    try {	
        dao.insert //插入一行记录	
       httpClient.queryRemoteResult() //请求访问	
        dao.update //更新一行记录	
        Transaction.commit()//事务提交	
    } catch(Exception e) {	
          Transaction.rollFor//事务回滚	
    } 	
}


项目中相似这样的程序有不少,常常把相似httpClient,或者有可能形成长时间超时的操做混在事务代码中,不只会形成事务执行时间超长,并且会严重下降并发能力。

咱们在使用事务的时候,遵循的原则是快进快出,事务代码要尽可能小。针对以上伪代码,咱们要把httpClient这一行拆分出来,避免同事务性的代码混在一块儿。


3)滥用线程池,形成堆和栈溢出


Java经过Executors提供了四种线程池可供咱们直接使用。


  • newCachedThreadPool:建立一个可缓存线程池,这个线程池会根据实际须要建立新的线程,若是有空闲的线程,则空闲的线程也会被重复利用。

  • newFixedThreadPool:建立一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

  • newScheduledThreadPool:建立一个定长线程池,支持定时及周期性任务执行。

  • newSingleThreadExecutor:建立一个单线程化的线程池,它只会用惟一的工做线程来执行任务,保证全部任务按照指定顺序(FIFO,LIFO,优先级)执行。


JDK提供的线程池从功能上替咱们作了一些封装,也节省了不少参数设置的过程。若是使用不当则很容易形成堆和栈溢出的状况,示例代码以下所示。

 


 
 
private staticfinal ExecutorService executorService = Executors.newCachedThreadPool();	
 /**	
 * 异步执行短频快的任务	
 * @param task	
 */	
 public static voidasynShortTask(Runnable task){	
 executorService.submit(task);	
  //task.run();	
 }	
 	
 CommonUtils.asynShortTask(newRunnable() {	
      @Override	
      public void run() {	
          String sms =sr.getSmsContent();	
         sms = sms.replaceAll(finalCode, AES.encryptToBase64(finalCode,ConstantUtils.getDB_AES_KEY()));	
         sr.setSmsContent(sms);	
         smsManageService.addSmsRecord(sr);	
      }	
 });


以上代码的场景是每次请求过来都会建立一个线程,将DUMP日志导出进行分析,发现项目中启动了一万多个线程,并且每一个线程都显示为忙碌状态,已经将资源耗尽。咱们仔细查看代码会发现,代码中使用的线程池是使用如下代码来申请的。

 

 
 
private static final ExecutorServiceexecutorService = Executors.newCachedThreadPool();

 

在高并发的状况下,无限制地申请线程资源会形成性能严重降低,采用这种方式最大能够产生多少个线程呢?答案是Integer的最大值!查看以下源码:

 

 
 
public static ExecutorServicenewCachedThreadPool() {	
      return newThreadPoolExecutor(0, Integer.MAX_VALUE,	
                                       60L, TimeUnit.SECONDS,	
                                       newSynchronousQueue<Runnable>());	
  }


既然使用newCachedThreadPool可能带来栈溢出和性能降低,若是使用newFixedThreadPool设置固定长度是否是能够解决问题呢?使用方式如如下代码所示,设置固定线程数为50:

 

 
 
private static final ExecutorServiceexecutorService = Executors.newFixedThreadPool(50);

 

修改完成之后,并发量从新上升到100TPS以上,可是当并发量很是大的时候,项目GC(垃圾回收能力降低),分析缘由仍是由于Executors.newFixedThreadPool(50)这一行,虽然解决了产生无限线程的问题,但采用newFixedThreadPool这种方式会形成大量对象堆积到队列中没法及时消费,源码以下:

 


 
 
public static ExecutorService newFixedThreadPool(int nThreads,ThreadFactory threadFactory) {	
        return newThreadPoolExecutor(nThreads, nThreads,	
                                        0L, TimeUnit.MILLISECONDS,	
                                         newLinkedBlockingQueue<Runnable>(),	
                                         threadFactory);	
 }


能够看到采用的是无界队列,也就是说队列能够无限地存放可执行的线程,形成大量对象没法释放和回收。


其实JDK还提供了原生的线程池ThreadPoolExecutor,这个线程池基本上把控制的权力交给了使用者,使用者设置线程池的大小、任务队列、拒绝策略、线程空闲时间等,无论使用哪一种线程池,都是创建在咱们对其精准把握的前提下才能真正使用好。


4)经常使用配置信息依然从数据库中读取


无论是什么业务场景的项目,只要是老项目,咱们常常会遇到一个很是头疼的问题就是项目的配置信息是在本地项目的properties文件中存放的,或者是将经常使用的配置信息存放到数据库中,这样形成的问题是:


  • 若是使用本地properties文件,每次修改文件都须要一台一台地在线上环境中修改,在服务器数量很是多的状况下很是容易出错,若是修改错了则会形成生产事故。

  • 若是是用采集数据库来统一存放配置信息,在并发量很是大的状况下,每一次请求都要读取数据库配置则会形成大量的I/O操做,会对数据库形成较大的压力,严重的话对项目也会产生性能影响。


比较合理的解决方案之一:使用统一配置中心利用缓存对配置信息进行统一管理,具体的实现方案能够参考《深刻分布式缓存》这本书。


5)从库中查询数据,每次所有取出


咱们在代码中常常会看到以下SQL语句:

 

 
 
select * from order where status = 'init'

 

这句SQL从语法上确实看不出什么问题,可是放在不一样的环境上却会产生不一样的效果,若是此时咱们的数据库中状态为init的数据只有100条,那么这条SQL会很是快地查询出来并返回给调用端,在这种状况下对项目没有任何影响。若是此时咱们的数据库中状态为init的数据有10万条,那么这条SQL语句的执行结果将是一次性把10万记录所有返回给调用端,这样作不只会给数据库查询形成沉重的压力,还会给调用端的内存形成极大的影响,带来很是很差的用户体验。


比较合理的解决方案之一:使用limit关键字控制返回记录的数量。


6)业务代码研发不考虑幂等操做


幂等就是用户对于同一操做发起的一次请求或屡次请求所产生的结果是一致的,不会由于屡次点击而产生多种结果。


以支付场景为例,用户在网上购物选择完商品后进行支付,由于网络的缘由银行卡上面的钱已经扣了,可是网站的支付系统返回的结果倒是支付失败,这时用户再次对这笔订单发起支付请求,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条,这种场景就不是幂等。


实际工做中的幂等其实就是对订单进行防重,防重措施是经过在某条记录上加锁的方式进行的。


针对以上问题,彻底没有必要使用悲观锁的方式来进行防重,不然不只对数据库自己形成极大的压力,对于项目扩展性来讲也是很大的扩展瓶颈,咱们采用了三种方法来解决以上问题:


  • 使用第三方组件来作控制,好比ZooKeeper、Redis均可以实现分布式锁。

  • 使用主键防重法,在方法的入口处使用防重表,可以拦截全部重复的订单,当重复插入时数据库会报一个重复错,程序直接返回。

  • 使用版本号(version)的机制来防重。


注意:以上三种方式都必须设置过时时间,当锁定某一资源超时的时候,可以释放资源让竞争从新开始。


7)使用缓存不合理,存在惊群效应、缓存穿透等状况


  • 缓存穿透


咱们在项目中使用缓存一般先检查缓存中数据是否存在,若是存在则直接返回缓存内容,若是不存在就直接查询数据库,而后进行缓存并将查询结果返回。若是咱们查询的某一数据在缓存中一直不存在,就会形成每一次请求都查询DB,这样缓存就失去了意义,在流量大时,可能DB就“挂掉”了,这就是缓存穿透,如图11-7所示。


640?wx_fmt=png

图11-7


要是有黑客利用不存在的缓存key频繁攻击应用,就会对数据库形成很是大的压力,严重的话会影响线上业务的正常进行。一个比较巧妙的作法是,能够将这个不存在的key预先设定一个值,好比“key”“NULL”。在返回这个NULL值的时候,应用就能够认为这是不存在的key,应用就能够决定是继续等待访问,仍是放弃掉此次操做。若是继续等待访问,则过一个时间轮询点后,再次请求这个key,若是取到的值再也不是NULL,则能够认为这时候key有值了,从而避免透传到数据库,把大量的相似请求挡在了缓存之中。


  • 缓存并发


看完上面的缓存穿透方案后,可能会有读者提出疑问,若是第一次使用缓存或缓存中暂时没有须要的数据,那么又该如何处理呢?


在这种场景下,客户端从缓存中根据key读取数据,若是读到了数据则流程结束,若是没有读到数据(可能会有多个并发都没有读到数据),则使用缓存系统中的setNX方法设置一个值(这种方法相似加锁),没有设置成功的请求则“sleep”一段时间,设置成功的请求则读取数据库获取值,若是获取到则更新缓存,流程结束,以前sleep的请求唤醒后直接从缓存中读取数据,此时流程结束,如图11-8所示。


640?wx_fmt=png

图11-8


这个流程里面有一个漏洞,若是数据库中没有咱们须要的数据该怎么处理?若是不处理请求则会形成死循环,不断地在缓存和数据库中查询,这时就能够结合缓存穿透的思路,这样其余请求就能够根据“NULL”直接进行处理,直到后台系统在数据库成功插入数据后同步更新清理NULL数据和更新缓存。


  • 缓存过时致使惊群效应


咱们在使用缓存组件的时候,常常会使用缓存过时这一功能,这样能够不按期地释放使用频率很低的缓存,节省出缓存空间。若是不少缓存设置的过时时间是同样的,就会致使在一段时间内同时生成大量的缓存,而后在另一段时间内又有大量的缓存失效,大量请求就直接穿透到数据库中,致使后端数据库的压力陡增,这就是“缓存过时致使的惊群效应”!


比较合理的解决方案之一:为每一个缓存的key设置的过时时间再加一个随机值,能够避免缓存同时失效。


  • 最终一致性


缓存的最终一致性是指当后端的程序在更新数据库数据完成以后,同步更新缓存失败,后续利用补偿机制对缓存进行更新,以达到最终缓存的数据与数据库的数据是一致的状态。


经常使用的方法有两种,分别是基于MQ和基于binlog的方式。


(1)基于MQ的缓存补偿方案。


这种方案是当缓存组件出现故障或网络出现抖动的时候,程序将MQ做为补偿的缓冲队列,经过重试的方式机制更新缓存,如图11-9所示。


640?wx_fmt=png

图11-9


说明:


  • 应用同时更新数据库和缓存。

  • 若是数据库更新成功,则开始更新缓存;若是数据库更新失败,则整个更新过程失败。

  • 判断更新缓存是否成功,若是成功则返回。

  • 若是缓存没有更新成功,则将数据发到MQ中。

  • 应用监控MQ通道,收到消息后继续更新Redis。


问题点:


若是更新Redis失败,同时在将数据发到MQ以前应用重启了,那么MQ就没有须要更新的数据,若是Redis对全部数据没有设置过时时间,同时在读多写少的场景下,那么只能经过人工介入来更新缓存。


(2)基于binlog的方式来实现统一缓存更新方案。


第一种方案对于应用的研发人员来说比较“重”,须要研发人员同时判断据库和Redis是否成功来作不一样的考虑,而使用binlog更新缓存的方案可以减轻业务研发人员的工做量,而且也有利于造成统一的技术方案,如图11-10所示。

 

640?wx_fmt=png

图11-10


说明:


  • 应用直接写数据到数据库中。

  • 数据库更新binlog日志。

  • 利用Canal中间件读取binlog日志。

  • Canal借助于限流组件按频率将数据发到MQ中。

  • 应用监控MQ通道,将MQ的数据更新到Redis缓存中。

能够看到这种方案对研发人员来讲比较轻量,不用关心缓存层面,虽然这个方案实现起来比较复杂,但却容易造成统一的解决方案。


问题点:


这种方案的弊端是须要提早约定缓存的数据结构,若是使用者采用多种数据结构来存放数据,则方案没法作成通用的方式,同时极大地增长了方案的复杂度。


8)程序中打印了大量的无用日志,而且引发性能问题


先来看一段伪代码:

 


 
 
QuataDTO quataDTO = null;	
try {	
   quataDTO = getRiskLimit(payRequest.getQueryRiskInfo(),payRequest.getMerchantNo(), payRequest.getIndustryCatalog(),cardBinResDTO.getCardType(), cardBinResDTO.getBankCode(), bizName);	
} catch (Exception e) {	
    logger.info("获取风控限额异常", e);	
}


经过上面的代码,发现了如下须要注意的点:


  • 日志的打印必须以logger.error或logger.warn的方式打印出来。

  • 日志打印格式:[系统来源] 错误描述 [关键信息],日志信息要打印出能看懂的信息,有前因和后果。甚至有些方法的入参和出参也要考虑打印出来。

  • 在输入错误信息的时候,Exception不要以e.getMessage的方式打印出来。


合理地日志打印,能够参考以下格式:

 

 
 
logger.warn("[innersys] - ["+ exceptionType.description + "] - [" + methodName + "] - "	
                +"errorCode:[" + errorCode + "], "	
                +"errorMsg:[" + errorMsg + "]", e);	
 	
logger.info("[innersys] - [入参] - [" +methodName + "] - "	
                    + LogInfoEncryptUtil.getLogString(arguments)+ "]");	
 	
logger.info("[innersys] - [返回结果] - [" +methodName + "] - " + LogInfoEncryptUtil.getLogString(result));


在程序中大量地打印日志,虽然可以打印不少有用信息帮助咱们排查问题,但日志量太多不只影响磁盘I/O,还会形成线程阻塞,对程序的性能形成较大影响。在使用Log4j1.2.14设置ConversionPattern的时候,使用以下格式:

 

 
 
%d %-5p %c:%L [%t] - %m%n

 

在对项目进行压测的时候却发现了大量的锁等待,如图11-11所示。

 

640?wx_fmt=png

图11-11


对Log4j进行源码分析,发如今org.apache.log4j.spi.LocationInfo类中有以下代码:

 


 
 
String s;	
// Protect against multiple access to sw.	
synchronized(sw) {	
 t.printStackTrace(pw);	
 s = sw.toString();	
 sw.getBuffer().setLength(0);	
}	
//System.out.println("s is ["+s+"].");	
int ibegin, iend;


能够看出在该方法中用了synchronized锁,而后又经过打印堆栈来获取行号,因而将ConversionPattern的格式修改成%d %-5p %c [%t] - %m%n后,线程大量阻塞的问题解决了,极大地提升了程序的并发能力。


9)关于索引的优化


  • 组合索引的原则是偏左原则,因此在使用的时候须要多加注意。

  • 不须要过多地添加索引的数量,在添加的时候要考虑汇集索引和辅助索引,二者的性能是有区别的。

  • 索引不会包含NULL值的列。

只要列中包含NULL值都不会被包含在索引中,复合索引中只要有一列含有NULL值,那么这一列对于此复合索引就是无效的。因此咱们在设计数据库时不要让字段的默认值为NULL。

  • MySQL索引排序。

MySQL查询只使用一个索引,若是where子句中已经使用了索引,那么order by中的列是不会使用索引的。所以数据库默认排序能够在符合要求的状况下不使用排序操做;尽可能不要包含多个列的排序,若是须要,最好给这些列建立复合索引。

  • 使用索引的注意事项。

如下操做符能够应用索引:

m  大于等于;

m  Between;

m  IN;

m  LIKE 不以%开头。

如下操做符不能应用索引:

m  NOT IN;

m  LIKE %_开头。


4.2 从总体架构的角度看


1)采用单体集群的部署模式


当团队和项目发展到必定规模后,就须要根据业务和团队人数进行适当拆分。若是依然使用单体项目作总体部署,则项目之间互相影响极大,再加上团队人员达到必定规模后,没有办法进行项目的维护和升级。


2)采用单机房的部署方式


如今互联网项目对稳定性的要求愈来愈高,采用单机房部署的风险性也愈来愈高,像黑客恶意攻击、机房断电、网线损坏等不可预知的故障发生时,单机房是没法提供稳定性保障的,这就须要互联网企业开始建设同城双活、异步多活等确保机房的稳定性。


3)采用Nginx+Hessian的方式实现服务化


Hessian是一个轻量级的Remoting on HTTP框架,采用的是Binary RPC协议。由于其易用性等特色,直到如今依然有不少企业还在使用Hessian做为远程通讯工具,但Hessian并不具有微服务的特色,只做为远程通讯工具使用,并且Hessian多偏重于数据如何打包、传输与解包,因此不少时候须要借助Nginx来作服务路由、负载和重试等,并且还须要在Nginx中进行配置,也不能动态对服务进行加载和卸载,因此在业务愈来愈复杂,请求量愈来愈多的状况下,Hessian不太适合做为微服务的服务治理框架,这时就须要Spring Cloud或Dubbo了。


4)项目拆分不完全,一个Tomcat共用多个应用(见图11-12)

 

640?wx_fmt=png

图11-12


注:一个Tomcat中部署多个应用war包,彼此之间互相牵制,在并发量很是大的状况下性能下降很是明显,如图11-13所示。


640?wx_fmt=png

图11-13


注:拆分前的这种状况其实仍是挺广泛的,以前一直认为项目中不会存在这种状况,但事实上仍是存在了。解决的方法很简单,每个应用war只部署在一个Tomcat中,这样应用程序之间就不会存在资源和链接数的竞争状况,性能和并发能力提高较为明显。


5)无服务降级策略


举个例子来讲明什么是服务降级,咱们要出门旅游但只有一只箱子,咱们想带的东西太多了把箱子都塞满了,结果发现还有不少东西没有放,因而只能把全部东西所有再拿出来作对比和分类,找到哪些是必需要带的,哪些是非必需的,最终箱子里面放满了必需品,为了防止这种状况再次发生,下次再旅游的时候就能够提早多准备几只箱子。其实服务降级也是相似的思路,在资源有限的状况下舍弃一些东西以保证更重要的事情可以进行下去。


服务降级的主要应用场景就是当微服务架构总体的负载超出了预设的上限阈值或即将到来的流量预计超过预设的阈值时,为了保证重要的服务能正常运行,将一些不重要、不紧急的服务延迟或暂停使用。


6)支付运营报表,大数据量查询


咱们先来回顾一下微服务的数据去中心化核心要点:


  • 每一个微服务有本身私有的数据库。

  • 每一个微服务只能访问本身的数据库,而不能访问其余服务的数据库。

  • 某些业务场景下,请求除了要操做本身的数据库,还要对其余服务的数据库进行添加、删除和修改等操做。在这种状况下不建议直接访问其余服务的数据库,而是经过调用每一个服务提供的接口完成操做。

  • 数据的去中心化进一步下降了微服务之间的耦合度。


经过上述核心要点能够看到,微服务中关于数据的描述是去中心化,也就是说要根据业务属性独立拆分数据库,使其业务领域与数据库的关系是一一对应的。咱们仍是以支付业务场景为例,单体支付项目进行微服务改造后,业务架构如图11-14所示。


640?wx_fmt=png

图11-14


能够看到将单体支付项目进行微服务改造后增长了多个服务项目,咱们能够把每一个服务项目都理解为一个限界上下文,每一个服务项目又对应一个数据库,这样数据库由原来适应单体支付系统的大库拆分红了多个独立的数据库。问题来了,对于后台运营统计来讲这就是噩梦的开始,由于运营报表常常会跨业务进行统计和汇总,在原有运营系统上面作报表会给运营人员额外增长巨大的工做量,须要逐库进行统计,而后进行汇总。


凡事都有两面性,微服务给咱们带来去中心化高度解耦的同时,也会带来报表数据及历史数据没法统一汇总和查询的问题,这时咱们就须要从各个服务数据库中抽取数据到大数据平台作数据集中化,如图11-15所示。


640?wx_fmt=png

图11-15


一般大数据平台也会和每一个服务的读库配合使用,大数据平台存放的每每是大而全的数据。能够把大数据平台理解为一个数据仓库里面存放若干年的数据,研发人员能够根据数据量的大小及业务状况合理利用服务的读库,这样也能够减轻查询大数据平台的压力。好比用户要查询某个服务一周内的订单状况,则能够直接从读库中进行查询,这样既能够查询到最新的订单详细信息,也能够充分发挥读库的做用。若是用户要查询半年以上的数据,由于数据量大的缘由历史数据早已经被迁移走,这时能够在大数据平台进行查询。


7)运维手动打包和上线


微服务架构的顺利实施还须要强有力的运维作支撑,这就至关于一辆宝马车表面看上去特别豪华,但里面装的倒是老旧的发动机。这时就须要将DevOps在全公司推广,让自动化运维和部署成为微服务的“发动机”。


5、微服务架构中常见的一些故障分析技巧



1)开发者的自测利器——hprof命令


640?wx_fmt=png


示例程序以下所示。


注:这是一段测试代码,经过sleep方法进行延时。


如何分析程序中哪块代码出现延时故障呢?


在程序中加上以下运行参数:

 

640?wx_fmt=png


再次运行程序,发如今工程目录里面多了一个文本文件java.hprof.txt,打开文件,内容以下所示。


640?wx_fmt=png


注:经过上面内容能够看到是哪一个类的方法执行时间长,耗费了CPU时间,一目了然,方便咱们快速定位问题。


hprof不是独立的监控工具,它只是一个Java Agent工具,它监控Java应用程序在运行时的CPU信息和堆内容,使用Java -agentlib:hprof=help命令能够查看hprof的使用文档。


上面的例子统计的是CPU时间,一样咱们还能够统计内存占用的dump信息。例如:-agentlib:hprof=heap,format=b,file=/test.hprof。


咱们在用JUnit自测代码的时候结合hprof,既能够解决业务上的bug,又可以在必定程度上解决可发现的性能问题,很是实用。


2)性能排查工具——pidstat


示例代码以下所示。


640?wx_fmt=png


将示例代码运行起来后,在命令行中输入:

 


 
 
pidstat -p 843 1 3 -u -t	
/*	
-u:表明对CPU使用率的监控	
参数1 3表明每秒采样一次,一共三次	
-t:将监控级别细化到线程	
*/

 

结果如图11-16所示。

 

640?wx_fmt=png

 图11-16


注:其中TID就是线程ID,%usr表示用户线程使用率,从图中能够看到855这个线程的CPU占用率很是高。


再次在命令行中输入命令:

 

 
 
jstack -l 843 > /tmp/testlog.txt

 

查看testlog.txt,显示以下所示的内容。

 

注:咱们关注的是日志文件的NID字段,它对应的就是上面说的TID,NID是TID的16进制表示,将上面的十进制855转换成十六进制为357,在日志中进行搜索看到以下内容。

 

以此能够推断出有性能瓶颈的问题点。


出处:架构文摘(ID:ArchDigest)


资源下载

关注公众号:数据和云(OraNews)回复关键字获取

2018DTCC , 数据库大会PPT

2018DTC,2018 DTC 大会 PPT

ENMOBK《Oracle性能优化与诊断案例》

DBALIFE ,“DBA 的一天”海报

DBA04 ,DBA 手记4 电子书

122ARCH ,Oracle 12.2体系结构图

2018OOW ,Oracle OpenWorld 资料

产品推荐

云和恩墨Bethune Pro企业版,集监控、巡检、安全于一身,你的专属数据库实时监控和智能巡检平台,漂亮的不像实力派,你值得拥有!


640?wx_fmt=jpeg


云和恩墨zData一体机现已发布超融合版本和精简版,支持各类简化场景部署,零数据丢失备份一体机ZDBM也已发布,欢迎关注。


640?wx_fmt=jpeg

云和恩墨大讲堂 | 一个分享交流的地方

长按,识别二维码,加入万人交流社群


640?wx_fmt=jpeg

请备注:云和恩墨大讲

相关文章
相关标签/搜索