歡迎光臨
每天分享高質量文章

【.NET Core專案實戰-統一認證平臺】第十五章 網關篇-使用二級快取提升性能

首先說聲抱歉,可能是因為假期綜合症(其實就是因為懶哈)的原因,已經很長時間沒更新博客了,現在也調整的差不多了,準備還是以每周1-2篇的進度來更新博客,並完成本專案所有功能。

 

言歸正傳,本重構專案是在我根據實際需求重構,由於還未完全寫完,所以也沒進行壓測,在2月份時,張善友老師給我留言說經過壓測發現我重構的Ocelot網關功能性能較差,其中根本原因就是快取模塊,由於重構專案的快取強依賴Redis快取,造成性能瓶頸,發現問題後,我也第一時間進行測試,性能影響很大,經過跟張老師請教,可以使用二級快取來解決性能問題,首先感謝張老師關註並指點迷津,於是就有了這篇文章,如何把現有快取改成二級快取並使用。

為瞭解決redis的強依賴性,首先需要把快取資料儲存到本地,所有請求都優先從本地提取,如果提取不到再從redis提取,如果redis無資料,在從資料庫中提取。提取流程如下:

MemoryCache > Redis > db

此種方式減少提取快取的網絡開銷,也合理利用了分佈式快取,並最終減少資料庫的訪問開銷。但是使用此種方案也面臨了一個問題是如何保證集群環境時每個機器本地快取資料的一致性,這時我們會想到redis的發佈、訂閱特性,在資料發生變動時更新redis資料併發布快取更新通知,由每個集群機器訂閱變更事件,然後處理本地快取記錄,最終達到集群快取的快取一致性。

但是此方式對於快取變更非常頻繁的業務不適用,比如限流策略(準備還是使用分佈式redis快取實現),但是可以擴展配置單機限流時使用本地快取實現,如果誰有更好的實現方式,也麻煩告知下集群環境下限流的實現,不勝感激。

改造代碼

首先需要分析下目前改造後的Ocelot網關在哪些業務中使用的快取,然後把使用本地快取的的業務重構,增加提取資料流程,最後提供網關外部快取初始化接口,便於與業務系統進行集成。

1

重寫快取方法

找到問題的原因後,就可以重寫快取方法,增加二級快取支持,預設使用本地的快取,新建CzarMemoryCache類,來實現IOcelotCache方法,實現代碼如下。

 

using Czar.Gateway.Configuration;using Czar.Gateway.RateLimit;using Microsoft.Extensions.Caching.Memory;using Ocelot.Cache;using System;namespace Czar.Gateway.Cache
{    
///
/// 金焰的世界
/// 2019-03-03
/// 使用二級快取解決集群環境問題
///


public class CzarMemoryCache : IOcelotCache
{        
private readonly CzarOcelotConfiguration _options;        private readonly IMemoryCache _cache;        public CzarMemoryCache(CzarOcelotConfiguration options,IMemoryCache cache)
{
_options = options;
_cache = cache;
}        
public void Add(string key, T value, TimeSpan ttl, string region)
{
key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix,region, key);            
if (_options.ClusterEnvironment)
{
var msg = value.ToJson();                
if (typeof(T) == typeof(CachedResponse))
{
//帶過期時間的快取
_cache.Set(key, value, ttl);
//添加本地快取
RedisHelper.Set(key, msg);
//加入redis快取
RedisHelper.Publish(key, msg);
//發佈
}                
else if (typeof(T) == typeof(CzarClientRateLimitCounter?))
{
//限流快取,直接使用redis
RedisHelper.Set(key, value, (
int)ttl.TotalSeconds);
}                
else
{
//正常快取,發佈
_cache.Set(key, value, ttl);
//添加本地快取
RedisHelper.Set(key, msg);
//加入redis快取
RedisHelper.Publish(key, msg);
//發佈
}
}            
else
{
_cache.Set(key, value, ttl);
//添加本地快取
}
}        
public void AddAndDelete(string key, T value, TimeSpan ttl, string region)
{
Add(key, value, ttl, region);
}        
public void ClearRegion(string region)
{            if (_options.ClusterEnvironment)
{
var keys = RedisHelper.Keys(region +
“*”);
RedisHelper.Del(keys);
foreach (var key in keys)
{
RedisHelper.Publish(key,
“”); //發佈key值為空,處理時刪除即可。
}
}            
else
{
_cache.Remove(region);
}
}        
public T Get(string key, string region)
{
key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);            
if(region== CzarCacheRegion.CzarClientRateLimitCounterRegion&& _options.ClusterEnvironment)
{
//限流且開啟了集群支持,預設從redis取
return RedisHelper.Get(key);
}
var result = _cache.Get(key);            
if (result == null&& _options.ClusterEnvironment)
{
result= RedisHelper.Get(key);                
if (result != null)
{                    
if (typeof(T) == typeof(CachedResponse))
{
//查看redis過期時間
var second = RedisHelper.Ttl(key);                        
if (second > 0)
{
_cache.Set(key, result, TimeSpan.FromSeconds(second));
}
}                    
else
{
_cache.Set(key, result, TimeSpan.FromSeconds(_options.CzarCacheTime));
}
}
}            
return result;
}
}
}

