【.com原创稿件】在高并发场景下,很多人把Cache(缓存内存)当成“续命”的灵丹妙药。高并发压力大,所以上传Cache来解决并发问题。但是有的时候,即使使用了Cache,发现系统还是会卡顿、崩溃。是因为Cache技术不行吗?不是的,其实这是因为缓存的管理没有做好。2018年5月18-19日,由主办方主办的全球软件与运维技术峰会在北京召开。19日下午,在“高并发与实时处理”环节,同程艺龙机票事业群CTO王晓波发表主题演讲《高并发场景的缓存治理》。他详细阐述了如何让缓存更适合高并发使用,如何正确使用缓存,以及如何通过治理解决缓存问题。对我们来说,我们是OTA的角色,所以有很多数据需要计算处理成可销售的商品,总的来说是“goodmovers,notproducers”。因此,面对大数据并发运行的各种应用场景,我们需要使用各种缓存技术来提高服务质量。想必大家都听说过服务治理和数据治理,那么你听说过缓存治理吗?诚然,在很多场景下,Cache已经成为处处应对高并发问题的“银弹”。但它也不是放之四海而皆准的,有时会成为导致系统“挂掉”的自杀式子弹。有时候造成这种情况的原因并不是Cache本身的技术不好,而是我们没有做好治理。下面,我们将从三个方面详细讨论缓存治理:缓存使用中的一些痛点如何用好缓存,以及如何正确使用缓存,通过治理让缓存问题不可见缓存使用中的一些痛点我们同一个业务的特点是:OTA产品,没有一个价格是固定的。像酒店,客户可以订今天、明天、连续三天、两天,不管是周末还是周末,最终得到的价格是不一样的。价格随时间波动。这些波动会引起大量的计算,从而造成性能损失。要解决性能损失的问题,我们必须插入各种缓存,包括:价格缓存、时区缓存、库存缓存。而且,写入这些缓存的数据量远大于外部请求的数据量,即写多于读。下面介绍同一个进程缓存的使用历史:一开始我们只使用一个Memcache来提供缓存服务。后来我们发现Memcache存在对并发支持差、可操作性差、原子操作不足、误操作时数据不一致等问题。所以,我们改用Redis,单线程保证原子操作,数据类型更多。当一批新的业务逻辑写入Redis时,我们把它当作一个累加计数器。当然,更重要的是,把它当作一个数据库。因为数据库比较慢,所以他们让数据行先写到Redis,然后放到数据库中。后来我们发现,在单机Redis的情况下,Cache已经成为了系统的“命门”。即使上层的计算还好,即使流量不大,我们的服务也会“挂掉”。所以我们引入了集群Redis。同时,我们用Java语言开发了Redis客户端。我们还在客户端实现了二级缓存。但是,我们发现偶尔会出现不一致。后来我们也尝试了分布式缓存,将Redis部署到Docker中。最后我们发现,这些问题都与场景有关。如果你搭建的场景比较混乱,会直接导致底层无法提供服务。我们来看看需要治理的场景。通俗地说,就是什么“洞”需要“填”。早期在单机部署Redis服务时,我们部署业务系统的平台,使用脚本进行运维。当时一台虚拟机可以跑6万左右的并发数据,对于Redis服务器来说基本够用了。但是,当大量部署达到上百台时,我们遇到了两个问题:面对高并发的性能需求,我们不能仅仅依靠脚本来运维。一旦运维操作失误或失控,就可能导致Redis的主从切换失败,甚至导致服务宕机,直接影响到整个业务方。应用程序调用很乱。在采用微服务之前,我们经常面对一个包含各种模块的大型系统。在使用场景中,我们常常把Redis当做数据库,当做各种项目的数据源。同时,我们将Cache看成一个黑盒子,将各种应用数据放入其中。比如一个订单交易系统,你可能会把订单点、订单描述、订单数量等信息放到里面,这就导致大量的业务模块耦合在这里,所有的业务逻辑数据块也集中在Redis。所以即使我们拆分微服务,做代码解耦,大多数情况下缓冲池中的大数据并没有解耦,仍然是多台服务器通过Redis共享和调用数据。一旦出现宕机,即使可以降级服务,也无法降级数据本身,仍然会导致整体业务“挂”。脆弱的数据消失了。由于大家习惯把Redit当作数据库来使用(虽然大家都知道工程上不应该这样),毕竟不是数据库,没有持久化,所以一旦数据丢失,麻烦就大了.为了防止单台机器挂掉,我们可以使用多台Redis机器。这时候运维和应用有两种解决方案:运维认为:可以做“主从”,提供浮动虚拟IP(VIP)地址。当一个节点出现问题时,无需更改VIP地址,直接连接到下一个节点即可。应用认为可以在应用客户端写入两个地址,采用“哨兵”监控实现自动切换。这两种方案看似没有问题,但是经不起Redis的滥用。我们遇到过一个真实的案例:如上图右下角所示,两个Redis可以根据主从关系相互切换。根据需求,拥有20G数据的masterRedis开始同步slaveRedis。这时网络卡顿了,应用恰好发现自己的请求相应变慢了,于是上层应用根据网络故障采用主从切换。但是,此时由于主从Redis刚好处于同步状态,资源耗尽,从上次申请看来此时主从Redis是不可达的。经过深入排查,最终发现在Cache中的一张表的key中存放了20G的数据。在程序层面,他们没有控制key的消失时间(比如一周),导致key不断的增加和增加。从上面可以看出,即使我们对Redis进行了拆分,这个巨大的Key还是会存在于某个“分片”上。如上图所示,仍然以Redis为例,我们可以监控的方面包括:当前客户端连接数客户端的输出和输入是否阻塞分配的内存总量集群运行期间的状态信息master-slavereplication服务器每秒执行的命令数,可以说这些监控方面无法及时发现上述20个GKey数据。又如:通常系统只会在客户下单后增加会员积分。但是在应用设计中,核心顺序中的核心Key和应该增加滞后的点辅助进程放在了同一个实例中。由于我们只能监控延迟信息,因此无法监控高层数据与低层数据的混淆。以上是一段真实的运营与开发的对话,发生在我们内部IM上。反映了DevOps推进前运维与开发的矛盾。开发问题:为什么Redis无法访问?运维答:刚才内存故障导致服务器自动重启。其背后的原因是:Cache故障导致业务失败。业务认为自己的代码没有问题,原因出在运维的Cache上。开发问题:为什么我的Cache延迟这么大?运维解答:发现开发在这里放了几万条数据,影响了插入排序。开发问题:我写的Key找不到了?一定是缓存错误。这其实是操作维护Cache和使用Cache最大的矛盾。运维答:你的Redis已经超过了最大限制,根本就没有写成功,要不写完就直接淘汰了。这是大家把它当成黑盒子造成的问题。开发问题:为什么刚才读取全部失败?运维解答:网络暂时中断。在完全同步完成之前,从机读取全部失败。这是一个非常经典的问题。当时运维为了简单起见,把集群模式换成了主从。开发问题:我的系统需要800G的Redis,什么时候准备好?运维解答:我们的线上服务器最大容量只有256G。DevQ:为什么Redis慢如驴,是不是服务器挂了?运维解答:千万级的key,用Keys*肯定会慢。从以上可以看出,这些问题来自运维和开发两方面,也有当前技术的局限性。我们在处理并发查询时,只关注它给我们带来的“快”的性能特点,而忽略了Cache的使用规范和设计时需要考虑的各种缺点。如何用好缓存,正确使用缓存?因此,在发生重大故障后,我们得出结论:没想到一个初始状态只有3万行代码的小型Redis,能带来如此神奇的功能。以至于变成了程序员手中的“见钉子想锤的锤子”,即:他们看到任何需求都想用缓存来解决。于是他们相继开发了基于缓存的日志收集器、倒计时、计数器、订单系统等,却忘了它只是一个Cache。一旦失败,他们将如何保护自己?我们来看看缓存失效的具体因素是什么?过度依赖是指:不需要设置缓存,但必须使用缓存的地方。程序员往往会想到以后某个地方可能会有大量的并发,所以就放置了一个缓存,却忘记了将数据隔离开来并以正确的方式使用它。比如:在一些代码中,一个函数会进行一到两百次的Cache读取,通过反复的get操作不断读取同一个Key。试想一下,一次并发会给Redis带来多少操作?这些负载对于Redis来说是相当大的。数据卸载是一个经常发生的问题。因为大家确实需要一个高速的KV存储来满足数据存储需求。因此,他们会将整个Cache当作一个数据库,任何不允许丢失的数据都放在Cache中。即使公司有各种使用规定,也无法杜绝这种现象。最后我们实际上在Cache平台上创建了一个KV数据库供程序员使用,并要求他们在使用时声明是使用KV数据库还是Cache。超大容量因为大家都知道“放在内存里是最快的”,所以对内存的需求是无止境的。更何况,曾经有人向我提出10T容量的需求,完全没有考虑营收成本。雪崩效应因为我们大量使用依赖缓存来提供并发支持的数据,一旦缓存出现问题,就会出现雪崩效应。即:外部流量还在,但是你要重启整个缓存服务器,会导致Cache被清空。由于数据源被切断,这将导致后端服务“挂掉”。为了防止雪崩,我们会将额外的数据副本写入特定磁盘。它的数据的“新鲜度”可能不够,但是当发生雪崩时,它会被加载到内存中,以防止下一波雪崩,这样就可以平滑过渡,直到我们重新填充“新鲜”的数据。总结一下上面提到的“坑”:最厉害的是:用户误用、滥用和懒用。在前面的例子中提到,我们通常对缓存在哪里使用、如何使用以及要防止什么考虑的太少。数以千计的无使用规则的缓存服务器的运维。我们常说DevOps的方式是让应用和运维更加紧密,但是在对缓存进行运维的时候,既然应用开发不关心里面的数据,怎么可能更加紧密呢?运维不懂开发,开发不懂运维。这导致缓存系统碎片化,无法真正应用缓存。缓存的使用没有设计也没有控制。一般情况下,JVM可以监测到内存爆炸,考虑是否需要回收。但是,如前面的例子所示,当一个Key的大小为20G时,我们往往忽略了一个Key在缓存服务器上的爆炸。开发人员能力的差异。既然不可能要求所有的开发人员都是前端工程师,那么当你的团队中有不同经验的人时,你怎么能让他们写出相同的标准化代码呢?毕竟我们做的是工程,需要更多的人来保证写出来的代码不会出现上面的问题。太多的服务器资源被浪费了。特别是Cache整体的浪费是非常巨大的。不管并发是高是低,是不是真的需要,大家都在用它的内存。举个例子:在我们上千个CacheServer中,最高的浪费可以达到60%。一些只需要几百或几千KPS的系统或数据也被设计成运行在昂贵的Cache内存中。实际上,它们可能只是用来应对每月一次或每年一次的促销活动的缓存峰值需求。心态懒惰,应对变化的速度不够快。面对高并发,十个程序员就有五个会说:在数据层加Cache,并没有真正对架构做长远规划。如何通过治理让缓存问题不可见那么应该如何治理缓存呢?从真正的开发理念来看,我们想要的是一个千变万化的魔盒,可以快速的改变和处理自己,而不需要开发和运维人员担心滥用。除此之外,其他需要处理的还有:应用对缓存大小的需求就像一条贪婪的蛇,一堆孤立的单机服务器,缓存服务的运维就像迷宫。因此,我们希望构建的是一个可以适用于各种应用场景的缓存服务,而不是一个冷冰冰的CacheServer。一开始我们尝试了各种现成的开源方案,后来发现它们或多或少都有问题。比如:Cachecloud,不利于部署和运维。Codis本身已经搭建了一个很大的集群,但是我们考虑到这么大的池出现问题的时候,整个团队会失去处理的灵活性。例如:我们担心业务数据块可能会在没有隔离的情况下被放入池中,那么当一个实例“挂掉”时,所有的数据块都会受到影响。Pika,虽然可以用硬盘,但是部署方式很少。Twemproxy只擅长演戏,其他能力都不好。后来我们选择了自己做,做了一个凤凰解决方案。整个系统包括客户端、运维平台、存储扩展。在最初的架构设计中,我们只是让应用端通过一个简单的SDK来使用系统。为了防止服务器继续寻找CacheServer模式,我们要求应用程序提前声明自己的项目和数据场景,然后给系统分配一个Key。SDK使用它为应用程序分配新的或现有的缓存存储库。如上图所示,为了加快速度,我们将缓存划分为多个虚拟的逻辑池,分别为上层调度系统的各个场景。然后应用程序就可以通过这个来申请包含什么样的数据需要存储的场景,最后根据分配的Key调用。这里,底层是各种数据的复制和迁移,两侧是相应的监控和运维。但是,当系统真正“运行起来”的时候,我们却发现部署和扩展困难重重。所以在改造的时候,我们对整个缓存客户端SDK进行了重写,引入了场景的配置。我们管理本地缓存并添加过滤条件,保证客户端在读取缓存时能够知道具体的数据来源和基本协议,从而决定是否访问Redis、MemCache或者其他类型的存储。Cache客户端完成后,我们又遇到了一个新的问题:由于同程采用了包括Java、.Net、Go、Node.js等多种语言的开发模型,如果我们准备和维护一套Cache客户端显然是非常劳动密集型。同时对于维护来说:只要是程序就会有bug,只要有bug就需要升级。一旦所有业务单元的所有应用都需要升级SDK,就必须对所有嵌套的应用中间件进行升级测试,这将涉及到巨大的回归量。可以说,这样的反复试验几乎是不现实的。所以我们需要做一个代理层,通过将协议、过滤、场景等内容下沉到Proxy中,从而实现SDK整体的轻量化。同时我们在部署时也引入了容器,将整个Redis运行在容器中,让容器完成整个应用的部署。通过容器化部署,集群的建立变得异常简单,我们也大大丰富了集群的解决方案。我们已经意识到,每个应用场景都可以配备一个(或一种)Key,并由一个(或一种)集群提供服务。众所周知,Redis虽然实现了迁移和扩容,但是其操作更为复杂。因此,我们自己开发了一套迁移调度系统,自动实现从流量扩展到数据扩展,从垂直扩展到水平扩展。前面提到,我们有两个客户端,Redis和Memcache,它们使用不同的协议进行访问。因此,我们通过统一的Proxy实现了很好的支持。现在在我们的缓存平台上,运维人员唯一需要做的就是:在缓存平台上添加一台物理服务器,插上网线,系统会自动发现新服务器的添加,然后启动Redis。对于单机场景下的Redis实例,我们还可以通过控制台获取多个监控项,包括Top10Keys、访问次数最多的Keys、Keys的所有者、最后写入或修改的人。可以看到,由于上下层都是自建的,所以我们扩展了原来Redis中没有的监控项。上图是Topkey的使用示例,就像程序失败时经常使用的dump文件一样,可以体现后面的各种编排。王晓波,同程艺龙机票事业群CTO,专注于高并发互联网架构设计、分布式电商交易平台设计、大数据分析平台设计、高可用系统设计。设计了多个百万级以上的并发平台。十多年丰富的技术架构和技术咨询经验,深刻理解电子商务系统对技术选型的重要性。【原创稿件,合作网站转载请注明原作者和出处为.com】
