信息系统实践手记5-CACHE设计一例

说明:

正文:

  • 信息系统实践手记系列是系笔者在平时研发中先后遇到的大小的问题,其中比较典型的内容加以收集和分享。
  • 信息系统实践手记目录:博客园(或查看源码的README.MD文件)

摘要:

  • 此文描述了对CACHE缓存的一些理解。

正文

1.CACHE是啥?

最近一直在了解scala,Spark,Python,数据挖掘,以及复杂科学的一些内容,在充实和忙碌之余,虽兴趣浓厚,但体力不支,疲劳的很,无奈耽搁了实践手记。
原本计划2周挂出一篇来分享,实在抱歉。要分享的东西太多,码字太费时间,有时候甚至觉得开会也许更好(很多人反感开会,那是因为没开过高效的会)!其实很多东西主要还是考自己去走,去体会。
闲话不说,切入正题。
很多研发人员,尤其是开发程序的同志,找到一个cache框架或类库可用,比如hibernate的cache功能,然后就一股脑用上去。虽然会有效果,很愉悦,是好事,既增加经验,也体会了框架的魅力和效率,
但其实还有东西可以进一步去领会。究竟为啥要用CACHE?CACHE是啥?什么样的场景用什么样的CACHE?很少有人问。
那CACHE到底是啥呢?还真难于表述清晰,但有一些要点:
  • CACHE的产生是为了缓解处理链上不同节点之间速度不协调的情况。举例若干:

    • 计算机系统:一般有速度梯度:CPU内部寄存器(register)IO>内存IO>磁盘IO>网络IO,为协调它们之间传递数据的速度差,就可设置缓存结构,叫CACHE;
      • 说明1:以上说的是一般情况,不考虑特别的磁阵RAID,高速SCSI,千兆或万兆网络等;
      • 说明2:CPU内部也分L1,L2,L3等多级寄存器结构,当做多级CACHE使用,早先的386/486处理器没分那么多层次;
    • 地铁系统:有时候出事故,人特多,站内站外由管理员把守入口分批放人到1层大厅,则1层大厅是一个CACHE;管理员分批从大厅放人到B1层站台,则B1站台也是一个CACHE;当CACHE快撑满的时候,管理员就会喊“慢慢来,不要挤,暂时关闭,大家耐心等待!”,于是人就被囤积在不同的CACHE里,以便协调地铁运量和人流涌入的速度差问题。
    • 工厂流水线:另一个典型例子是工厂的流水线。以前老式流水线每个工位站1个人,处理1个装配动作,比如给黄小鸭玩具装上1条腿。一个工件,如果流经10个处理节点,那就会有10个人来处理10个步骤。显然,老板期望流水线一直处于充满状态,则最高效的源源不断有产出。但意外总会发生,比如某工件N流转到Px节点,Px节点的工人很累,处理较慢,导致Px-1步骤流出的工件不断堆积,而这个Px处理节点处理台的工件堆积场所,就是一个BUFFER缓存区(也就是CACHE),它越来越满,就提醒Px的工人,加紧干,否则一旦CACHE撑爆,就无法克服(追上)速度差(时间差)的问题!(画外音:可以看出流水线机制的高效和科学,以及“残酷”)。
    • 生产者消费者模式:经典的程序设计模式“生产者和消费者”模型也有一个CACHE的设计,就是协调生产和消费的速度差,类似“工厂流水线”
  • CACHE实际上是用空间上的代价来缓解时间上的不协调。

    • 你肯定要问“既然速度有差异,那CACHE再大也没用?”,的确,再大的CACHE也是用来临时克服速度差异的,它期望可以用空间暂时的堆积追回偶发导致的时间不协调(速度差是绝对的,这个在设计场景之前就知道,但速度不协调是偶发的,这个要区别开!)。如果永远无法“追回来”,那么再大的CACHE也会有问题,那么就是设计上的问题,就不是CACHE机制可以解决的。
    • 处理节点Px往往速度不同,所以必须用不同算法来安排。比如:
      • 通过编译器编译的源码,会根据OS的不同及其内存页面调度算法,互相结合,处理好代码的摆放,并充分考虑到CPU/MEM/DISK/NETWORK的差异;保持MEM代码段高命中,降低页丢失中断,等等。
      • 程序本身也应该充分利用诸如“多线程”,“并发计算”,“分布式计算”等方法,来从宏观的算法层面,解决掉速度差异,从而适当的利用CACHE来防止偶发的速度不协调,而不是算法本身有永久性的硬伤“速度不匹配”。可以适当的使用一些主流成熟前三名的框架来解决实际问题。
      • 程序终究是要达成业务需要的,也就是完成功能目标(Feature)。而功能和业务本身是有特点的,有时空特性的,是高并发业务?还是随机业务?业务的时空特性如何?是否和上下班高峰有关? 是否和网站的维护周期有关?是否和购物的时间段有关?总之从业务触发,需要专业的领域知识(行业支持),并且需要深厚的技术功底,这样的从场景出发的系统分析和系统设计行为,是何其重要!这方面的人才非常紧缺。
  • CACHE所处理的时空不协调,可能是偶发的,也可能是来自于业务模型;

    • 通过上述的不同层次(编译器,OS,程序,框架类库,业务场景分析)的讨论,基本上能妥善的处理业务,达成场景,使用必要的CACHE结构。但业务模型总是会偶发时空不一致,尤其是在当今多任务的计算机架构(以前主机时代只有大型机是多进程的,现在微机也是多任务并细化到线程)。很容易计算力不够需要增加更多CPU的core,数据量大没地方放,就增加MEM,MEM增加后也会导致低速的磁盘IO和网络IO也变大。
    • 另外在对业务模型不断调优和扩容的过程中,不协调也会自然产生的,而如果程序模型比较优秀,相关必要的地方本来就有CACHE,那么CACHE将为程序运作提供余地。
    • 即便是简单的业务模型,而且调优完成,且一直正常运作。但也可能因为“网络故障,CPU失效,内存或磁盘损坏,某个计算节点离线”等问题而打破平衡。这时候,甚至连写LOG日志来记录状态也显得很奢侈或捉襟见肘。此时,如果程序在框架设计上已经未雨绸缪的安排了充分和必要的CACHE,则完全可以提升健壮性,甚至在偶发灾难冲击中坚强运行,恢复运作。
  • 客观世界和程序世界都是复杂的,而复杂系统经常引发意外和不协调,CACHE则应运而生:

    • CPU里面的微指令集中用到了L1,L2,L3,它们就是各级CACHE;
    • MEM,DISK,网卡等设备,无论在硬件的接口驱动程序BIOS中,还是在程序员编制的应用程序中,都安排了CACHE,来处理设备受到外界速度不匹配的冲击。
    • 程序设计中很多模式,比如消费者生产者,就必然有CACHE存在,以缓解生产和消费的速度不匹配;
    • 真实世界的仓库存货,小超市囤货,高架路上的紧急停车带,上下岔道区域,展览会门口的回形排队护栏,流水线的每个节点的BUFFER缓冲带等等,都是CACHE的好例子;

