当前位置: 首页 > news >正文

Azure反模式——无缓存

在处理大并发的云应用程序中,反复提取相同数据可能会降低性能和可伸缩性。


问题描述
在没有缓存的情况下,可能会出现一些异常行为,包括:
反复从访问开销较高(I/O开销或网络延迟)的资源提取相同信息。
为多个请求反复构造相同的对象或数据结构。
向具有服务配额的远程服务发出过多的调用请求。
这些问题可能进一步导致系统响应时间不佳、数据存储中资源争用加剧,以及可伸缩性下降。
以下示例使用EF连接到数据库。即使多个请求提取完全相同的数据,每个客户端请求也会访问数据库。重复请求的成本(体现在 I/O开销和数据访问方面)可能会迅速累积。


public class PersonRepository : IPersonRepository
{
    public async Task<Person> GetAsync(int id)
    {
        using (var context = new AdventureWorksContext())
        {
            return await context.People
                .Where(p => p.Id == id)
                .FirstOrDefaultAsync()
                .ConfigureAwait(false);
        }
    }
}vvv




可在此处找到完整示例。(https://github.com/mspnp/performance-optimization/tree/master/NoCaching)
出现此反模式的原因通常是:
没有使用缓存。因为这种解决方案更容易实施,在低负载下可正常运转。缓存使代码变得更复杂。
不能清楚地了解使用缓存的优缺点。
注重缓存的准确性和刷新缓存所带来的开销。
应用程序是从本地系统迁移过来的,网络延迟不是问题,并且系统在昂贵的高性能硬件上运行,因此,原设计中未考虑缓存。
开发人员未意识到缓存在指定方案中是可行的。例如,开发人员在实现Web API时可能不会考虑使用Etag。


解决方案
最流行的缓存策略是按需策略或缓存预留策略。
读取时,应用程序先从缓存中读取数据。如果数据不在缓存中,应用程序会从数据源中检索数据,并将其添加到缓存。
写入时,应用程序将更改直接写入数据源,并从缓存中删除旧值。下一次有需要时,会在缓存中检索,如果不存在则将新数据添加到缓存。
该方法适用于经常更改的数据。下面将前一示例更新为使用缓存预留模式的实现。


public class CachedPersonRepository : IPersonRepository
{
    private readonly PersonRepository _innerRepository;


    public CachedPersonRepository(PersonRepository innerRepository)
    {
        _innerRepository = innerRepository;
    }


    public async Task<Person> GetAsync(int id)
    {
        return await CacheService.GetAsync<Person>("p:" + id, () => _innerRepository.GetAsync(id)).ConfigureAwait(false);
    }
}


public class CacheService
{
    private static ConnectionMultiplexer _connection;


    public static async Task<T> GetAsync<T>(string key, Func<Task<T>> loadCache, double expirationTimeInMinutes)
    {
        IDatabase cache = Connection.GetDatabase();
        T value = await GetAsync<T>(cache, key).ConfigureAwait(false);
        if (value == null)
        {
            // Value was not found in the cache. Call the lambda to get the value from the database.
            value = await loadCache().ConfigureAwait(false);
            if (value != null)
            {
                // Add the value to the cache.
                await SetAsync(cache, key, value, expirationTimeInMinutes).ConfigureAwait(false);
            }
        }
        return value;
    }
}




请注意,GetAsync方法现在调用了CacheService类,而不是直接访问数据库。CacheService类首先尝试从AzureRedis缓存中获取。如果在Redis缓存中未找到,CacheService会回调传递给它的lambda。该lambda函数负责从数据库提取数据。该实现将数据存储与缓存的解决方案分离,并将CacheService与数据库分离。


注意事项
如果缓存不可用(可能是暂时性故障造成的),不要向客户端返回错误。而应该从原始数据源提取数据。但是在恢复缓存时,原始数据存储可能忙于处理请求,导致超时和连接失败。(毕竟这是首先使用缓存的动机之一。)可使用断路器(https://docs.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker)等技术来避免数据源瘫痪。


缓存非静态数据的应用程序应支持最终一致性。
对于WebAPI,可通过在请求和响应消息中包含Cache-Control标头并使用Etag标识对象版本来支持客户端缓存。有关详细信息,请参阅API实现。(https://docs.microsoft.com/en-us/azure/architecture/best-practices/api-implementation#optimizing-client-side-data-access)


不需要缓存整个实体。如果某实体的大部分内容是静态的,但只有一小部分经常更改,对静态元素做缓存,并直接从数据源检索动态元素。这样有助于减少对数据源执行的I/O数量。
在某些情况下,如果可变数据的生存期较短,将它缓存可能是一个选择。例如,假设某设备持续发送状态更新。一种做法可能是在对其做缓存,而完全不用存数据库。
为了防止数据过时,许多缓存解决方案支持可配置的失效期,以便在达到指定的间隔后,从缓存中自动删除数据。可根据具体场景对过期时间做优化。静态数据在缓存中的保留时间可以长于会很快过时的可变性数据。
如果缓存解决方案未内置过期策略,可能需要实现一个后台进程,定时扫描缓存,防止它不受限制地扩大。
缓存数据除了来自外部数据源以外,还可以使用缓存来保存复杂计算的结果。但是,在执行此操作之前,需要确定应用程序是否真正有CPU的使用约束。
在应用程序启动时准备好缓存可能很有帮助。在缓存中初始化最有可能会用到的数据。
始终提供检测机制来检测缓存命中数和缓存未命中数。使用此信息来优化缓存策略,例如,要缓存哪些数据,以及在数据过期之前要在缓存中保存数据多长时间。
如果缺少缓存造成了瓶颈,则添加缓存可能会大幅提高并发请求,导致Web前端过载。客户端可能会收到HTTP-503(服务不可用)错误。这些问题表明需要对前端进行横向扩展。


如何检测问题
可以执行以下步骤来鉴别是否是缺少缓存导致了性能问题:
检查应用程序设计。盘点应用程序使用的数据存储机制。对于每个数据存储,确定应用程序是否使用了缓存。在可能的情况下,确定数据的更改频率。适合缓存的初始候选项包括不经常更改的数据,以及频繁读取的静态数据。
检测应用程序并实时监视系统,找出应用程序检索数据或计算信息的频率。
在测试环境中分析应用程序,捕获有关数据访问或其他经常执行的与计算关联的开销度量指标。
在测试环境中执行负载测试,识别系统在承受正常工作负荷和重度负载的情况下如何响应。 负载测试应使用真实工作负荷来模拟在生产环境中观察到的数据访问情形。
检查底层数据存储的数据访问统计信息,并检查相同数据请求的重复频率。


诊断示例
以下部分将这些步骤应用到前面所述的示例应用程序。


检测应用程序并实时监控系统
应用程序已部署到生产环境,检测并监视应用程序,以获取有关指定用户请求的信息。
下图显示了在负载测试期间NewRelic(http://newrelic.com/azure)捕获的监视数据。在本例中,执行唯一的GET操作是Person/GetAsync。但在生产环境中,了解每个请求的执行频率可以用于深入分析应该缓存哪些资源。



如果需要更深入的分析,可以在测试环境(而不是生产环境)中使用探查器捕获底层性能数据。 查看I/O请求速率、内存使用率和 CPU利用率等指标。这些指标可以反应出向数据存储或服务发出的大量请求,或者相同计算的重复执行。


对应用程序进行负载测试
下图显示了对示例应用程序执行负载测试的结果。该负载测试模拟一个包含800个用户的阶跃负载,这些用户执行一系列操作。



每秒成功执行的测试数达到平稳状态,因此,接下来的请求速度会减慢。平均测试时间随着工作负荷的增大而平稳增加。达到用户负载峰值后,响应时间趋向平稳。


检查数据访问统计信息
数据存储能够提供有用的数据访问统计信息和其他信息,例如,哪些查询最频繁。在微软SQLServer中,sys.dm_exec_query_stats管理视图就提供了最近执行的查询统计信息。sys.dm_exec-query_plan视图中能提供每个查询的文本。可使用SQL Server Management Studio等工具来运行以下SQL查询,以确定查询的执行频率。

SELECT UseCounts, Text, Query_Plan
FROM sys.dm_exec_cached_plans
CROSS APPLY sys.dm_exec_sql_text(plan_handle)
CROSS APPLY sys.dm_exec_query_plan(plan_handle)

结果中的UseCount列指示每个查询的运行频率。下图显示第三个查询已运行250,000次以上,远远超过其他任何查询。

下面是导致发送过多数据库请求的查询:


(@p__linq__0 int)SELECT TOP (2)
[Extent1].[BusinessEntityId] AS [BusinessEntityId],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[LastName] AS [LastName]
FROM [Person].[Person] AS [Extent1]
WHERE [Extent1].[BusinessEntityId] = @p__linq__0

这是EF在前面所示的GetByIdAsync方法中生成的查询。


解决方案
合并缓存后,重复负载测试,并将结果与前面未使用缓存执行的负载测试进行比较。下图是将缓存添加到示例应用程序后的负载测试结果。



成功测试数量仍保持平稳状态,但用户负载更高。在承受负载时,请求速率明显高于前面的测试结果。平均测试时间仍然随着负载的增大而增加,但最大响应时间为0.05毫秒,而前面的测试中为1毫秒—有了20倍左右的改善。


相关资源

API实现的最佳实践

https://docs.microsoft.com/en-us/azure/architecture/best-practices/api-implementation#optimizing-client-side-data-access

缓存预留模式(https://docs.microsoft.com/en-us/azure/architecture/patterns/cache-aside)
缓存的最佳实践(https://docs.microsoft.com/en-us/azure/architecture/best-practices/caching)
断路器模式(https://docs.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker)

相关文章:

  • Azure反模式——同步IO
  • irrlicht引擎源码剖析2 - IrrlichtDevice
  • 在Azure App service 中配置时区
  • Azure 最佳实践 - API 的设计与实现(2)
  • Azure 最佳实践 - CDN
  • Azure 最佳实践 - 自动伸缩
  • 《BREW进阶与精通——3G移动增值业务的运营、定制与开发》连载之6---移动增值业务概述...
  • Azure 最佳实践 - 后台作业
  • Python 使用dlib 5行代码实现人脸比对
  • 《BREW进阶与精通——3G移动增值业务的运营、定制与开发》连载之7---WAP,SMS,MMS,移动电子邮件...
  • android studio error 'unable to merge dex'
  • 在Nebula3中加载自定义模型的思路
  • asp.net core 部署在ubuntu
  • 获取当前月的第一天和最后一天...
  • tensorflowsharp异常could not load DLL 'libtensorflow'
  • Angular数据绑定机制
  • docker python 配置
  • electron原来这么简单----打包你的react、VUE桌面应用程序
  • happypack两次报错的问题
  • JavaScript类型识别
  • Java知识点总结(JDBC-连接步骤及CRUD)
  • JDK 6和JDK 7中的substring()方法
  • Linux快速复制或删除大量小文件
  • Nginx 通过 Lua + Redis 实现动态封禁 IP
  • Swoft 源码剖析 - 代码自动更新机制
  • vue自定义指令实现v-tap插件
  • Web设计流程优化:网页效果图设计新思路
  • yii2权限控制rbac之rule详细讲解
  • Yii源码解读-服务定位器(Service Locator)
  • 对话 CTO〡听神策数据 CTO 曹犟描绘数据分析行业的无限可能
  • 飞驰在Mesos的涡轮引擎上
  • 前端技术周刊 2018-12-10:前端自动化测试
  • 想使用 MongoDB ,你应该了解这8个方面!
  • TPG领衔财团投资轻奢珠宝品牌APM Monaco
  • 不要一棍子打翻所有黑盒模型,其实可以让它们发挥作用 ...
  • ​批处理文件中的errorlevel用法
  • ​人工智能书单(数学基础篇)
  • ​中南建设2022年半年报“韧”字当头,经营性现金流持续为正​
  • # 深度解析 Socket 与 WebSocket:原理、区别与应用
  • ### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTr
  • #pragam once 和 #ifndef 预编译头
  • (delphi11最新学习资料) Object Pascal 学习笔记---第5章第5节(delphi中的指针)
  • (定时器/计数器)中断系统(详解与使用)
  • (附源码)springboot建达集团公司平台 毕业设计 141538
  • (含react-draggable库以及相关BUG如何解决)固定在左上方某盒子内(如按钮)添加可拖动功能,使用react hook语法实现
  • (九)信息融合方式简介
  • (十)c52学习之旅-定时器实验
  • (已更新)关于Visual Studio 2019安装时VS installer无法下载文件,进度条为0,显示网络有问题的解决办法
  • (转) ns2/nam与nam实现相关的文件
  • (转)eclipse内存溢出设置 -Xms212m -Xmx804m -XX:PermSize=250M -XX:MaxPermSize=356m
  • (转)从零实现3D图像引擎:(8)参数化直线与3D平面函数库
  • .bat批处理(六):替换字符串中匹配的子串
  • .net core 依赖注入的基本用发
  • .Net 访问电子邮箱-LumiSoft.Net,好用
  • .NET/C# 编译期能确定的字符串会在字符串暂存池中不会被 GC 垃圾回收掉