译ServiceNow的《数据缓存以提升性能》

网友投稿 288 2022-09-28

译ServiceNow的《数据缓存以提升性能》

​​原文地址​​​()

概述

此文是由ServiceNow(以下简写SN)技术支持的Performance团队所作。我们是致力于解决客户性能问题的global专家团队,如果你对此文有任何疑问可在原文下方提问,如果你对其他类别问题有疑问,可参照我们团队页面列出的一个​​资源清单​​。

在开始之前,我们假设您已能熟练使用SN的script,然后进行讨论关于Business Rule(以下简写BR)查询的几种性能优化方案和最佳实践。

在我们早期的文章《​​Performance Best Practice for Before Query Business Rules​​​》中提到过潜在的数据缓存方案:

​​       ​利用缓存来避免静态数据的频繁查询,举个例子:用户地域关系Region是不常变的,是否有方法可以避免在每次查询任务中在Region表和其他多对多的表中应用复杂查询,比如将用户的Region关系缓存在有效的session内来简化查询?其实这可​​​​以使用gs.getSession().putClientData()此类操作将复杂计算或者查询的结果放在内存缓存中(actual in-memory cache)或者直接使用一张数据表来实现。​​​

此文将会探讨多类型,广泛使用,对事务(transaction)更甚至于整个实例性能有作用的缓存策略。为了帮助大家理解,我以团队最近处理的客户实例升级为背景进行描述。尽管客户使用的特定实例,但我们讨论的概念适用于多种场景。

背景

客户实例需要被不用国家或地区的用户访问,根据其业务需要,数据表中的数据需要按国境(​territory​)隔离。整个设计大体可分为以下几点:

​在User表上新加字段,用户每个根据地理位置的不同来指定分其所属​territory​,​国境关系表使用一张自定义的​territory表来存储,按此方法实现,就意味着有国境层级关系的概念,举个例子:United Kingdom​用户应该可以访问北爱尔兰、威尔士、苏格兰和英格兰地区拥有的记录,而​Great Britain​地区的用户应该只能访问威尔士、苏格兰和英格兰地区​​(译者注:英国各称谓的区别参照文章末尾)。​其他表(例如task, cmdb)中的具体的记录会使用自定义字段​owning territory​来关联到国境关系​territory​。​

为了保证数据隔离,实例中实现了一系列的Script Include(以下简写SI)和before query BR,总的来说有以下几点:

用户每次执行会触发针对需要数据隔离的表的 SQL 查询的事务时(大白话:需要查询已经数据隔离的表),将执行一条before query BR。相应的BR将调用SI中的函数来获取一个Encoded query语句(实际调用哪个函数依实际数据隔离的情况而论)。在SI中,在返回一个Encoded query之前,使用​territory表中数据来决定用户可访问范围,​通常的情况是追加类似“AND ​territory ​IN (territory1, territory2, ...,territoryN)”的query语句BR将SI返回的Encoded query句子追加到SQL查询并执行。

性能问题

首先,我们应该指出,虽然在​Before Query BR​里能实现,但不是最佳;其次,如前一篇文章所说:​​​​​

​      ​​Roles,ACL's,Domain Separation和其他的开箱Security功能是​​​​确保数据安全和数据隔离​​​​SN推荐的首选方式,与自定义的查询类BR相比,它们优化了性能和减少了技术负担。理想情况下,如果您尝试以某种方式实现数据安全/隔离,从而引入在每次访问数据表时还需要频繁读取的额外的表评估访问权限的复杂逻辑,您应该优先考虑使用​Before Query BR​之外的其他方式。实际情况中,可用于实现相同目标的功能已经用尽时,才应该将​Before Query BR作为最后的选择。​​

然而,在此次升级中,客户的目标不是完全重写或重新设计其数据隔离逻辑(此代码逻辑在其平台已是根深蒂固),而单纯优化他们现有的逻辑。在审查实例的性能时,很明显对某些事务有重大影响的BR。例如:

​​2021-06-01 06:02:47 (585) Default-thread-5 6C7C6C6CDBE0FC98AC705F8BD396194C txid=2dc2b028db28 EXCESSIVE *** End #11860627 /pm_project.do, user: userxzy, total time: 0:00:41.346, ​processing time: 0:00:41.344​, total wait: 0:00:00.002, semaphore wait: 0:00:00.002, SQL time: 0:00:24.343 (count: 12,284), ​business rule: 0:00:27.257 (count: 2,998​), ACL time: 0:00:01.435, UI Action time: 0:00:00.934, Cache build time: 0:00:00.682, source: xxx.xxx.xxx.xxx null​​

注意以上事务所耗费的时间:

41.334秒 - 程序执行过程的耗时27.257秒 - 执行了2998条BR,耗时约占整个过程的66%

进一步检查整个事务发现:

未有执行任何单个的引起卡顿的BR(指耗时大于> 100ms),BR耗时居高不下主要是由于执行大量耗时短,运行快的BR。实现数据隔离的​Before Query BR被​数千次地执行,且针对同一个基础表的BR和SI中相同的函数的被反复执行。

调试数据隔离的​Before Query BR​时发现:

​Before Query BR​的单次调用平均执行时间约为 10 毫秒这 10 毫秒的大部分时间都花在遍历“​territory​”表上,以确定给定用户应该有权访问的区域层次结构(并构建相应的encoded query句子)

很不幸,这种场景在我们在处理客户实例时很常见,虽然BR或者SI代码编写得还不错,毋庸置疑地,客户在生产部署之前已经详尽测试。但是,更有可能的情况是这类逻辑的作者发现BR耗时很短,认定对整个实例和终端用户的影响微乎其微。有欠考虑的情况是:某些事务会数千次的执行整个BR,忽略BR的执行速度的快慢来说,这类设计也是不利于功能扩展的,最终引发整个实例的性能问题,导致用户对SN失去信心。

调侃地讲,在和处理客户实例时,这类设计的劣势成为我们的优势,使我们可以大展拳脚( ̄_, ̄ ),当我们审视完​Before Query BR时发现:​

​​​一个SI中调用三个函数(依据不同的访问站点、表、用户来返回可见性)BR调用SI时传入BR的表名和当前用户信息,

同样,SI会将一个由三个方法的运行结果组合起来作为Encoded query返回,此外,该设计意味着在单个事务中相同的组合值反复被传递个SI,简而言之,绝大多数BR时间都花在了反复地产生相同的Enoded query。那就是说,如果每个组合只生成一次encoded query的话,然后在适用时重复使用,我们就可能显著提高性能/减少这些BR的影响。

还有一个问题需要考虑:SN是如何存储、复用指定的Encoded Query?有两个选择:1 自定义表来作缓存,2 使用后端session来缓存。接下来我们挨个讨论一番。

情况一:自定义表做缓存

请记住,在这种情况下,SI生成了一个以作为访问站点(u_function)、表(u_table)、用户(u_user)组合起来的encoded query,所以需要像以下这样整个表来存储数据:

SQL> show columns from u_mf_cache;+------+----------------+-------------+------+-----+---------+-------+| Port | Field | Type | Null | Key | Default | Extra |+------+----------------+-------------+------+-----+---------+-------+| 3403 | u_encodedquery | mediumtext | YES | | | | <=== HOLDS CALCULATED ENCODED QUERY| 3403 | u_table | varchar(40) | YES | | | | <=== TABLE PROVIDED TO SCRIPT INCLUDE| 3403 | u_user | varchar(32) | YES | MUL | | | <=== USER SYS_ID PROVIDED TO SCRIPT INCLUDE| 3403 | sys_id | char(32) | NO | PRI | | || 3403 | sys_updated_by | varchar(40) | YES | | | || 3403 | sys_updated_on | datetime | YES | | | | <=== DATE / TIME CACHE RECORD CREATED / LAST UPDATED| 3403 | sys_created_by | varchar(40) | YES | | | || 3403 | sys_created_on | datetime | YES | | | || 3403 | sys_mod_count | int(11) | YES | | | || 3403 | u_function | varchar(40) | YES | | | | <=== FUNCTION / ENTRY POINT WITHIN SCRIPT INCLUDE CALLED BY BUSINESS RULE+------+----------------+-------------+------+-----+---------+-------+

