当我们改善了Redis的key结构后,处理时间缩短了三分之一
首先
我目前在LINE株式会社担任服务器端工程师,负责开发工作。在这篇文章中,我想分享一下我之前在株式会社ZUU工作时的开发经验。
这次我们要讨论的内容是关于改进Redis的键结构,以将处理时间缩短到原来的1/3以下的故事。
服务提供的概述
在ZUU公司中,我们经营着多种媒体,发布与金融相关的文章。我们管理着ZUU Online、fuelle、MoneyTimes、dメニューマネー等多个媒体。在这些媒体中,除了用户可以浏览的页面外,还有编辑和开发人员等可以访问的管理界面(CMS)。通过管理界面,可以发布和预订文章,进行编辑,上传用于文章的图片等。
事情的起因/事件的起因
有一次在外观监视中出现了回应延迟的错误。外观监视每分钟访问页面,当其响应时间连续5次超过5秒时会触发警报。
这次目标媒体使用Redis缓存文章数据,但调查后发现是因为Redis负载增加导致的。Redis是单线程运行的,Redis负载增加对响应时间造成了延迟的影响。
瓶颈调查
为什么Redis的负载增加了呢?
由于使用Redis作为文章数据的缓存,所以在CMS中更新该文章时,需要清除与该文章相关的所有数据。如果不清除,用户将继续看到更新前的文章。
总结来说,我们发现这个清除过程的负荷非常大。
经典的密钥结构
在过去使用了字符串作为键。例如,与A文章相关的各种数据的键如下所示(实际情况略有不同,但大致是这种类型的键)。
-
- articleA_meta
-
- articleA_content
-
- articleA_image_url
-
- articleA_related_articles
- ・・・(一つの記事に対して数十のkey)
清除处理流程
-
- 使用SCAN命令来获取与已更新文章相关的所有键(在上述示例中为articleA_*)→ O(N)
- 删除所有获取的键→ DEL 操作本身的时间复杂度为O(1)
功率增加的瓶颈
Redis中存储了数十万个数据(除了文章之外还有其他数据),其中O(N)的处理1成为了瓶颈。每次更新文章时,处理1都会被执行。在CMS中,即使只更新了一个字符,也需要扫描数十万条数据。而且我们发现,这个Redis负载增加的CMS操作导致了用户界面响应时间的延迟。
应急响应
-
- てっとりばやく運用でカバー、記事編集者さんに更新ボタンのクリック頻度を下げてもらうようにしてもらっていました。
- 1時間ごとにflushdbしてデータがたまり続けることを抑止していました。(たしか、cronjobで実行していたと思います。)
通过这些方法可以在一定程度上减轻对Redis的负载。
长期解决方案(本文主题)
方法
正如前面所提到的,处理1(获取与SCAN相关的所有键)是瓶颈。因此我们决定不再使用字符串作为键,而是改用哈希。使用哈希,我们可以将与某篇文章相关的数据聚合到一个键(articleA)中,并使用字段来构建层次结构。
将字符串转换为哈希的步骤如下:
键的字符串
-
- articleA_meta
-
- articleA_content
-
- articleA_meta
-
- articleA_meta
- ・・・
哈希(键, 字段)
-
- articleA
meta
content
image_url
related_articles
当使用string时进行的SCAN&DEL操作在使用hash时将如何变化?
简单来说,一次DEL搞定!!!
DEL articleA
用这个方法可以删除articleA以下字段的所有数据,也就是与articleA相关的所有数据。这个操作的时间复杂度是O(1),效果非常可观!!!
“实施”
由于与本题无关,因此在此不会详细解释,但非常困难。将在文章的最后进行介绍。
结果
通过使用哈希,相比以前使用传统字符串构造的键结构,在 CMS 上更新文章的时间缩短了约30秒,只需不到10秒,削减了处理时间超过1/3。(虽然已经变成O(1),看起来还可进一步缩短,但实际上由于实施的范围较广时间耗费较长,所以在我离职之前无法完全实现哈希化所有内容。)
通过这种效果,即使在外部监控中也不再出现错误!!
最后
虽然在「通过优化SQL处理时间减少了约98%」的文章中也提到了类似的事情,但我认为只有工程师能够理解对Redis性能的影响。从工程师团队到运营团队,通过自下而上地提出问题并改善它,可以改善用户的响应时间,对业务做出坚定的承诺,这不仅对工程师来说是一次重要的经验,而且也能感受到巨大的喜悦。
在那时,我的同事A.N和R.M对瓶颈调查做出了重要贡献。同时,他们还仔细地审查了大量的差异,我非常感激。
(附录)关于实施的艰辛经历
为了避免使用Redis的部分对代码进行重构可能产生的影响,我们创建了针对所有重构目标的单元测试,以确保安全的实施。(当然这是理所当然的,但是实现量真的非常巨大……)
此外,由于无法对访问Redis的部分进行接口化,为了更容易编写测试,我们对所有部分进行了接口化。(虽然这有点自夸,但是我也发布了有关”接口和测试的文章”)
此外,在业务逻辑层面,由于实现涉及到Redis的key结构,我们也决定使用接口来进行封装隐藏。
由于这些实施涉及到了广泛的范围,所以工作量非常大,但是这对我来说是一次宝贵的经验。