上面就段代碼實現了本地快取和Redis快取的支持,優先從本地提取,如果在集群環境使用,增加redis快取支持,但是此種方式不適用快取變更非常頻繁場景,比如客戶端限流的實現,所以在代碼中把客戶端限流的快取直接使用redis快取實現。

2

註入實現和訂閱

有了實現代碼後,發現還缺少添加快取註入和配置信息修改。首先需要修改配置檔案來滿足是否開啟集群判斷,然後需要實現redis的不同部署方式能夠通過配置檔案配置進行管理,避免硬編碼導致的不可用問題。

配置檔案CzarOcelotConfiguration.cs修改代碼如下:

namespace Czar.Gateway.Configuration{    ///
/// 金焰的世界
/// 2018-11-11
/// 自定義配置信息
///


public class CzarOcelotConfiguration
{        
///
/// 資料庫連接字串,使用不同資料庫時自行修改,預設實現了SQLSERVER
///
public string DbConnectionStrings { get; set; }        ///
/// 金焰的世界
/// 2018-11-12
/// 是否啟用定時器,預設不啟動
///
public bool EnableTimer { get; set; } = false;        ///
/// 金焰的世界
/// 2018-11.12
/// 定時器周期,單位(毫秒),預設30分總自動更新一次
///
public int TimerDelay { get; set; } = 30 * 60 * 1000;        ///
/// 金焰的世界
/// 2018-11-14
/// Redis連接字串
///
public string RedisConnectionString { get; set; }        ///
/// 金焰的世界
/// 2019-03-03
/// 配置哨兵或分割槽時使用
///
public string[] RedisSentinelOrPartitionConStr { get; set; }        ///
/// 金焰的世界
/// 2019-03-03
/// Redis部署方式,預設使用普通方式
///
public RedisStoreMode RedisStoreMode { get; set; } = RedisStoreMode.Normal;        ///
/// 金焰的計界
/// 2019-03-03
/// 做集群快取同步時使用,會訂閱所有正則匹配的事件
///
public string RedisOcelotKeyPrefix { get; set; } = “CzarOcelot”;        ///
/// 金焰的世界
/// 2019-03-03
/// 是否啟用集群環境,如果非集群環境直接本地快取+資料庫即可
///
public bool ClusterEnvironment { get; set; } = false;        ///
/// 金焰的世界
/// 2018-11-15
/// 是否啟用客戶端授權,預設不開啟
///
public bool ClientAuthorization { get; set; } = false;        ///
/// 金焰的世界
/// 2018-11-15
/// 服務器快取時間,預設30分鐘
///
public int CzarCacheTime { get; set; } = 1800;        ///
/// 金焰的世界
/// 2018-11-15
/// 客戶端標識,預設 client_id
///
public string ClientKey { get; set; } = “client_id”;        ///
/// 金焰的世界
/// 2018-11-18
/// 是否開啟自定義限流,預設不開啟
///
public bool ClientRateLimit { get; set; } = false;
}
}

在配置檔案中修改了redis相關配置,支持使用redis的普通樣式、集群樣式、哨兵樣式、分割槽樣式,配置方式可參考csrediscore開源專案。

