分庫分表如何進行極致的優化
- 2022 年 2 月 16 日
- 筆記
- dotNETCore, efcore, entityframeworkcore, Sharding, sharding-core, shardingcore, 分庫, 分表
分庫分表下極致的優化
題外話
這邊說一句題外話,就是ShardingCore
目前已經正式加入 NCC 開源組織了,也是希望框架和社區能發展的越來越好,希望為更多.netter提供解決方案和開源組件
介紹
依照慣例首先介紹本期主角:ShardingCore
一款ef-core下高性能、輕量級針對分表分庫讀寫分離的解決方案,具有零依賴、零學習成本、零業務代碼入侵
dotnet下唯一一款全自動分表,多字段分表框架,擁有高性能,零依賴、零學習成本、零業務代碼入侵,並且支持讀寫分離動態分表分庫,同一種路由可以完全自定義的新星組件框架
你的star和點贊是我堅持下去的最大動力,一起為.net生態提供更好的解決方案
項目地址
本次優化點
直奔主題來講下本次的極致優化具體是優化了什麼,簡單說就是CircuitBreaker
和FastFail
.
斷路器CircuitBreaker
我們假設這麼一個場景,現在我們有一個訂單order表,訂單會按照月份進行分片,那麼訂單表會有如下幾個order_202201
、order_202202
、order_202203
、order_202204
、order_202205
,假設我們有5張表。
首先我們來看一條普通的語句
select * from order where id='xxx' limit 1
這是一條普通的不能在普通的sql了,查詢第一條id是xxx的訂單,
那麼他在分表下面會如何運行
//開啟5個線程並發查詢
select * from order_202201 where id='xxx' limit 1
select * from order_202202 where id='xxx' limit 1
select * from order_202203 where id='xxx' limit 1
select * from order_202204 where id='xxx' limit 1
select * from order_202205 where id='xxx' limit 1
//查詢出來的結果在內存中進行聚合成一個list集合
//然後在對這個list集合進行第一條的獲取
list.Where(o=>o is not null).FirstOrDefault()
這個操作我相信很多同學都是可以了解的,稍微熟悉點分表分庫的同學應該都知道這是基本操作了,但是這個操作看似高效(時間上)但是在連接數上而言並不是那麼的高效,因為同一時間需要開打的連接數將由5個
那麼在這個背景下ShardingCore
參考ShardingSphere 提供了更加友好的連接控制和內存聚合模式ConnectionMode
這個張圖上我們可以清晰的看到不同的數據庫直接才用了一個並發限制,比如設置的是2,那麼在相同庫裏面的查詢將是每2個一組,進行查詢,這樣可以控制在同一個數據庫下的連接數,進而解決了客戶端連接模式下的連接數消耗猛烈的一個弊端。
//開啟5個線程並發查詢
{
//並行
select * from order_202201 where id='xxx' limit 1
select * from order_202202 where id='xxx' limit 1
}
//串行
{
//並行
select * from order_202203 where id='xxx' limit 1
select * from order_202204 where id='xxx' limit 1
}
//串行
{
select * from order_202205 where id='xxx' limit 1
}
//查詢出來的結果在內存中進行聚合成一個list集合
//然後在對這個list集合進行第一條的獲取
list.Where(o=>o is not null).FirstOrDefault()
到目前為止這邊已經對分片的查詢優化到了一個新的高度。但是雖然我們優化了連接數的處理,但是就查詢速度而言基本上是沒有之前的那麼快,可以說和你分組的組數成線性增加時間的消耗。
所以到此為止ShardingCore
又再一次進化
出了全新的翅膀CircuitBreaker
斷路器,我們繼續往下看
我們現在的sql是
select * from order where id='xxx' limit 1
那麼如果我們針對這個sql進行優化呢,譬如
select * from order where id='xxx' order by create_time desc limit 1
同樣是查詢第一條,添加了一個order排序那麼情況就會大大的不一樣,首先我們來觀察我們的分片查詢
//開啟5個線程並發查詢
-- select * from order_202201 where id='xxx' order by create_time desc limit 1
-- select * from order_202202 where id='xxx' order by create_time desc limit 1
-- select * from order_202203 where id='xxx' order by create_time desc limit 1
-- select * from order_202204 where id='xxx' order by create_time desc limit 1
-- select * from order_202205 where id='xxx' order by create_time desc limit 1
-- 拋棄上述寫法
select * from order_202205 where id='xxx' order by create_time desc limit 1
select * from order_202204 where id='xxx' order by create_time desc limit 1
select * from order_202203 where id='xxx' order by create_time desc limit 1
select * from order_202202 where id='xxx' order by create_time desc limit 1
select * from order_202201 where id='xxx' order by create_time desc limit 1
如果在連接模式下那麼他們將會是2個一組,那麼我們在查詢第一組的結果後是否就可以直接拋棄掉下面的所有查詢,也就是我們只需要查詢
select * from order_202205 where id='xxx' order by create_time desc limit 1
select * from order_202204 where id='xxx' order by create_time desc limit 1
只要他們是有返回一個以上的數據那麼本次分片查詢將會被終止,ShardingCore
目前的大殺器,本來年前已經開發完成了,奈何太懶只是發佈了版本並沒有相關的說明和使用方法
CircuitBreaker
斷路器,它具有類似拉閘中斷操作的功能,這邊簡單說下linq操作下的部分方法的斷路器點在哪裡
方法名 | 是否支持中斷操作 | 中斷條件 |
---|---|---|
First | 支持 | 按順序查詢到第一個時就可以放棄其餘查詢 |
FirstOrDefault | 支持 | 按順序查詢到第一個時就可以放棄其餘查詢 |
Last | 支持 | 按順序倒敘查詢到第一個時就可以放棄其餘查詢 |
LastOrDefault | 支持 | 按順序倒敘查詢到第一個時就可以放棄其餘查詢 |
Single | 支持 | 查詢到兩個時就可以放棄,因為元素個數大於1個了需要拋錯 |
SingleOrDefault | 支持 | 查詢到兩個時就可以放棄,因為元素個數大於1個了需要拋錯 |
Any | 支持 | 查詢一個結果true就可以放棄其餘查詢 |
All | 支持 | 查詢到一個結果fasle就可以放棄其餘查詢 |
Contains | 支持 | 查詢一個結果true就可以放棄其餘查詢 |
Count | 不支持 | — |
LongCount | 不支持 | — |
Max | 支持 | 按順序最後一條並且查詢最大字段是分片順序同字段是,max的屬性只需要查詢一條記錄 |
Min | 支持 | 按順序第一條並且查詢最小字段是分片順序同字段,min的屬性只需要查詢一條記錄 |
Average | 不支持 | — |
Sum | 不支持 | — |
這邊其實只有三個操作是任何狀態下都可以支持中斷,其餘操作需要在額外條件順序查詢的情況下才可以,並且我們本次查詢分片涉及到過多的後綴表那麼性能和資源的利用將會大大提升
查詢配置
廢話不多說我們開始以mysql作為本次案例(不要問我為什麼不用SqlServer,因為寫文章的時候我是mac電腦),這邊我們創建一個項目新建一個訂單按月分表
新建項目
安裝依賴
添加訂單表和訂單表映射
public class Order
{
public string Id { get; set; }
public string Name { get; set; }
public DateTime Createtime { get; set; }
}
public class OrderMap : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.HasKey(o => o.Id);
builder.Property(o => o.Id).HasMaxLength(32).IsUnicode(false);
builder.Property(o => o.Name).HasMaxLength(255);
builder.ToTable(nameof(Order));
}
}
添加DbContext
public class ShardingDbContext:AbstractShardingDbContext,IShardingTableDbContext
{
public ShardingDbContext(DbContextOptions<ShardingDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfiguration(new OrderMap());
}
public IRouteTail RouteTail { get; set; }
}
添加訂單分片路由
從5月份開始按創建時間建表
public class OrderRoute:AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute<Order>
{
public override void Configure(EntityMetadataTableBuilder<Order> builder)
{
builder.ShardingProperty(o => o.Createtime);
}
public override bool AutoCreateTableByTime()
{
return true;
}
public override DateTime GetBeginTime()
{
return new DateTime(2021, 5, 1);
}
}
啟動配置
簡單的配置啟動創建表和庫,並且添加種子數據
ILoggerFactory efLogger = LoggerFactory.Create(builder =>
{
builder.AddFilter((category, level) => category == DbLoggerCategory.Database.Command.Name && level == LogLevel.Information).AddConsole();
});
builder.Services.AddControllers();
builder.Services.AddShardingDbContext<ShardingDbContext>()
.AddEntityConfig(op =>
{
op.CreateShardingTableOnStart = true;
op.EnsureCreatedWithOutShardingTable = true;
op.AddShardingTableRoute<OrderRoute>();
op.UseShardingQuery((conStr, b) =>
{
b.UseMySql(conStr, new MySqlServerVersion(new Version())).UseLoggerFactory(efLogger);
});
op.UseShardingTransaction((conn, b) =>
{
b.UseMySql(conn, new MySqlServerVersion(new Version())).UseLoggerFactory(efLogger);
});
}).AddConfig(op =>
{
op.ConfigId = "c1";
op.AddDefaultDataSource("ds0", "server=127.0.0.1;port=3306;database=db2;userid=root;password=root;");
op.ReplaceTableEnsureManager(sp=>new MySqlTableEnsureManager<ShardingDbContext>());
}).EnsureConfig();
var app = builder.Build();
app.Services.GetRequiredService<IShardingBootstrapper>().Start();
using (var scope=app.Services.CreateScope())
{
var shardingDbContext = scope.ServiceProvider.GetRequiredService<ShardingDbContext>();
if (!shardingDbContext.Set<Order>().Any())
{
var begin = new DateTime(2021, 5, 2);
List<Order> orders = new List<Order>(8);
for (int i = 0; i < 8; i++)
{
orders.Add(new Order()
{
Id = i.ToString(),
Name = $"{begin:yyyy-MM-dd HH:mm:ss}",
Createtime = begin
});
begin = begin.AddMonths(1);
}
shardingDbContext.AddRange(orders);
shardingDbContext.SaveChanges();
}
}
app.UseAuthorization();
app.MapControllers();
app.Run();
這邊默認連接模式的分組是Environment.ProcessorCount
編寫查詢
沒有配置的情況下那麼這個查詢將是十分糟糕
接下來我們將配置Order的查詢
public class OrderQueryConfiguration:IEntityQueryConfiguration<Order>
{
public void Configure(EntityQueryBuilder<Order> builder)
{
//202105,202106...是默認的順序,false表示使用反向排序,就是如果存在分片那麼分片的tail將進行反向排序202202,202201,202112,202111....
builder.ShardingTailComparer(Comparer<string>.Default, false);
//order by createTime asc的順序和分片ShardingTailComparer一樣那麼就用true
//但是目前ShardingTailComparer是倒序所以order by createTime asc需要和他一樣必須要是倒序,倒序就是false
builder.AddOrder(o => o.CreateTime,false);
//配置當不存在Order的時候如果我是FirstOrDefault那麼將採用和ShardingTailComparer相反的排序執行因為是false
//默認從最早的表開始查詢
builder.AddDefaultSequenceQueryTrip(false, CircuitBreakerMethodNameEnum.FirstOrDefault);
////默認從最近表開始查詢
//builder.AddDefaultSequenceQueryTrip(true, CircuitBreakerMethodNameEnum.FirstOrDefault);
//內部配置單表查詢的FirstOrDefault connections limit限制為1
builder.AddConnectionsLimit(1, LimitMethodNameEnum.FirstOrDefault);
}
}
public class OrderRoute:AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute<Order>
{
//......
//配置路由才用這個對象查詢
public override IEntityQueryConfiguration<Order> CreateEntityQueryConfiguration()
{
return new OrderQueryConfiguration();
}
}
帶配置的Order
現在我們將默認的配置修改回正確
//不合適因為一般而言我們肯定是查詢最新的所以應該和ShardingComparer一樣都是倒序查詢
//builder.AddDefaultSequenceQueryTrip(false, CircuitBreakerMethodNameEnum.FirstOrDefault);
builder.AddDefaultSequenceQueryTrip(true, CircuitBreakerMethodNameEnum.FirstOrDefault);
當然如果你希望本次查詢不使用配置的連接數限制可以進行如下操作
_shardingDbContext.Set<Order>().UseConnectionMode(2).Where(o=>o.Id=="7").FirstOrDefaultAsync();
結論:當我們配置了默認分片表應該以何種順序進行分片聚合時,如果相應的查詢方法也進行了配置那麼將這種查詢視為順序查詢,
所有的順序查詢都符合上述表格模式,遇到對應的將直接進行熔斷,不在進行後續的處理直接返回,保證高性能和防止無意義的查詢。
快速失敗FastFail
顧名思義就是快速失敗,但是很多小夥伴可能不清楚這個快速失敗的意思,失敗就是失敗了為什麼有快速失敗一說,因為ShardingCore內部的本質是將一個sql語句進行才分N條然後並行執行
-- 普通sql
select * from order where id='1' or id='2'
-- 分片sql
select * from order_1 where id='1' or id='2'
select * from order_2 where id='1' or id='2'
-- 分別對這兩個sql進行並行執行
在正常情況下程序是沒有什麼問題的,但是由於程序是並行查詢後迭代聚合所以會帶來一個問題,就是假設執行order_1的線程掛掉了,那麼Task.WhenAll會一致等待所有線程完成,然後拋出響應的錯誤,
那麼這在很多情況下等於其餘線程都在多無意義的操作,各自管各自。
static async Task Main(string[] args)
{
try
{
await Task.WhenAll(DoSomething1(), DoSomething2());
Console.WriteLine("execute success");
}
catch
{
Console.WriteLine("error");
}
Console.ReadLine();
}
static async Task<int> DoSomething1()
{
for (int i = 0; i < 10; i++)
{
if (i == 2)
throw new Exception("111");
await Task.Delay(1000);
Console.WriteLine("DoSomething1"+i);
}
return 1;
}
static async Task<int> DoSomething2()
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(1000);
Console.WriteLine("DoSomething2"+i);
}
return 1;
}
代碼很簡單就是Task.WhenAll
的時候執行兩個委託方法,然後讓其中一個快速拋異常的情況下看看是否馬上返回
結果是TaskWhenAll
哪怕出現異常也需要等待所有的線程完成任務,這會在某些情況下浪費不必要的性能,所以這邊ShardingCore
參考資料採用了FastFail
版本的
public static Task WhenAllFailFast(params Task[] tasks)
{
if (tasks is null || tasks.Length == 0) return Task.CompletedTask;
// defensive copy.
var defensive = tasks.Clone() as Task[];
var tcs = new TaskCompletionSource();
var remaining = defensive.Length;
Action<Task> check = t =>
{
switch (t.Status)
{
case TaskStatus.Faulted:
// we 'try' as some other task may beat us to the punch.
tcs.TrySetException(t.Exception.InnerException);
break;
case TaskStatus.Canceled:
// we 'try' as some other task may beat us to the punch.
tcs.TrySetCanceled();
break;
default:
// we can safely set here as no other task remains to run.
if (Interlocked.Decrement(ref remaining) == 0)
{
// get the results into an array.
tcs.SetResult();
}
break;
}
};
foreach (var task in defensive)
{
task.ContinueWith(check, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}
return tcs.Task;
}
採用failfast後當前主線程會直接在錯誤時返回,其餘線程還是繼續執行,需要自行進行canceltoken.cancel或者通過共享變量來取消執行
總結
ShardngCore
目前還在不斷努力成長中,也希望各位多多包涵可以在使用中多多提出響應的意見和建議
參考資料
下期預告
下一篇我們將講解如何讓流式聚合支持更多的sql查詢,如何將不支持的sql降級為union all
分表分庫組件求贊求star
您的支持是開源作者能堅持下去的最大動力
- Github ShardingCore
- Gitee ShardingCore
QQ群:771630778
個人QQ:326308290(歡迎技術支持提供您寶貴的意見)
個人郵箱:[email protected]