然后可将SI按以下作修改:

SI被调用时,优先查询此缓存表以查找具有相应访问站点(u_function)、表(u_table)和用户(u_user)的记录如果找到记录,将记录更新时间 (sys_updated_on) 与当前时间做比较,如果创建或更新于特定时间段内(例如过去 1 小时),则SI将立即将记录的的u_encodedquery值做Encoded query返回并退出如果未找到记录或记录已过时,SI按原有方式计算得到encoded query,并且创建或者更新记录到此表中。

这种机制意味着即使给定站点、表、用户对应的记录不存在,SI也将简单地计算所需的Encoded query(就优化前那样计算)并将结果放入缓存表中,此操作耗时大约与“非缓存”SI的单次调用相同(即,它为BR或SI的单次执行引入了可忽略不计的降级),但是,如果给定站点、表、用户对应的encoded qeury确实存在缓存记录,则SI(以及相关的BR)中函数可能会“短路”;完成时间比“非缓存”SI的函数快得多。

总而言之,对于可能仅使用不同的站点、表、用户来计算encoded query引起的任何性能下降都可以忽略不计,肯定不足以被用户察觉。然而,使用重复的站点、表、用户来计算过程中执行BR数千次的事务,将有显著的性能改进,因为绝大多数BR执行将“短路”已提前退出,因此整个BR时间将大幅减少。

请注意,缓存表的结构和SI的设计意味着缓存表将不断服务于 SQL 查询,例如 'SELECT ... FROM u_mf_cache WHERE u_user = [x] AND u_table = [y] AND u_function = [z ]'。为了确保这些查询在负荷下仍保持高性能,并且随着表大小的增加,可在 (u_user, u_table) 列中添加了一个复合索引。

缓存表的优点:

可以放置在表中的数据(即encoded query)的大小没有限制 - 如果需要缓存大型数据结构/字符串,这很有意义易于理解和实现 - 缓存表可以通过标准 API 进行交互,例如 GlideRecord()允许轻松避免安全问题 - 让我们考虑用户关联的区域不正确 - 这将在缓存表中生成不正确的encoded query,从而允许用户访问他们不应该拥有的数据。可以将业务规则添加到 sys_user 表中,如果用户区域发生变化,该表会立即清除所有用户缓存条目。这将确保一旦更正了不正确的区域,用户将立即被迫生成/使用新的(正确的)encoded query。

缓存表的缺点:

B​efore query BR​的每次调用都会对缓存表执行 SQL 查询,这会增部分事务甚至实例的延迟。在繁忙的情况下,缓存表可能会变得非常大(就记录数而言)和繁忙(就 SQL 查询/插入/更新而言),这可能导致该表成为底层数据库中的短板。一个好的索引策略(如上所述)将有助于在一定程度上缓解这种情况

情况二:使用后端session来缓存

除了在底层数据库的表中缓存数据,还可以在内存中的session中缓存字符串类型的数据。SN提供了以下几种方式来实现:

在session中缓存数据:

var session = gs.getSession();session.putClientData([key], [data]);//For example:session.putClientData('foo', 'bar');

从session检索缓存数据:

var session = gs.getSession();gs.info(session.getClientData('foo'));...*** Script: bar

从session中清除缓存数据:

var session = gs.getSession();session.clearClientData('foo');gs.info(session.getClientData('foo'));...*** Script: null

为了利用session中缓存数据,SI需要按如下修改:

当构建给定函数/表/用户组合的encoded query时,在session中创建一条数据,例如:

Key: mfc_[table]_[function]Data: 包含以下两部分数据的JSON序列化的字符串

计算产生的encoded queryUnix格式的数据创建的时间戳

调用时,脚本包含将:

立即检索session以确定是否找到给定表/函数组合的数据(即 mfc_[table]_[function] 的键)如果找到,则SI将检查数据中的 Unix 时间戳并与当前时间进行比较如果数据足够新(例如在过去 1 小时内创建),SI将立即返回缓存的encoded query(即来自具有 mfc_[table]_[function] 的键的数据),如果数据已过时(即创建> 1 小时)将其清除。如果在session中未找到数据或数据已过时,则SI将重新按常规方式计算encoded query并将相应的键/数据添加回session中。

请注意,session中的数据的key无需是代表这用户的详细信息(这在里使用自定义表缓存时非常重要) - 这是因为每个用户的session对该用户都是私有的,不共享的。这意味着每个用户的session中的任何数据只能由该用户运行的代码访问,因此无需在缓存中记录可区分用户的详细信息。

用在session做缓存的优点:

Session可以通过标准 API 进行交互 - 因此很容易在Session中实现缓存。Session保存在application node的内存(堆)中 - 因此, session的读写非常快,且无需任何类型的数据库查询。Session是为基于交互式(即基于用户)和非交互式(即​scheduled job​)创建的,因此,在执行​scheduled job​时session中的数据也是有效的。

用在session做缓存的缺点:

Application node运行内存相对较小(堆的大小始终为 2Gb,正常情况下,其中很大部分由节点所需的其他对象占用) - 如果在用户会话中持有大量数据,这可以迅速引起节点上的内存捉急,从而导致大面积的性能下降,影响服务。session只适用于缓存少量相对较短的数据。有关如何在内存使用方面保持代码高效的一些想法,请参阅文章 ServiceNow 中服务器端编码的性能最佳实践并阅读《​​Performance Best Practices for Server-side Coding in ServiceNow​​》中“​Running out of memory due to storing dot-walked GlideElement fields​”的这小节。如果您的实例内存不足或内存不足,则可能会导致宕机。Ssession在用户login时创建并在logout时销毁(导致内存中相应session中的数据丢失)。对于寿命相对较长但可能会影响每次使用新会话频繁执行的集成的用户会话,这并不是特别值得关注的问题,因为它们可能会一遍又一遍地计算/缓存相同的数据而没有什么好处Session是私有的(即一个用户无法操纵任何其他用户的session的内容) - 如果用户缓存有问题的encoded query(由于用户关联了“不正确”的​territory​),则有问题的encoded query将继续使用,直到用户注销(且他的session被销毁)或缓存过时。即便是管理员处理了用户和区域的“不正确”关联关系,用户仍会在一段时间内使用缓存中与之前的“​territory​“相关的错误数据。

性能优化的结果

为了证明缓存数据的有效性,在启用和不启用缓存的情况下都进行了重要的测试。最初,此测试有点综合,因为执行后台脚本以调用脚本中的单个函数,包括 10,000 次,计算总时间 - 例如:

var impUser = new GlideImpersonate();var initialUser = impUser.impersonate('d37741dcdbae2b0414f4bc2ffe96194c');var stopWatch = new GlideDateTime();for (var i = 0; i < 10000; i++) { var smf = new [script_include]](); [script_include].[function]]('pm_project', 'd37741dcdbae2b0414f4bc2ffe96194c');}gs.info('TOTAL MS: ' + (new GlideDateTime().getNumericValue() - stopWatch.getNumericValue()) + ' AVERAGE PER ITERATION MS: ' + ((new GlideDateTime().getNumericValue() - stopWatch.getNumericValue()) / 10000));impUser.impersonate(initialUser);

对于给定的函数,时间安排如下(请注意,脚本包含中的所有函数都是相似的):

无缓存:​每次迭代完成耗时 72.328s / 72328ms​​使用表做缓存:每次迭代完成耗时 7.864 秒 / 7864 毫秒(较无缓存提升 89%)​​使用session做缓存:每次迭代完成耗时 0.861 秒 / 861 毫秒(较无缓存提升99%)​

进一步的测试涉及识别客户生产实例中正在执行的缓慢事务,这些事务受到业务规则时间过长的不利影响(反过来,这被确定是由于大量执行查询前业务规则)。一般来说,启用缓存后,事务处理时间(即在应用程序节点上执行的时间)提高了 55 - 65%。未因执行大量“查询前业务规则”而受到不利影响的事务因使用缓存而导致性能下降非常有限(基本上没有)。

