你好! 我叫 Alexey Pyankov,是 Sportmaster 公司的一名开发人员。 在那里面
今天我想分享另一个主题的想法 - 在站点管理面板中为 java 后端选择缓存系统。 这个情节对我来说有着特殊的意义——虽然故事展开只有两个月,但在这2天里我们工作了60-12个小时,没有休息一天。 我从来没有想过或想象过可以如此努力地工作。
因此,我将文本分成两部分,以免完全加载它。 相反,第一部分将非常轻松 - 准备、介绍、关于什么是缓存的一些考虑。 如果您已经是一位经验丰富的开发人员或使用过缓存,那么从技术角度来看,本文很可能没有什么新内容。 但对于一个后辈来说,这样一个小小的回顾可以告诉他,当他发现自己处于这样的十字路口时该往哪个方向看。
当Sportmaster网站的新版本投入使用时,数据的接收方式,说得客气一点,不太方便。 基础是为网站的先前版本 (Bitrix) 准备的表格,必须将其引入 ETL,采用新的形式,并用来自十多个系统的各种小东西进行丰富。 为了让新的图片或产品描述出现在网站上,您必须等到第二天 - 仅在晚上更新,每天一次。
起初,投入生产的头几周有很多担忧,因此内容管理者的不便只是小事一桩。 但是,一旦一切安定下来,项目的开发就继续进行——几个月后,2015 年初,我们开始积极开发管理面板。 2015年和2016年,一切都很顺利,我们定期发布,管理面板涵盖了越来越多的数据准备工作,我们正在为这样一个事实做好准备:很快我们的团队将被委托处理最重要和最复杂的事情——产品电路(所有产品数据的充分准备和维护)。 但在 2017 年夏天,就在商品电路启动之前,该项目将发现自己陷入了非常困难的境地 - 正是因为缓存问题。 我想在这本由两部分组成的出版物的第二部分中谈论这一集。
但在这篇文章中,我将从远处开始,我将提出一些想法 - 关于缓存的想法,这将是在大型项目之前滚动浏览的一个很好的步骤。
当缓存任务发生时
缓存任务不只是出现。 我们是开发人员,编写软件产品并希望它受到需求。 如果产品有需求并且成功,用户就会来。 而且越来越多的人来了。 然后有很多用户,然后产品就会变得负载很高。
在第一阶段,我们不考虑优化和代码性能。 最重要的是功能,快速推出试点并测试假设。 如果负载增加,我们就会给熨斗打气。 我们将其增加两到三倍,五倍,也许十倍。 在这里的某个地方——财政状况不再允许这样做。 用户数量会增加多少倍? 不会像10-2-5那样,但如果成功的话,会是10-100到1000万次。 也就是说,迟早你要做优化。
假设代码的某些部分(我们称这部分为函数)花费了相当长的时间,而我们希望减少执行时间。 一个函数可以是访问数据库,也可以是执行一些复杂的逻辑——最主要的是它需要很长时间才能完成。 您可以减少多少执行时间? 在极限内,你可以将其减少到零,不再进一步。 如何将执行时间减少到零? 答案:完全消除执行。 相反,立即返回结果。 你怎样才能知道结果呢? 答案:要么计算一下,要么看看别的地方。 需要很长时间来计算。 例如,监视是记住该函数上次使用相同参数调用时产生的结果。
也就是说,函数的实现对我们来说并不重要。 只需知道结果取决于哪些参数就足够了。 然后,如果参数值以对象的形式表示,可以在某些存储中用作键,那么就可以保存计算结果并在下次访问时读出。 如果结果的写入和读取速度比执行函数更快,那么我们在速度方面就有优势。 盈利金额可以达到100倍、1000倍、100万倍(10^5是比较例外的情况,但在基数相当滞后的情况下,是很有可能的)。
缓存系统的基本要求
缓存系统的首要要求可能是快速读取速度,其次是写入速度。 这是事实,但前提是我们将系统投入生产。
我们来玩一下这个案例。
假设我们已经为当前负载提供了硬件,并且现在正在逐步引入缓存。 用户数量增加了一点,负载增加了 - 我们添加了一些缓存,到处都搞砸了。 这种情况持续了一段时间,现在实际上不再调用繁重的函数 - 整个主要负载都落在缓存上。 这段时间的用户数量增加了N倍。
如果硬件的初始供应量可以是 2-5 倍,那么在缓存的帮助下,我们可以将性能提高 10 倍,或者在良好的情况下提高 100 倍,在某些地方可能提高 1000 倍100。也就是说,在相同的硬件上,我们处理的请求多了 XNUMX 倍。 太棒了,你值得拥有姜饼!
但现在,在一个好的时刻,一次偶然的机会,系统崩溃了,缓存也崩溃了。 没什么特别的——毕竟缓存是根据“读写速度高,其他无所谓”的要求来选择的。
相对于启动负荷,我们的铁储备是2-5倍,这段时间的负荷增加了10-100倍。 使用缓存,我们消除了对繁重函数的调用,因此一切正常。 而现在,如果没有缓存,我们的系统会变慢多少倍? 我们会发生什么? 系统就会崩溃。
即使我们的缓存没有崩溃,只是清除了一段时间,它也需要预热,这需要一些时间。 在此期间,主要负担将落在功能上。
结论:高负载的生产项目要求缓存系统不仅要有较高的读写速度,还要保证数据安全和抗故障能力。
面粉选择
在一个带有管理面板的项目中,选择是这样的:首先我们安装了 Hazelcast,因为通过主站的体验,我们已经熟悉了这个产品。 但在这里,这个选择被证明是不成功的——在我们的负载配置下,Hazelcast 不仅慢,而且慢得可怕。 当时我们已经预定了发布日期。
剧透:情况到底是如何发展到我们错过了这么大的一笔交易并最终陷入了严重而紧张的局面——我将在第二部分告诉你——以及我们是如何结束的以及我们是如何摆脱困境的。 但现在 - 我只想说这是一个很大的压力,并且“思考 - 不知何故我无法思考,我们正在摇晃瓶子。” “摇动瓶子”也是剧透,稍后会详细介绍。
我们做了什么:
- 我们列出了 Google 和 StackOverflow 建议的所有系统。 30多一点
- 我们使用典型的生产负载来编写测试。 为此,我们记录了生产环境中通过系统的数据 - 一种嗅探器,其数据不是在网络上,而是在系统内部。 测试中正是使用了该数据。
- 在整个团队中,每个人都从列表中选择下一个系统,对其进行配置并运行测试。 它没有通过测试,也不能承载负载——我们把它扔掉,然后继续处理下一个。
- 到了17号系统,一切都变得毫无希望了。 别再摇晃瓶子了,是时候认真思考一下了。
但当您需要选择一个能够在预先准备的测试中“通过速度”的系统时,这是一个选择。 如果还没有这样的测试而您想快速选择怎么办?
让我们对这个选项进行建模(很难想象一个中+开发人员生活在真空中,并且在选择时还没有正式确定他首先尝试哪个产品的偏好 - 因此,进一步的推理更多的是理论家/哲学/关于大三)。
确定了需求后,我们就开始选择一个现成的解决方案。 为什么要重新发明轮子:我们将采用现成的缓存系统。
如果你刚开始用谷歌搜索,那么就给出或接受订单,但一般来说,指导方针会是这样的。 首先,你会遇到Redis,它随处可见。 然后你会发现EhCache是最古老、最成熟的系统。 接下来我们会写Tarantool,这是一个国内开发的解决方案,具有独特的方面。 还有 Ignite,因为它现在越来越受欢迎,并且得到了 SberTech 的支持。 最后还有 Hazelcast,因为在企业界它经常出现在大公司中。
该列表并不详尽;有数十个系统。 我们只会搞砸一件事。 我们就拿这次“选美”选出的5个系统来进行评选吧。 谁将成为赢家?
Redis的
我们在官方网站上阅读了他们写的内容。
看起来一切都很好,你可以拿走它并拧紧它 - 你需要的一切,它都可以。 但为了好玩,让我们看看其他候选人。
高速缓存
Redis忘记了,我准备选择EhCache。
但爱国主义精神促使我看到塔兰图尔的优点。
塔兰图
让我们看看实现:Mail.ru 公司高速公路、Avito、Beeline、Megafon、Alfa-Bank、Gazprom...
如果对 Tarantool 仍有任何疑问,那么万事达卡的实施案例就让我彻底明白了。 我选择塔兰图尔。
但不管怎么说…
点燃
…还有更多吗
实施:Sberbank、美国航空、Yahoo! 日本。 然后我发现 Ignite 不仅在 Sberbank 中实施,SberTech 团队也将其人员派往 Ignite 团队本身来完善产品。 这完全令人着迷,我已经准备好接受“点燃”了。
完全不清楚为什么,我正在看第五点。
淡褐色
我去网站
就这样,我准备好接受 Hazelcast 了。
对照
但如果你看一下,所有五位候选人的描述方式都表明他们每个人都是最好的。 如何选择? 我们可以看看哪一个最受欢迎,进行比较,头痛就会消失。
我们找到一个这样的
它们的排序如下:Redis 位居榜首,Hazelcast 位居第二,Tarantool 和 Ignite 越来越受欢迎,EhCache 一直保持不变。
但让我们看看
所有这些系统不仅仅是缓存系统。 它们还具有很多功能,包括当数据不传送到客户端进行处理时,反之亦然:需要在数据上执行的代码移动到服务器,在那里执行,然后返回结果。 而且它们通常不被视为独立的缓存系统。
好吧,我们不放弃,我们找个系统直接对比一下。 让我们看一下最重要的两个选项——Redis 和 Hazelcast。 我们感兴趣的是速度,我们会根据这个参数来比较它们。
Hz 与 Redis
我们发现这个
蓝色是Redis,红色是Hazelcast。 Hazelcast 处处胜出,这是有原因的:它是多线程的,高度优化的,每个线程都有自己的分区,所以不存在阻塞。 而且 Redis 是单线程的;它无法从现代多核 CPU 中受益。 Hazelcast 具有异步 I/O,Redis-Jedis 具有阻塞套接字。 毕竟,Hazelcast 使用二进制协议,而 Redis 是以文本为中心的,这意味着它效率低下。
以防万一,让我们转向另一个比较来源。 他会向我们展示什么?
Redis 与 Hz
多一个
相反,这里红色的是Redis。 也就是说,Redis 在性能方面优于 Hazelcast。 Hazelcast 赢得了第一个比较,Redis 赢得了第二个。
事实证明,第一个结果实际上是被操纵的:Redis 被放在基础盒子中,Hazelcast 是为测试用例量身定制的。 事实证明:首先,我们不能相信任何人,其次,当我们最终选择一个系统时,我们仍然需要正确配置它。 这些设置包括数十个、几乎数百个参数。
摇晃瓶子
我可以用以下比喻来解释我们现在所做的整个过程:“摇动瓶子”。 也就是说,现在你不必编程,现在主要的是能够阅读 stackoverflow。 我的团队里有一个人,一个专业人士,在关键时刻就是这样工作的。
他在做什么? 他看到了一个坏掉的东西,看到了堆栈跟踪,从中提取了一些单词(这些单词是他在该程序中的专业知识),在 Google 上搜索,在答案中找到了 stackoverflow。 没有阅读,没有思考,他在问题的答案中选择了与“做这个做那个”这句话最相似的内容(选择这样的答案是他的天赋,因为它并不总是获得最多赞的答案),适用,看起来:如果有什么改变,那就太好了。 如果没有改变,则回滚。 并重复启动-检查-搜索。 通过这种直观的方式,他确保代码在一段时间后可以工作。 他不知道为什么,他不知道自己做了什么,他无法解释。 但! 这种感染有效。 并且“火已经扑灭了。” 现在让我们弄清楚我们做了什么。 当程序运行时,它会容易一个数量级。 而且它节省了大量时间。
这个例子很好地解释了这个方法。
曾经很流行收集一艘装在瓶子里的帆船。 同时,帆船又大又脆弱,而且瓶颈很窄,根本不可能推进去。 如何组装呢?
有这样一个方法,非常快,而且非常有效。
这艘船由一堆小东西组成:棍子、绳索、帆、胶水。 我们把所有这些都装在一个瓶子里。
我们用双手拿起瓶子并开始摇晃。 我们不断地摇晃她。 当然,通常结果证明它完全是垃圾。 但是有时。 有时它竟然是一艘船! 更准确地说,是类似于船的东西。
我们向某人展示这个东西:“Seryoga,你看到了吗!?” 事实上,从远处看它就像一艘船。 但不能允许这种情况继续下去。
还有另一种方法。 它们被更高级的人使用,例如黑客。
我给了这个人一个任务,他做了一切就离开了。 你看——看起来已经完成了。 而过了一段时间,当代码需要敲定的时候,这一切都是因为他而开始的……还好他已经跑得远远的了。 这些人以瓶子为例,会这样做:你看,底部所在的地方,玻璃会弯曲。 而且其是否透明尚不完全清楚。 然后“黑客”切掉这个底部,在那里插入一艘船,然后再把底部粘回去,就好像它应该是这样的。
从设定问题的角度来看,一切似乎都是正确的。 但以船舶为例:为什么要制造这艘船呢?谁需要它呢? 它不提供任何功能。 通常这样的船是送给地位很高的人的礼物,他们把它放在他们上方的架子上,作为某种象征,作为标志。 如果是这样一个人,一个大企业的负责人或一个高级官员,那么旗帜怎么会代表这样一个脖子被砍掉的黑客呢? 如果他从来不知道这件事就好了。 那么,他们最终是如何制造出这些可以送给重要人物的船只的呢?
唯一你真正无能为力的关键地方就是身体。 船体恰好适合颈部。 而船是在瓶子外面组装的。 但这不仅仅是组装一艘船,而是真正的珠宝工艺品。 组件上添加了特殊的杠杆,然后可以将它们抬起。 例如,将帆折叠起来,小心地放入内部,然后在镊子的帮助下,非常精确地拉动和升起它们。 其结果是一件可以问心无愧和自豪地赠送的艺术品。
如果我们希望项目成功,团队中必须至少有一名珠宝商。 关心产品质量并考虑所有方面的人,即使在有压力的时刻,当情况需要以牺牲重要为代价来处理紧急情况时,也不牺牲任何东西。 所有可持续的、经受住时间考验的成功项目都是建立在这一原则之上的。 它们有一些非常精确和独特的东西,可以利用所有可用的可能性。 在瓶中船的例子中,船体穿过颈部的事实得到了体现。
回到选择缓存服务器的任务,如何应用此方法? 我提供了从所有现有系统中进行选择的选项 - 不要摇晃瓶子,不要选择,而是看看它们原则上有什么,选择系统时要寻找什么。
去哪里寻找瓶颈
我们尽量不要摇动瓶子,不要一一检查那里的所有东西,但让我们看看如果我们突然为了我们的任务,自己设计这样一个系统,会出现什么问题。 当然,我们不会组装自行车,但我们会用这张图来帮助我们弄清楚产品描述中需要注意的点。 让我们画出这样一个图。
如果系统是分布式的,那么我们将有几台服务器(6)。 假设有四个(将它们放置在图片中很方便,但是当然,可以有任意多个)。 如果服务器位于不同的节点上,则意味着它们都运行一些代码,负责确保这些节点形成一个集群,并在发生故障时相互连接和识别。
我们还需要代码逻辑(2),它实际上是关于缓存的。 客户端通过一些 API 与此代码交互。 客户端代码 (1) 可以位于同一 JVM 中,也可以通过网络访问它。 内部实现的逻辑是决定哪些对象留在缓存中以及哪些对象被丢弃。 我们使用内存(3)来存储缓存,但如果有必要,我们可以将一些数据保存在磁盘(4)上。
让我们看看负载会发生在哪些部分。 实际上,每个箭头和每个节点都会被加载。 首先,在客户端代码和api之间,如果这是网络通信,那么下沉会非常明显。 其次,在 api 本身的框架内 - 如果我们过度使用复杂的逻辑,我们可能会遇到 CPU 问题。 如果逻辑不把时间浪费在记忆上就好了。 并且仍然存在与文件系统的交互 - 在通常的版本中,这是序列化/恢复和写入/读取。
接下来是与集群的交互。 最有可能的是,它会在同一个系统中,但也可能是单独的。 这里还需要考虑向其传输数据、数据序列化的速度以及集群之间的交互。
现在,一方面,我们可以想象在处理代码请求时缓存系统中“哪些齿轮会转动”,另一方面,我们可以估计我们的代码将向该系统生成什么请求以及多少个请求。 这足以做出一个或多或少清醒的选择——为我们的用例选择一个系统。
淡褐色
让我们看看如何将这种分解应用到我们的列表中。 例如,黑泽尔卡斯特。
为了从 Hazelcast 中放入/获取数据,客户端代码访问 (1) api。 Hz 允许您将服务器作为嵌入式运行,在这种情况下,访问 api 是 JVM 内部的方法调用,可以认为是免费的。
为了使(2)中的逻辑起作用,Hz 依赖于序列化密钥的字节数组的哈希值 - 也就是说,密钥在任何情况下都将被序列化。 这对于 Hz 来说是不可避免的开销。
驱逐策略实施得很好,但对于特殊情况,您可以添加自己的策略。 您不必担心这部分。
可以连接存储装置 (4)。 伟大的。 嵌入式交互(5)可以被认为是即时的。 集群中节点之间的数据交换 (6) - 是的,它存在。 这是以牺牲速度为代价的容错投资。 Hz 功能近缓存允许您降低价格 - 从集群中其他节点接收的数据将被缓存。
在这种情况下可以采取什么措施来提高速度?
例如,为了避免 (2) 中的键序列化 - 在 Hazelcast 之上附加另一个缓存,以存储最热的数据。 Sportmaster 为此选择了咖啡因。
对于级别 (6) 的扭曲,Hz 提供两种类型的存储:IMap 和 ReplicatedMap。
值得一提的是 Hazelcast 是如何进入 Sportmaster 技术堆栈的。
2012 年,当我们致力于未来网站的第一个试点时,Hazelcast 成为搜索引擎返回的第一个链接。 这次相识是“第一次”——我们被这样的事实所吸引:仅仅两个小时后,当我们将 Hz 拧入系统时,它就起作用了。 而且效果很好。 到一天结束时,我们已经完成了多项测试,并且很高兴。 而这种储备的活力足以克服赫兹随着时间的推移所带来的惊喜。 现在Sportmaster团队没有理由放弃Hazelcast。
但诸如“搜索引擎中的第一个链接”和“HelloWorld 很快就组装好了”之类的论点当然是一个例外,也是选择发生时刻的一个特征。 对所选系统的真正测试从发布到生产环境开始,在这个阶段,您在选择任何系统(包括缓存)时都应该注意。 事实上,在我们的例子中,我们可以说我们选择了 Hazelcast 是偶然的,但后来证明我们的选择是正确的。
对于生产来说,更重要的是:监控、处理单个节点上的故障、数据复制、扩展成本。 也就是说,值得关注系统维护期间出现的任务 - 当负载比计划高数十倍时,当我们不小心将某些内容上传到错误的位置时,当我们需要推出新版本时代码、替换数据并且在客户不注意的情况下进行。
对于所有这些要求,Hazelcast 确实能够满足要求。
待续
但 Hazelcast 并不是万能药。 2017 年,我们选择 Hazelcast 作为管理缓存,仅仅是基于过去经验的良好印象。 这在一个非常残酷的笑话中发挥了关键作用,因此我们发现自己陷入了困境,并在 60 天的时间里“英勇地”摆脱了困境。 但下一部分将对此进行更多介绍。
与此同时...新代码快乐!
来源: habr.com