只有理解了自然现象,业务规律和程序能力边界,才能深入体会如何在程序设计中,在什么样的业务场景下使用怎样的CACHE设计,以便到达好处和结果。不建议只为了技术用技术,必须明确使用的目的为何,才能正确的使用。

2.CACHE设计一例

这里简单举一个例子,还是前几篇分享的客户端和服务器,它们是提供GIS地图服务的服务器和客户端;而这个提供GIS业务的服务器就用到很多CACHE; 撇开客户端业务展现不说,很多后台服务是由做GIS业务的应用SERVER提供,简称为GS,而GS需要从设备管理平台,简称DS获取设备信息;

  • 因为DS不提供设备信息的订阅(很遗憾),则无法使用高效的观察者模式来获取设备变更信息,而只能由GS通过阶段同步(轮询模型)来花巨大的代价向DS获取其实很少变化的设备更新信息。为了阶段同步,GS只能在内存(或DB)做了一个CACHE,来存放比如每30分钟的同步信息;
  • 最先是X分钟同步,按照经典做法,同步数据放在DB。但为提升速度,解决业务场景的高速要求和DB处理的慢速情况,将DB的数据“上提”到内存的CACHE中,以后做业务直接查CACHE,处理CACHE;这里就自然的引发了一个问题,CACHE的脏数据需要同步回到DB,因为数据存在于两个地方,一个CACHE是临时的,一个DB是持久化的,那么它们之间也有同步问题,这是使用CACHE经常要面临的情况。其实最后因为原来程序框架做的不佳,导致了批量修改完成后,还遗留了不少小尾巴,只能case by case的修复,不细说。
  • 我们GS平台,还和第三方,比如一个城市的路口卡口管理平台(简称KS)对接,上次讲过平台对接问题了,这里假设顺利对接。那么KS的数据,也会阶段性的同步到我们GS来。一个典型的应用是GS向KS订阅了一种“告警A”,当KS发生事件A而发出“告警A”时,将会推送到GS平台,那么GS平台分发给客户端。这里显然有速度的不协调,如果告警频繁,GS一时处理不过来,那么就要CACHE,怎么弄?也可以不用自己写代码,我们GS和GS的客户端是通过ActiveMQ的消息中间件来传递消息。订阅之后,不同主题的消息和用户就会有自己的BUFFER来存放,即便KS平台突发大量告警A到GS,GS来不及分发,那就放入MQ自己管理的缓存队列,MQ自己处理。当然这里我们看到了上面讨论过的CACHE的几个问题:
    • 如果KS平台的“告警A”一直高频持续发送,那么最终GS接收代码来不急放入MQ,或者来得及放入MQ,但MQ终将崩溃。所以这里就遇到了前几篇提到的平台对接中,对两个有交互的平台之间的信令和数据的“时空特性”一定要定义清晰,比如我们GS和KS就定义了最高并发不能超过1秒10条(10CAPS),否则GS拒绝而不是通过CACHE尽量容纳和处理。万事都有限度,讨论清楚就行!
    • 这里还有一个隐蔽问题,大家不知道看出来没有。这里假设我GS的JAVA接收代码,从网络socket套接字获取的批量字节流,速度极快,放入MQ几乎不花时间。如果没有这个假设,而且放入速度的确很慢,那么MQ不会最终“爆炸”,首先奔溃的及时GS的接收代码片段。这个判断,一般是根据程序员的经验和业界的经验来确定的。它总是非常快,放入message,一般总是比消费处理message快不少。
  • CACHE也能存在于高层次和高抽象级别的。比如当前的hibernate库支持对象到DATA MODEL的映射,而业务是针对对象,不是直接赤裸的和数据交互的。针对对象的操作比如func1()也是可以CACHE缓存的,以便第二次同样的调用func1()的时候,极大的提升速度和效率。 这里当然也会看到一个脏数据和失效的问题,不再讨论。

END