簡介
官網:https://cap.dotnetcore.xyz/
CAP 是什麼?
是一個 EventBus,同時也是一個在微服務或者 SOA 系統中解決分佈式事務問題的一個框架。它有助於創建可擴展,可靠並且易於更改的微服務系統。
什麼是 EventBus?
事件總線是一種機制,它允許不同的組件彼此通信而不彼此瞭解。 組件可以將事件發送到 Eventbus,而無需知道是誰來接聽或有多少其他人來接聽。 組件也可以偵聽 Eventbus 上的事件,而無需知道誰發送了事件。 這樣,組件可以相互通信而無需相互依賴。 同樣,很容易替換一個組件。 只要新組件瞭解正在發送和接收的事件,其他組件就永遠不會知道.
CAP 支持的運輸器
CAP 支持的持久化數據庫
集成 CAP + RabbitMQ + MySQL
安裝 CAP NuGet 包
在你的.NET Core 項目中,通過 NuGet 包管理器安裝 CAP。
dotnet add package DotNetCore.CAP
dotnet add package DotNetCore.CAP.RabbitMQ
dotnet add package DotNetCore.CAP.MySql
dotnet add package DotNetCore.CAP.Dashboard #Dashboard
dotnet add package Pomelo.EntityFrameworkCore.MySql #這個之後主要用於冪等性判斷,可以不要
配置 CAP
/// <summary>
/// 添加分佈式事務服務
/// </summary>
/// <param name="services">服務集合</param>
/// <param name="capSection">cap鏈接項</param>
/// <param name="rabbitMQSection">rabbitmq配置項</param>
/// <param name="expiredTime">成功消息過期時間</param>
/// <returns></returns>
public static IServiceCollection AddMCodeCap(this IServiceCollection services, Action<CapOptions> configure = null, string capSection = "cap", string rabbitMQSection = "rabbitmq")
{
var rabbitMQOptions = ServiceProviderServiceExtensions.GetRequiredService<IConfiguration>(services.BuildServiceProvider()).GetSection(rabbitMQSection).Get<RabbitMQOptions>();
var logger = ServiceProviderServiceExtensions.GetRequiredService<ILogger<CapContext>>(services.BuildServiceProvider());
if (rabbitMQOptions == null)
{
throw new ArgumentNullException("rabbitmq not config.");
}
var capJson = ServiceProviderServiceExtensions.GetRequiredService<IConfiguration>(services.BuildServiceProvider()).GetValue<string>(capSection);
if (string.IsNullOrEmpty(capJson))
{
throw new ArgumentException("cap未設置");
}
//services.AddDbContext<CapContext>(options => options.UseMySql(capJson, ServerVersion.AutoDetect(capJson)));
services.AddCap(x =>
{
//使用RabbitMQ傳輸
x.UseRabbitMQ(opt => { opt = rabbitMQOptions; });
////使用MySQL持久化
x.UseMySql(capJson);
//x.UseEntityFramework<CapContext>();
x.UseDashboard();
//成功消息的過期時間(秒)
x.SucceedMessageExpiredAfter = 10 * 24 * 3600;
x.FailedRetryCount = 5;
//失敗回調,通過企業微信,短信通知人工干預
x.FailedThresholdCallback = (e) =>
{
if (e.MessageType == MessageType.Publish)
{
logger.LogError("Cap發送消息失敗;" + JsonExtension.Serialize(e.Message));
}
else if (e.MessageType == MessageType.Subscribe)
{
logger.LogError("Cap接收消息失敗;" + JsonExtension.Serialize(e.Message));
}
};
configure?.Invoke(x);
});
return services;
}
internal class JsonExtension
{
private static readonly JsonSerializerSettings _jsonSerializerSettings;
internal static JsonSerializerSettings CustomSerializerSettings;
static JsonExtension()
{
_jsonSerializerSettings = DefaultSerializerSettings;
}
internal static JsonSerializerSettings DefaultSerializerSettings
{
get
{
var settings = new JsonSerializerSettings();
// 設置如何將日期寫入JSON文本。默認值爲“IsoDateFormat”
//settings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
// 設置在序列化和反序列化期間如何處理DateTime時區。默認值爲 “RoundtripKind”
//settings.DateTimeZoneHandling = DateTimeZoneHandling.RoundtripKind;
// 設置在序列化和反序列化期間如何處理默認值。默認值爲“Include”
//settings.DefaultValueHandling = DefaultValueHandling.Include;
// 設置寫入JSON文本時DateTime和DateTimeOffset值的格式,以及讀取JSON文本時預期的日期格式。默認值爲“ yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK ”。
settings.DateFormatString = "yyyy-MM-dd HH:mm:ss";
// 設置在序列化和反序列化期間如何處理空值。默認值爲“Include”
//settings.NullValueHandling = NullValueHandling.Include;
// 設置序列化程序在將.net對象序列化爲JSON時使用的契約解析器
settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
// 設置如何處理引用循環(例如,類引用自身)。默認值爲“Error”。
settings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
// 是否格式化文本
settings.Formatting = Formatting.Indented;
//支持將Enum 由默認 Number類型 轉換爲String
//settings.SerializerSettings.Converters.Add(new StringEnumConverter());
//將long類型轉爲string
//settings.SerializerSettings.Converters.Add(new NumberConverter(NumberConverterShip.Int64));
return settings;
}
}
public static T Deserialize<T>(string json, JsonSerializerSettings serializerSettings = null)
{
if (string.IsNullOrEmpty(json)) return default;
if (serializerSettings == null) serializerSettings = _jsonSerializerSettings;
//值類型和String類型
if (typeof(T).IsValueType || typeof(T) == typeof(string))
{
return (T)Convert.ChangeType(json, typeof(T));
}
return JsonConvert.DeserializeObject<T>(json, CustomSerializerSettings ?? serializerSettings);
}
public static object Deserialize(string json, Type type, JsonSerializerSettings serializerSettings = null)
{
if (string.IsNullOrEmpty(json)) return default;
if (serializerSettings == null) serializerSettings = _jsonSerializerSettings;
return JsonConvert.DeserializeObject(json,type, CustomSerializerSettings ?? serializerSettings);
}
public static string Serialize<T>(T obj, JsonSerializerSettings serializerSettings = null)
{
if (obj is null) return string.Empty;
if (obj is string) return obj.ToString();
if (serializerSettings == null) serializerSettings = _jsonSerializerSettings;
return JsonConvert.SerializeObject(obj, CustomSerializerSettings ?? serializerSettings);
}
}
appsettings.json
{
"cap": "Server=127.0.0.1;Port=3306;Database=spring;Uid=root;Pwd=123456;Allow User Variables=true;Pooling=true;Min Pool Size=0;Max Pool Size=100;Connection Lifetime=0;",
"rabbitmq": {
"HostName": "127.0.0.1",
"Port": 5672,
"UserName": "guest",
"Password": "guest",
"VirtualHost": "/"
}
}
使用 CAP 發佈事件
public class YourService
{
private readonly ICapPublisher _capPublisher;
public YourService(ICapPublisher capPublisher)
{
_capPublisher = capPublisher;
}
public async Task DoSomethingAsync()
{
// ... 業務邏輯 ...
await _capPublisher.PublishAsync("your.event.name", new YourEventData { /* ... */ },"callback.name");
}
}
訂閱事件
你需要實現一個事件處理器來訂閱並處理事件。這通常是通過繼承 ICapSubscribe 接口或使用 CAP 的[CapSubscribe]屬性來實現的
public class YourEventHandler : ICapSubscribe
{
[CapSubscribe("your.event.name")]
public async Task Handle(YourEventData eventData)
{
// 處理事件邏輯
}
}
或者,使用特性:
[CapSubscribe("your.event.name")]
public class YourEventHandler
{
public async Task Handle(YourEventData eventData)
{
// 處理事件邏輯
}
}
其它說明
配置
DefaultGroupName
默認值:cap.queue.
默認的消費者組的名字,在不同的 Transports 中對應不同的名字,可以通過自定義此值來自定義不同 Transports 中的名字,以便於查看。
GroupNamePrefix
默認值:Null
爲訂閱 Group 統一添加前綴。 https://github.com/dotnetcore/CAP/pull/780
TopicNamePrefix
默認值: Null
爲 Topic 統一添加前綴。 https://github.com/dotnetcore/CAP/pull/780
Version
默認值:v1
用於給消息指定版本來隔離不同版本服務的消息,常用於A/B測試或者多服務版本的場景。以下是其應用場景:
FailedRetryInterval *
默認值:60 秒
在消息發送的時候,如果發送失敗,CAP將會對消息進行重試,此配置項用來配置每次重試的間隔時間。
在消息消費的過程中,如果消費失敗,CAP將會對消息進行重試消費,此配置項用來配置每次重試的間隔時間。
ConsumerThreadCount *
默認值:1
消費者線程並行處理消息的線程數,當這個值大於1時,將不能保證消息執行的順序。
FailedRetryCount *
默認值:50
重試的最大次數。當達到此設置值時,將不會再繼續重試,通過改變此參數來設置重試的最大次數。
SucceedMessageExpiredAfter
默認值:24*3600 秒(1天后)
成功消息的過期時間(秒)。 當消息發送或者消費成功時候,在時間達到 SucceedMessageExpiredAfter
秒時候將會從 Persistent 中刪除,你可以通過指定此值來設置過期的時間。
FailedMessageExpiredAfter *
默認值:15243600 秒(15天后)
失敗消息的過期時間(秒)。 當消息發送或者消費失敗時候,在時間達到 FailedMessageExpiredAfter
秒時候將會從 Persistent 中刪除,你可以通過指定此值來設置過期的時間。
EnablePublishParallelSend
默認值: false
默認情況下,發送的消息都先放置到內存同一個Channel中,然後線性處理。 如果設置爲 true,則發送消息的任務將由.NET線程池並行處理,這會大大提高發送的速度。
補償事務
某些情況下,消費者需要返回值以告訴發佈者執行結果,以便於發佈者實施一些動作,通常情況下這屬於補償範圍。
你可以在消費者執行的代碼中通過重新發佈一個新消息來通知上游,CAP 提供了一種簡單的方式來做到這一點。 你可以在發送的時候指定 callbackName
來得到消費者的執行結果,通常這僅適用於點對點的消費。以下是一個示例。
例如,在一個電商程序中,訂單初始狀態爲 pending,當商品數量成功扣除時將狀態標記爲 succeeded ,否則爲 failed。
序列化
意味着你可以調整序列化配置
自定義序列化
public class MessageSerializer : ISerializer
{
public Message Deserialize(string json)
{
return JsonExtension.Deserialize<Message>(json);
}
public object Deserialize(object value, Type valueType)
{
if (value is JToken jToken)
{
return jToken.ToObject(valueType);
}
throw new NotSupportedException("Type is not of type JToken");
}
public ValueTask<Message> DeserializeAsync(TransportMessage transportMessage, Type valueType)
{
if (valueType == null || transportMessage.Body.IsEmpty)
{
return ValueTask.FromResult(new DotNetCore.CAP.Messages.Message(transportMessage.Headers, null));
}
var json = Encoding.UTF8.GetString(transportMessage.Body.ToArray());
return ValueTask.FromResult(new DotNetCore.CAP.Messages.Message(transportMessage.Headers, JsonExtension.Deserialize(json, valueType)));
}
public bool IsJsonType(object jsonObject)
{
return jsonObject is JsonToken || jsonObject is JToken;
}
public string Serialize(Message message)
{
return JsonExtension.Serialize(message);
}
public ValueTask<TransportMessage> SerializeAsync(Message message)
{
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}
if (message.Value == null)
{
return ValueTask.FromResult(new TransportMessage(message.Headers, null));
}
var json = JsonExtension.Serialize(message.Value);
return ValueTask.FromResult(new TransportMessage(message.Headers, Encoding.UTF8.GetBytes(json)));
}
}
然後將你的實現註冊到容器中:
services.AddSingleton<DotNetCore.CAP.Serialization.ISerializer, MessageSerializer>();
services.AddCap(x =>
{ xxx
}
事務
CAP 不直接提供開箱即用的基於 DTC 或者 2PC 的分佈式事務,相反我們提供一種可以用於解決在分佈式事務遇到的問題的一種解決方案。
在分佈式環境中,由於涉及通訊的開銷,使用基於2PC或DTC的分佈式事務將非常昂貴,在性能方面也同樣如此。另外由於基於2PC或DTC的分佈式事務同樣受CAP定理的約束,當發生網絡分區時它將不得不放棄可用性(CAP中的A)。
針對於分佈式事務的處理,CAP 採用的是“異步確保”這種方案。類似於Java中Seata的Saga模式
冪等性
在說冪等性之前,我們先來說下關於消費端的消息交付。
由於CAP不是使用的 MS DTC 或其他類型的2PC分佈式事務機制,所以存在至少消息嚴格交付一次的問題,具體的說在基於消息的系統中,存在以下三種可能:
- Exactly Once() (僅有一次)
- At Most Once (最多一次)
- At Least Once (最少一次)
在CAP中,我們採用的交付保證爲 At Least Once。
由於我們具有臨時存儲介質(數據庫表),也許可以做到 At Most Once, 但是爲了嚴格保證消息不會丟失,我們沒有提供相關功能或配置。
以自然的方式處理冪等消息
通常情況下,保證消息被執行多次而不會產生意外結果是很自然的一種方式是採用操作對象自帶的一些冪等功能。比如:
數據庫提供的 INSERT ON DUPLICATE KEY UPDATE
或者是採取類型的程序判斷行爲。
顯式處理冪等消息
另外一種處理冪等性的方式就是在消息傳遞的過程中傳遞ID,然後由單獨的消息跟蹤器來處理。
下面我們基於MySql和Redis實現顯式處理冪等消息
public interface IMessageTracker
{
Task<bool> HasProcessedAsync(string msgId);
bool HasProcessed(string msgId);
Task MarkAsProcessedAsync(string msgId);
void MarkAsProcessed(string msgId);
}
internal class MessageTrackLog
{
public MessageTrackLog(string messageId)
{
MessageId = messageId;
CreatedTime = DateTime.Now;
}
public string MessageId { get; set; }
public DateTime CreatedTime { get; set; }
}
public class MessageData<T>
{
public string Id { get; set; }
public T MessageBody { get; set; }
public DateTime CreatedTime { get; set; }
public MessageData(T messageBody)
{
MessageBody = messageBody;
CreatedTime = DateTime.Now;
Id = SnowflakeGenerator.Instance().GetId().ToString();
}
}
internal class SnowflakeGenerator
{
private static long machineId;//機器ID
private static long datacenterId = 0L;//數據ID
private static long sequence = 0L;//計數從零開始
private static long twepoch = 687888001020L; //惟一時間隨機量
private static long machineIdBits = 5L; //機器碼字節數
private static long datacenterIdBits = 5L;//數據字節數
public static long maxMachineId = -1L ^ -1L << (int)machineIdBits; //最大機器ID
private static long maxDatacenterId = -1L ^ (-1L << (int)datacenterIdBits);//最大數據ID
private static long sequenceBits = 12L; //計數器字節數,12個字節用來保存計數碼
private static long machineIdShift = sequenceBits; //機器碼數據左移位數,就是後面計數器佔用的位數
private static long datacenterIdShift = sequenceBits + machineIdBits;
private static long timestampLeftShift = sequenceBits + machineIdBits + datacenterIdBits; //時間戳左移動位數就是機器碼+計數器總字節數+數據字節數
public static long sequenceMask = -1L ^ -1L << (int)sequenceBits; //一微秒內能夠產生計數,若是達到該值則等到下一微妙在進行生成
private static long lastTimestamp = -1L;//最後時間戳
private static object syncRoot = new object();//加鎖對象
static SnowflakeGenerator snowflake;
static SnowflakeGenerator()
{
snowflake = new SnowflakeGenerator();
}
public static SnowflakeGenerator Instance()
{
if (snowflake == null)
snowflake = new SnowflakeGenerator();
return snowflake;
}
public SnowflakeGenerator()
{
Snowflakes(0L, -1);
}
public SnowflakeGenerator(long machineId)
{
Snowflakes(machineId, -1);
}
public SnowflakeGenerator(long machineId, long datacenterId)
{
Snowflakes(machineId, datacenterId);
}
private void Snowflakes(long machineId, long datacenterId)
{
if (machineId >= 0)
{
if (machineId > maxMachineId)
{
throw new Exception("機器碼ID非法");
}
SnowflakeGenerator.machineId = machineId;
}
if (datacenterId >= 0)
{
if (datacenterId > maxDatacenterId)
{
throw new Exception("數據中心ID非法");
}
SnowflakeGenerator.datacenterId = datacenterId;
}
}
/// <summary>
/// 生成當前時間戳
/// </summary>
/// <returns>毫秒</returns>
private static long GetTimestamp()
{
//讓他2000年開始
return (long)(DateTime.UtcNow - new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
}
/// <summary>
/// 獲取下一微秒時間戳
/// </summary>
/// <param name="lastTimestamp"></param>
/// <returns></returns>
private static long GetNextTimestamp(long lastTimestamp)
{
long timestamp = GetTimestamp();
int count = 0;
while (timestamp <= lastTimestamp)//這裏獲取新的時間,可能會有錯,這算法與comb同樣對機器時間的要求很嚴格
{
count++;
if (count > 10)
throw new Exception("機器的時間可能不對");
System.Threading.Thread.Sleep(1);
timestamp = GetTimestamp();
}
return timestamp;
}
/// <summary>
/// 獲取長整形的ID
/// </summary>
/// <returns></returns>
public long GetId()
{
lock (syncRoot)
{
long timestamp = GetTimestamp();
if (SnowflakeGenerator.lastTimestamp == timestamp)
{ //同一微妙中生成ID
sequence = (sequence + 1) & sequenceMask; //用&運算計算該微秒內產生的計數是否已經到達上限
if (sequence == 0)
{
//一微妙內產生的ID計數已達上限,等待下一微妙
timestamp = GetNextTimestamp(SnowflakeGenerator.lastTimestamp);
}
}
else
{
//不一樣微秒生成ID
sequence = 0L;
}
if (timestamp < lastTimestamp)
{
throw new Exception("時間戳比上一次生成ID時時間戳還小,故異常");
}
SnowflakeGenerator.lastTimestamp = timestamp; //把當前時間戳保存爲最後生成ID的時間戳
long Id = ((timestamp - twepoch) << (int)timestampLeftShift)
| (datacenterId << (int)datacenterIdShift)
| (machineId << (int)machineIdShift)
| sequence;
return Id;
}
}
}
基於Redis顯式處理冪等消息
internal class RedisMessageTracker : IMessageTracker
{
#region 屬性和字段
private const string KEY_PREFIX = "msgtracker:"; // 默認Key前綴
private const int DEFAULT_CACHE_TIME = 60 * 60 * 24 * 3; // 默認緩存時間爲3天,單位爲秒
private readonly IDatabase _redisDatabase;
#endregion
//依賴StackExchange.Redis;
public RedisMessageTracker(ConnectionMultiplexer multiplexer)
{
_redisDatabase = multiplexer.GetDatabase();
}
public bool HasProcessed(string msgId)
{
return _redisDatabase.KeyExists(KEY_PREFIX + msgId);
}
public async Task<bool> HasProcessedAsync(string msgId)
{
return await _redisDatabase.KeyExistsAsync(KEY_PREFIX + msgId);
}
public void MarkAsProcessed(string msgId)
{
var msgRecord = new MessageTrackLog(msgId);
_redisDatabase.StringSet($"{KEY_PREFIX}{msgId}", JsonExtension.Serialize(msgRecord), TimeSpan.FromMinutes(DEFAULT_CACHE_TIME));
}
public async Task MarkAsProcessedAsync(string msgId)
{
var msgRecord = new MessageTrackLog(msgId);
await _redisDatabase.StringSetAsync($"{KEY_PREFIX}{msgId}", JsonExtension.Serialize(msgRecord), TimeSpan.FromMinutes(DEFAULT_CACHE_TIME));
}
}
public static IServiceCollection AddRedisMessageTracker(this IServiceCollection services)
{
services.AddScoped<IMessageTracker, RedisMessageTracker>();
return services;
}
基於Mysql顯式處理冪等消息
internal class MySqlMessageTracker : IMessageTracker
{
private readonly CapContext _capContext;
public MySqlMessageTracker(CapContext capContext)
{
_capContext = capContext;
}
public bool HasProcessed(string msgId)
{
return _capContext.MessageTrackLogs.Any(x => x.MessageId == msgId);
}
public Task<bool> HasProcessedAsync(string msgId)
{
return _capContext.MessageTrackLogs.AnyAsync(x => x.MessageId == msgId);
}
public void MarkAsProcessed(string msgId)
{
MessageTrackLog messageTrackLog = new MessageTrackLog(msgId);
_capContext.MessageTrackLogs.Add(messageTrackLog);
_capContext.SaveChanges();
}
public async Task MarkAsProcessedAsync(string msgId)
{
MessageTrackLog messageTrackLog = new MessageTrackLog(msgId);
await _capContext.MessageTrackLogs.AddAsync(messageTrackLog);
await _capContext.SaveChangesAsync();
}
}
internal class CapContext : DbContext
{
public CapContext(DbContextOptions<CapContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 可以在這裏進行模型配置
modelBuilder.Entity<MessageTrackLog>().ToTable("message_track_log");
modelBuilder.Entity<MessageTrackLog>().HasKey(b => b.MessageId);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
}
public DbSet<MessageTrackLog> MessageTrackLogs { get; set; }
}
public static IServiceCollection AddMySqlMessageTracker(this IServiceCollection services)
{
services.AddScoped<IMessageTracker, MySqlMessageTracker>();
var serviceProvider = services.BuildServiceProvider();
using (var context = serviceProvider.GetService<CapContext>())
{
context.Database.ExecuteSqlRaw(@"
CREATE TABLE IF NOT EXISTS `message_track_log` (
`MessageId` varchar(255) CHARACTER SET utf8mb4 NOT NULL,
`CreatedTime` datetime NOT NULL,
CONSTRAINT `PK_message_track_log` PRIMARY KEY (`MessageId`)
) CHARACTER SET=utf8mb4;
");
}
return services;
}
使用
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly IMessageTracker _messageTracker;
public WeatherForecastController(IMessageTracker messageTracker)
{
_messageTracker = messageTracker;
}
[CapSubscribe("order.test")]
[NonAction]
public void OrderTest(MessageData<string> messageData)
{
try
{
if (_messageTracker.HasProcessed(messageData.Id))
return;
Console.WriteLine("業務邏輯:"+messageData.MessageBody);
//xxxx
_messageTracker.MarkAsProcessed(messageData.Id);
}
catch (Exception ex)
{
throw ex;
}
}
}
監控
Consul
CAP的 Dashboard 使用 Consul 作爲服務發現來顯示其他節點的數據,然後你就在任意節點的 Dashboard 中切換到 Servers 頁面看到其他的節點。
通過點擊 Switch 按鈕來切換到其他的節點看到其他節點的數據,而不必訪問很多地址來分別查看。
以下是一個配置示例, 你需要在每個節點分別配置:
services.AddCap(x =>
{
x.UseMySql(Configuration.GetValue<string>("ConnectionString"));
x.UseRabbitMQ("localhost");
x.UseDashboard();
x.UseConsulDiscovery(_ =>
{
_.DiscoveryServerHostName = "localhost";
_.DiscoveryServerPort = 8500;
_.CurrentNodeHostName = Configuration.GetValue<string>("ASPNETCORE_HOSTNAME");
_.CurrentNodePort = Configuration.GetValue<int>("ASPNETCORE_PORT");
_.NodeId = Configuration.GetValue<string>("NodeId");
_.NodeName = Configuration.GetValue<string>("NodeName");
});
});
啓用 Dashboard
首先,你需要安裝Dashboard的 NuGet 包。
PM> Install-Package DotNetCore.CAP.Dashboard
然後,在配置中添加如下代碼:
services.AddCap(x =>
{
x.UseDashboard();
});
默認情況下,你可以訪問 http://localhost:xxx/cap
這個地址打開Dashboard。
Dashboard 配置項
- PathBase
默認值:N/A
當位於代理後時,通過配置此參數可以指定代理請求前綴。
- PathMatch *
默認值:'/cap'
你可以通過修改此配置項來更改Dashboard的訪問路徑。
- StatsPollingInterval
默認值:2000 毫秒
此配置項用來配置Dashboard 前端 獲取狀態接口(/stats)的輪詢時間
- AllowAnonymousExplicit
Default: true
顯式允許對 CAP 儀表板 API 進行匿名訪問,當啓用ASP.NET Core 全局授權篩選器請啓用 AllowAnonymous。
- AuthorizationPolicy
Default: null.
Dashboard 的授權策略。 需設置 AllowAnonymousExplicit
爲 false。