然後修改ServiceCollectionExtensions.cs代碼,註入相關實現和redis客戶端。

    builder.Services.AddMemoryCache(); //添加本地快取
            #region 啟動Redis快取,並支持普通樣式 官方集群樣式  哨兵樣式 分割槽樣式
            if (options.ClusterEnvironment)
            {
                //預設使用普通樣式
                var csredis = new CSRedis.CSRedisClient(options.RedisConnectionString);
                switch (options.RedisStoreMode)
                {
                    case RedisStoreMode.Partition:
                        var NodesIndex = options.RedisSentinelOrPartitionConStr;
                        Func<string, string> nodeRule = null;
                        csredis = new CSRedis.CSRedisClient(nodeRule, options.RedisSentinelOrPartitionConStr);
                        break;
                    case RedisStoreMode.Sentinel:
                        csredis = new CSRedis.CSRedisClient(options.RedisConnectionString, options.RedisSentinelOrPartitionConStr);
                        break;
                }
                //初始化 RedisHelper
                RedisHelper.Initialization(csredis);
            }
            #endregion
            builder.Services.AddSingleton, CzarMemoryCache>();
            builder.Services.AddSingleton, CzarMemoryCache>();
            builder.Services.AddSingleton, CzarMemoryCache>();
            builder.Services.AddSingleton();
            builder.Services.AddSingleton, CzarMemoryCache>();
            builder.Services.AddSingleton, CzarMemoryCache>();
            builder.Services.AddSingleton, CzarMemoryCache>();
            builder.Services.AddSingleton, CzarMemoryCache>();

現在需要實現redis訂閱來更新本地的快取信息,在專案啟動時判斷是否開啟集群樣式,如果開啟就啟動訂閱,實現代碼如下:

public static async Task UseCzarOcelot(this IApplicationBuilder builder, OcelotPipelineConfiguration pipelineConfiguration)
{
    //重寫創建配置方法
    var configuration = await CreateConfiguration(builder);
    ConfigureDiagnosticListener(builder);
    CacheChangeListener(builder);
    return CreateOcelotPipeline(builder, pipelineConfiguration);
}
/// 
/// 金焰的世界
/// 2019-03-03
/// 添加快取資料變更訂閱
/// 

///
///
private static void CacheChangeListener(IApplicationBuilder builder)
{
var config= builder.ApplicationServices.GetService();
var _cache= builder.ApplicationServices.GetService();
if (config.ClusterEnvironment)
{
//訂閱滿足條件的所有事件
RedisHelper.PSubscribe(new[] { config.RedisOcelotKeyPrefix + “*” }, message =>
{
var key = message.Channel;
_cache.Remove(key); //直接移除,如果有請求從redis里取
//或者直接判斷本地快取是否存在,如果存在更新,可自行實現。
});
}
}

 

使用的是從配置檔案提取的正則匹配的所有KEY都進行訂閱,由於本地快取增加了定時過期策略,所以為了實現方便,當發現redis資料發生變化,所有訂閱端直接移除本地快取即可,如果有新的請求直接從redis取,然後再次快取,防止集群客戶端快取信息不一致。

為了區分不同的快取物體,便於在原始資料發送變更時進行更新,定義CzarCacheRegion類。

namespace Czar.Gateway.Configuration{    ///
/// 快取所屬區域
///


public class CzarCacheRegion
{        
///
/// 授權
///
public const string AuthenticationRegion = “CacheClientAuthentication”;        ///
/// 路由配置
///
public const string FileConfigurationRegion = “CacheFileConfiguration”;        ///
/// 內部配置
///
public const string InternalConfigurationRegion = “CacheInternalConfiguration”;        ///
/// 客戶端權限
///
public const string ClientRoleModelRegion = “CacheClientRoleModel”;        ///
/// 限流規則
///
public const string RateLimitRuleModelRegion = “CacheRateLimitRuleModel”;        ///
/// Rpc遠程呼叫
///
public const string RemoteInvokeMessageRegion = “CacheRemoteInvokeMessage”;        ///
/// 客戶端限流
///
public const string CzarClientRateLimitCounterRegion = “CacheCzarClientRateLimitCounter”;
}
}

 

現在只需要修改快取的region為定義的值即可,唯一需要改動的代碼就是把之前寫死的代碼改成如下代碼即可。

