京东服务市场是京东商户与第三方独立软件提供商(ISV)进行服务交易的在线交易平台。作为京东生态系统的重要组成部分,伴随着整个京东的快速成长,也在飞速发展。随着服务市场访问量和交易量的指数级增长,系统从原来的ALLINONE架构迅速演化为SOA架构。桶的容量由桶的最短木板决定。在高并发环境下,单个服务的性能决定了整个服务市场的性能。“可用插件列表服务”是服务市场的核心服务之一。这种服务性能优化的过程,带动了整个服务市场服务架构的演进。从宏观上看,大到系统,小到模块都是由自身+外部依赖组成,性能优化主要从自身和外部依赖两个方面进行。1、优化单线程到多线程的升级,尝试通过并行提升服务性能。根据日志分析,“服务详情”在整体调用中占用时间最多。虽然并行压缩了一些并行服务的调用时间,但是对于无法并行的“服务详情”环节,仍然没有任何改善。要改进,就要找到“商品服务”绩效不高的原因。可见,自我优化可以起到一定的作用,但外部依赖起着更决定性的作用。2.解决外部依赖冲突“商品服务”的性能不高。为什么是这样?我们先来分析一下“商品服务”的依赖。单独调用这个服务,或者压测这个服务,性能还不错,但是为什么线上性能不好呢?1、不同服务对外依赖资源冲突梳理“商品服务”依赖的资源后发现,“品类服务”使用相同的数据库资源,非调用高峰期资源充足互不影响。在大型并发环境中,两个服务开始竞争资源。分离依赖资源,不同的服务使用不同的资源,通过调用不同的数据源来解决冲突。2、同服务外部资源依赖冲突解决了两个服务对数据库资源的依赖冲突,性能有所提升,但性能始终波动较大。排除其他服务的外部资源的依赖冲突,看看“商品服务”本身是如何使用资源的。“商品服务”的所有功能完全依赖于数据库资源。服务上线后,自身的多个功能开始争夺数据库资源。根据使用场景对外部依赖资源进行解耦:保证事务一致性,继续使用MySQL。MySQL的INNODB引擎比OLTP在线事务处理更长。为了保证数据的强一致性,继续使用MySQL数据库。客户端登录用户需要获取最新的数据反馈,PIN码有固定维度。查询条件简单,可以符合KEY-VALUE方式。Redis非常适合这种场景。在大前端的非登录状态下,访问的用户不需要登录,访问量很大,更多的是为了获取服务的一些介绍。大量的数据可以容忍一定程度的延迟,所以用ES做查询支持。外部系统想要获取最新的服务变化,push方式比轮流pull方式强很多。通过MQ更改订阅服务。计算复杂,但实时性要求不高。业务统计分析系统通过大数据平台获取数据进行分析。3、建立统一的内存缓存模型计算机的世界里没有魔法。时间换空间和空间换时间是所有解决方案的基础。参考常用的MySQLINNODB引擎,为了加快查询速度,会在内存中设置一块内存作为缓冲区,查询结果会从硬盘加载到缓冲区中,下次执行相同的查询将直接使用缓冲区数据。同样,如果要提高查询响应速度,就必须在内存中缓存服务数据。单机内存是有限的,无法容纳所有的数据,服务器重启时重建整个内存的时间也是难以接受的,所以根据不同的使用场景选择了Redis和ES来构建内存缓存。1、选择主动缓存的定时缓存方案:查询构造+周期失效。适用于具有大量重复查询的环境,但实际上,它在某些场景下并不能按预期工作。场景特点:每个用户只会打开一次客户端获取一次插件信息,不会频繁重复拉榜。参观集中在8:00-9:00这个时间段。使用被动缓存的后果:8点之前Redis缓存为空。8:00-9:00,第一次获取所有列表信息,所有查询直接通过缓存发送到数据库。8:00-9:00获取插件列表后,进行插件更新或权限变更。因为缓存定期过期,所以无法反馈更新。用户不断刷新插件列表,直到缓存过期获取更新结果。人为的流量高峰,Redis抵抗这些无用的人为重复调用。9点以后,缓存逐渐过期,不再使用。在测试中表现良好但在实践中无用的缓存。基于以上,缓存层决定通过主动构建来构建缓存。数据修改后,修改后的数据主动加载到Redis缓存中,缓存不再设置过期时间。有些服务每次得到结果都要经过非常繁琐的计算。如果这些繁琐的计算集中在同一个时间点,对后端资源(数据库)的负担会非常重。错峰使用资源,将构建缓存的过程分散成离散调用,密集使用时直接调用缓存获取最终结果。前文提到,“分类服务”需要多次查询数据库获取分类层级列表,对数据库的负担很大。提前构建,在创建或更改类别时重建类别层级列表,将结果存储在缓存中,使用高峰时直接获取完成的类别层级列表。2、缓存分片系统在使用一段时间后,由于业务系统的业务数据需求不一致,业务开发者开始为各个外部系统提供主动缓存。这些缓存根本不是通用的,但数量众多。每次修改服务模型时,开发人员都会花费大量时间来维护这些非通用缓存。缓存占用越来越多,但是缓存的使用率并不高。为了去除冗余和减少维护工作量,最初根据数据表的维度将每个表用作缓存。这种方案可以作为ES缓存使用,但是对于Redis缓存来说,这种缓存方式带来了很大的麻烦。数据库表的设计保证了强一致性。该表是严格按照范式构建的。数据中几乎没有冗余,并且表被裁减到很小的尺寸。查询时,通过联合查询得到整体数据。但是Redis没有联合查询的功能,所以要多次调用不同的缓存,大大降低了性能。对于查询,数据库执行一些反范式操作。由于Reids缓存可以支持查询,所以也可以做一定的冗余,将这些关联数据缓存为一个整体对象。对于服务开发者来说,主要职责是根据环境的变化不断演进服务模型。服务开发者维护一套最好最完善的服务模型并开放模型;服务调用者,尤其是只获取服务数据的调用者,可以通过自定义完整的服务模型获取自己需要的数据,每个开发者只关注自己需要关注的,大大提高了工作效率。3、缓存构建方案面临的问题:服务缓存构建和修改是非核心流程,只能异步执行,通过MQ与主流程解耦。服务属性修改入口多,通过MQ会出现操作重排序问题。业务属性修改条目较多,每次修改或增加一个条目都要进行相应的修改,对业务的侵入性强。发送MQ的时机影响事务进行中的事务性能,事务回滚时需要发送补偿;不保证交易后一定能寄出。解决方案:采用binlake方式异步构建缓存,与主进程解耦。Binlake是京东的数据异构产品,解析MySQL的binlog日志,通过MQ队列解析,通过数据变化事件传递。数据库是函数修改后唯一持久化数据的地方。你只需要监控数据库的修改就可以知道所有的服务属性修改。您不再需要关注业务,也不必担心重新排序操作。只有事务提交后才会产生binlog日志。binlog的产生表明数据修改处于某种状态,不会回滚,解决了MQ发送定时的问题。Binlog事件通过MQ发送。如果发送不成功,则不修改logoffset,下次继续发送。接收队列是一个接收确认队列,在消费完成和接收确认前会不断重试,解决发送丢失或接收后丢失的问题。前期直接解析binlog消息,根据消息内容更新数据。为了保证消费的顺序,消息传递必须只有一个队列,这样大大降低了效率,埋下了单点的隐患。解决方案是MQ不作为数据变化的承载者,而是作为通知者。当缓存构建器接收到MQ时,它会从数据库中获取最新的服务属性并将其更新到缓存中。通过拉取的方式获取完整的服务属性数据,保证了数据的完整性和一致性。主动拉取数据不局限于消息本身,也不需要保证消息的顺序,解决了效率和单点的问题。当多次修改属性时,可以在收到其他修改消息之前拉取最新数据并更新缓存数据,进一步提高了实时性。***,单向事件触发有小概率数据不一致。解决方案是采用定时比较的方式,每隔一小时(可调)通过时间戳比较当天数据与缓存数据的差异,并进行最后的补偿。4.后记解决不同服务对同一个资源的调用冲突,服务中不同场景使用不同的资源支持,建立统一的缓存层摆脱对数据库的依赖。使用不同的方法来解决建立统一缓存后如何使查询摆脱对数据库的强依赖,服务性能得到极大的提升。支持改造前调用量:支持改造后调用量:通过以上演进,“可用插件列表服务”的并发性能得到了极大的提升。2018年11月11日,10分钟内通话量暴增6倍,顺利通过。作者简介:张俊青,研发元老,热爱技术,热爱挑战。熟悉各种开源框架,有丰富的大型分布式系统架构和设计经验。卓越的性能和优雅的设计是他毕生的追求。【本文来自专栏作者张凯涛微信公众号(凯涛的博客)公众号id:kaitao-1234567】点此查看作者更多好文