小结

缓存数据以提高性能是一个有点高级的话题,它可能只在非常特定的场景中才有用(基本上是在使用一组一致的输入/返回一组一致的结果反复执行昂贵的逻辑操作的地方) .但是,如果您的实例确实有这样的用例,那么按照本文所述实现数据缓存可以显着提高性能并允许您的实例按预期扩展。

作者补充:

以上全文未提及Glide properties(即sys_properties表),在这提及它似乎更为谨慎,因为它确是一种可提高性能的服务端缓存方案。令人疑惑的是更改property可能反而引起性能问题。默认情况下,每次更改property会引起系统级更新缓存,从而导致5到30分钟内部分潜在的严重性能下降。让我们解释一下原因:

ServiceNow 是一个集群平台,具有多个“节点”或作为 ServiceNow 的单个“实例”工作的 JVM(例如 acme.service-now.com)。属性(sys_properties 表中的记录)缓存在节点内存中。这是一种避免访问常用数据时引起数据库查询的方法。

当某个节点上的属性更改时,它会通知其他节点丢弃现有properties缓存并再次从数据库中获取其所有properties的新值。这对系统性能的影响非常微不足道。无论 ignore_cache 字段是否设置为 true,这个过程都会发生。

但是,如果 ignore_cache 设置为 false - 这是默认值 - 那么我们不仅会刷新与property相关的特定缓存,还会刷新整个 Glide System 缓存!!让我再说一遍,请注意双重否定。如果一个property设置为不忽略缓存(​not ignore the cache​),那么我们告诉它刷新整个 Glide System 缓存!这就是触发显着性能影响的原因。那我们为什么要这样做?需要缓存刷新的原因是我们确保刷新缓存中的其他任何依赖项或陈旧值

举个例子:假设您在服务端为每个用户的session有一个 UI 缓存,假设是页面的 HTML 存储,这种缓存是为避免为同一个用户反复渲染同一个页面。现在进一步假设 HTML 呈现的方式取决于刚刚更改的properties的值。如果我们不刷新此 UI 缓存,则该属性的旧值仍将在呈现的 HTML 中使用,并且您希望在 UI 中看到的基于相关property的任何更改都不会反映在 UI 中,直到用户重新登出登入。在这种情况下,您需要确保将ignore_cache设置为 false,以便我们不会忽略缓存刷新,从而确保任何相关缓存也将被刷新。

​注意:关于 sys_property.ignore_cache 的整个讨论仅与 ServiceNow 服务器端缓存有关。它与客户端(浏览器,用户代理)中实现的缓存机制无关。​​原文 NOTE: This whole discussion about sys_property.ignore_cache is only in relation to ServiceNow server-side caching. It has nothing to do with the caching mechanisms implemented on the client-side; within Browsers/User Agents.​

因此,总而言之,如果您有一个会经常更新的property值(例如,每月一次以上)并且您知道没有其他缓存可能依赖于该property的值,那么设置ignore_cache=true.这样系统只会在property更新时刷新特定于property的缓存,而不刷新整个Glide系统缓存。

或者,您可以只使用 sys_properties 之外的某个表来存储值。但是,我们需要提醒的是,如果您使用新的自定义表会产生许可费用,因此这可能不是一个可行的选择。

​Best regards, your performance team​

友链:

知乎 - 英国各称谓的区别

问:Britain,UK,great Britain,England,British Isles区别用法?

答 :Best Practice for Before Query Business Rules​​》(Best Practices for Server-side Coding in ServiceNow​​》(https://community.servicenow.com/community?id=community_article&sys_id=c01fb3261b59101017d162c4bd4bcb64)​

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:SpringBoot中使用HTTP客户端工具Retrofit
下一篇:万亿级超高清产业变奏,分布式存储支撑关键应用落地
相关文章

 发表评论

暂时没有评论,来抢沙发吧~