var enablePrefix = CzarCacheRegion.AuthenticationRegion;

3

開發快取變更接口

現在整個二級快取基本完成,但是還遇到一個問題就是外部如何根據資料庫變更資料時來修改快取資料,這時就需要提供外部修改api來實現。

添加CzarCacheController.cs對外部提供快取更新相關接口,詳細代碼如下:

using Czar.Gateway.Authentication;using Czar.Gateway.Configuration;using Czar.Gateway.RateLimit;using Czar.Gateway.Rpc;using Microsoft.AspNetCore.Authorization;using Microsoft.AspNetCore.Mvc;using Microsoft.Extensions.Caching.Memory;using Ocelot.Configuration;using Ocelot.Configuration.Creator;using Ocelot.Configuration.Repository;using System;using System.Threading.Tasks;namespace Czar.Gateway.Cache{    ///
/// 提供外部快取處理接口
///


[
Authorize]
[
Route(“CzarCache”)]    public class CzarCacheController : Controller
{        
private readonly CzarOcelotConfiguration _options;        private readonly IClientAuthenticationRepository _clientAuthenticationRepository;        private IFileConfigurationRepository _fileConfigurationRepository;        private IInternalConfigurationCreator _internalConfigurationCreator;        private readonly IClientRateLimitRepository _clientRateLimitRepository;        private readonly IRpcRepository _rpcRepository;        private readonly IMemoryCache _cache;        public CzarCacheController(IClientAuthenticationRepository clientAuthenticationRepository, CzarOcelotConfiguration options,
IFileConfigurationRepository fileConfigurationRepository,
IInternalConfigurationCreator internalConfigurationCreator,
IClientRateLimitRepository clientRateLimitRepository,
IRpcRepository rpcRepository,
IMemoryCache cache
)        
{
_clientAuthenticationRepository = clientAuthenticationRepository;
_options = options;
_fileConfigurationRepository = fileConfigurationRepository;
_internalConfigurationCreator = internalConfigurationCreator;
_clientRateLimitRepository = clientRateLimitRepository;
_rpcRepository = rpcRepository;
_cache = cache;
}        
///
/// 更新客戶端地址訪問授權接口
///
/// 客戶端ID
/// 請求模板
///
[
HttpPost]
[
Route(“ClientRule”)]        public async Task UpdateClientRuleCache(string clientid, string path)        {            var region = CzarCacheRegion.AuthenticationRegion;            var key = CzarOcelotHelper.ComputeCounterKey(region, clientid, “”, path);
key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);            
var result = await _clientAuthenticationRepository.ClientAuthenticationAsync(clientid, path);            var data = new ClientRoleModel() { CacheTime = DateTime.Now, Role = result };            if (_options.ClusterEnvironment)
{
RedisHelper.Set(key, data);
//加入redis快取
RedisHelper.Publish(key, data.ToJson());
//發佈事件
}            
else
{
_cache.Remove(key);
}
}        
///
/// 更新網關配置路由信息
///
///
[
HttpPost]
[
Route(“InternalConfiguration”)]        public async Task UpdateInternalConfigurationCache()        {            var key = CzarCacheRegion.InternalConfigurationRegion;
key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix,
“”, key);            var fileconfig = await _fileConfigurationRepository.Get();            var internalConfig = await _internalConfigurationCreator.Create(fileconfig.Data);            var config = (InternalConfiguration)internalConfig.Data;            if (_options.ClusterEnvironment)
{
RedisHelper.Set(key, config);
//加入redis快取
RedisHelper.Publish(key, config.ToJson());
//發佈事件
}            
else
{
_cache.Remove(key);
}
}        
///
/// 刪除路由配合的快取信息
///
/// 區域
/// 下端路由
///
[
HttpPost]
[
Route(“Response”)]        public async Task DeleteResponseCache(string region,string downurl)        {            var key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, downurl);            if (_options.ClusterEnvironment)
{                
await RedisHelper.DelAsync(key);
RedisHelper.Publish(key,
“”);//發佈時間
}            
else
{
_cache.Remove(key);
}
}        
///
/// 更新客戶端限流規則快取
///
/// 客戶端ID
/// 路由模板
///
[
HttpPost]
[
Route(“RateLimitRule”)]        public async Task UpdateRateLimitRuleCache(string clientid, string path)        {            var region = CzarCacheRegion.RateLimitRuleModelRegion;            var key = clientid + path;
key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);            
var result = await _clientRateLimitRepository.CheckClientRateLimitAsync(clientid, path);            var data = new RateLimitRuleModel() { RateLimit = result.RateLimit, rateLimitOptions = result.rateLimitOptions };            if (_options.ClusterEnvironment)
{
RedisHelper.Set(key, data);
//加入redis快取
RedisHelper.Publish(key, data.ToJson());
//發佈事件
}            
else
{
_cache.Remove(key);
}
}        
///
/// 更新客戶端是否開啟限流快取
///
///
///
[
HttpPost]
[
Route(“ClientRole”)]        public async Task UpdateClientRoleCache(string path)        {            var region = CzarCacheRegion.ClientRoleModelRegion;            var key = path;
key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);            
var result = await _clientRateLimitRepository.CheckReRouteRuleAsync(path);            var data = new ClientRoleModel() { CacheTime = DateTime.Now, Role = result };            if (_options.ClusterEnvironment)
{
RedisHelper.Set(key, data);
//加入redis快取
RedisHelper.Publish(key, data.ToJson());
//發佈事件
}            
else
{
_cache.Remove(key);
}
}        
///
/// 更新呢客戶端路由白名單快取
///
///
///
///
[
HttpPost]
[
Route(“ClientReRouteWhiteList”)]        public async Task UpdateClientReRouteWhiteListCache(string clientid, string path)        {            var region = CzarCacheRegion.ClientReRouteWhiteListRegion;            var key = clientid + path;
key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);            
var result = await _clientRateLimitRepository.CheckClientReRouteWhiteListAsync(clientid, path);            var data = new ClientRoleModel() { CacheTime = DateTime.Now, Role = result };            if (_options.ClusterEnvironment)
{
RedisHelper.Set(key, data);
//加入redis快取
RedisHelper.Publish(key, data.ToJson());
//發佈事件
}            
else
{
_cache.Remove(key);
}
}

[HttpPost]
[
Route(“Rpc”)]        public async Task UpdateRpcCache(string UpUrl)        {            var region = CzarCacheRegion.RemoteInvokeMessageRegion;            var key = UpUrl;
key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);            
var result = await _rpcRepository.GetRemoteMethodAsync(UpUrl);            if (_options.ClusterEnvironment)
{
RedisHelper.Set(key, result);
//加入redis快取
RedisHelper.Publish(key, result.ToJson());
//發佈事件
}            
else
{
_cache.Remove(key);
}
}
}
}

現在基本實現整個快取的更新策略,只要配合後臺管理界面,在相關快取原始資料發送變更時,呼叫對應接口即可完成redis快取的更新,並自動通知集群的所有本機清理快取等待重新獲取。

接口的呼叫方式參考之前我寫的配置信息接口變更那篇即可。

 

性能測試

完成了改造後,我們拿改造前網關、改造後網關、原始Ocelot、直接呼叫API四個環境分別測試性能指標,由於測試環境有效,我直接使用本機環境,然後是Apache ab測試工具測試下相關性能(本測試不一定准確,只作為參考指標),測試的方式是使用100個併發請求10000次,測試結果分別如下。

改造網關性能測試

 

 

改造後網關測試

 

Ocelot預設網關性能

直接呼叫API性能

本測試僅供參考,因為由於網關和服務端都在本機環境部署,所以使用網關和不使用網關性能差別非常小,如果分開部署可能性別差別會明顯寫,這不是本篇討論的重點。

從測試中可以看到,重構的網關改造前和改造後性能有2倍多的提升,且與原生的Ocelot性能非常接近。

最後

 

 

本篇主要講解瞭如何使用redis的發佈訂閱來實現二級快取功能,並提供了快取的更新相關接口供外部程式呼叫,避免出現集群環境下無法更新快取資料導致提取資料不一致情況,但是針對每個客戶端獨立限流這塊集群環境目前還是採用的redis的方式未使用本地快取,如果有寫的不對或有更好方式的,也希望多提寶貴意見。

 

 

    赞(0)

    分享創造快樂