前言
.Net Core為我們提供了一套強大的Configuration配置系統,使用簡單擴充套件性強。通過這套配置系統我們可以將Json、Xml、Ini等資料來源載入到程式中,也可以自己擴充套件其他形式的儲存源。今天我們要做的就是通過自定義的方式為其擴充套件Etcd資料來源操作。
何為Etdc
在使用etcd之前我們先介紹一下Etcd,我相信很多同學都早有耳聞。Etcd是一款高可用、強一致的分散式KV儲存系統,它內部採用raft協議作為一致性演算法,本身也是基於GO語言開發的,最新版本為v3.4.9,具體版本下載地址可參閱官方GitHub地址。相信瞭解過K8S的同學對這個肯定不陌生,它是K8S的資料管理系統。官方地址為https://etcd.io/。
在此之前,我相信大家已經瞭解過很多儲存系統了,Etcd到底能實現了什麼功能呢?其一用於配置中心和服務發現,再者也可以實現分散式鎖和訊息系統。它本身就是基於目錄型儲存,並且內部有一套強大的Watch機制可以監聽針對節點和資料的操作變化,每次對節點的事務操作都會有對於的版本資訊。
Etcd VS Zookeeper
通過上面的介紹是不是感覺和Zookeeper有點類似呢???,網上有很多很多關於Etcd和Zookeeper的對比文章,大致如下可以得到以下結論
功能 | Etcd | Zookeeper |
---|---|---|
分散式鎖 | 有(採用節點版本號資訊) | 有(採用臨時節點和順序臨時節點) |
watcher | 有 | 有 |
一致性演算法 | raft | zab |
選舉 | 有 | 有 |
後設資料(metadata)儲存 | 有 | 有 |
應用場景 | Etcd | Zookeeper |
---|---|---|
釋出與訂閱(配置中心) | 有(不限次Watch) | 有(一次性觸發的,需要重新註冊Watch) |
軟負載均衡 | 有 | 有 |
命名服務(Naming Service) | 有 | 有 |
服務發現 | 有(基於租約節點) | 有(基於臨時節點) |
分散式通知/協調 | 有 | 有 |
叢集管理與Master選舉 | 有 | 有 |
分散式鎖 | 有 | 有 |
分散式佇列 | 有 | 有 |
- 更輕量級(Etcd基於GO語言開發,Zookeeper基於Java開發)、更易用(開箱即用)
- 高負載下的穩定讀寫
- 資料模型的多版本併發控制
- 穩定的watcher功能,通知訂閱者監聽值的變化(Zookeeper基於資料的監聽是一次性的,每次監聽完成還需重新註冊)
- 客戶端協議使用GRPC協議,支援語言更廣泛
在.Net Core中使用Etcd
在Nuget上可以搜尋到很多.Net Core的Etcd客戶端驅動程式,我使用了下載量最多的一個名字叫dotnet-etcd的驅動包,順便找到了它在GayHub上,不好意思手滑打錯了???GitHub上的專案地址,大概學習了一下基本的使用方式。其實我們結合Configuration配置這一塊,只需要兩個功能。一個是Get獲取資料,另一個是Watch節點變化(更新資料會用到)。個人認為,前期有目有邊界的學習還是非常總要的。
Configuration擴充套件Etcd
前面我們講到過自定義擴充套件Configuration是非常方便的,相信瞭解過Configuration相關原始碼的小夥伴們已經非常熟悉了,大致總結一下分為三步:
- 編寫IConfigurationBuilder擴充套件方法,我們這裡叫AddEtcd
- 編寫實現IConfigurationSource的配置源資訊類,我們這裡叫EtcdConfigurationSource
- 編寫繼承自ConfigurationProvider的ConfigurationSource的配置資料提供類,我們這裡叫EtcdConfigurationProvider
首先是定義擴充套件類EtcdConfigurationExtensions,這個類是針對IConfigurationBuilder的擴充套件方法,實現如下
public static class EtcdConfigurationExtensions
{
/// <summary>
/// AddEtcd擴充套件方法
/// </summary>
/// <param name="serverAddress">Etcd地址</param>
/// <param name="path">讀取路徑</param>
/// <returns></returns>
public static IConfigurationBuilder AddEtcd(this IConfigurationBuilder builder, string serverAddress,string path)
{
return AddEtcd(builder, serverAddress:serverAddress, path: path,reloadOnChange: false);
}
/// <summary>
/// AddEtcd擴充套件方法
/// </summary>
/// <param name="serverAddress">Etcd地址</param>
/// <param name="path">讀取路徑</param>
/// <param name="reloadOnChange">如果資料傳送改變是否重新整理</param>
/// <returns></returns>
public static IConfigurationBuilder AddEtcd(this IConfigurationBuilder builder, string serverAddress, string path, bool reloadOnChange)
{
return AddEtcd(builder,options => {
options.Address = serverAddress;
options.Path = path;
options.ReloadOnChange = reloadOnChange;
});
}
public static IConfigurationBuilder AddEtcd(this IConfigurationBuilder builder, Action<EtcdOptions> options)
{
EtcdOptions etcdOptions = new EtcdOptions();
options.Invoke(etcdOptions);
return builder.Add(new EtcdConfigurationSource { EtcdOptions = etcdOptions });
}
}
這裡我還定義了一個EtcdOptions的POCO,用於承載讀取Etcd的配置屬性
public class EtcdOptions
{
/// <summary>
/// Etcd地址
/// </summary>
public string Address { get; set; }
/// <summary>
/// Etcd訪問使用者名稱
/// </summary>
public string UserName { get; set; }
/// <summary>
/// Etcd訪問密碼
/// </summary>
public string PassWord { get; set; }
/// <summary>
/// Etcd讀取路徑
/// </summary>
public string Path { get; set; }
/// <summary>
/// 資料變更是否重新整理讀取
/// </summary>
public bool ReloadOnChange { get; set; }
}
接下來我們定義EtcdConfigurationSource,這個類非常簡單就是返回一個配置提供物件
public class EtcdConfigurationSource : IConfigurationSource
{
public EtcdOptions EtcdOptions { get; set; }
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new EtcdConfigurationProvider(EtcdOptions);
}
}
真正的讀取操作都在EtcdConfigurationProvider裡
public class EtcdConfigurationProvider : ConfigurationProvider
{
private readonly string _path;
private readonly bool _reloadOnChange;
private readonly EtcdClient _etcdClient;
public EtcdConfigurationProvider(EtcdOptions options)
{
//例項化EtcdClient
_etcdClient = new EtcdClient(options.Address,username: options.UserName,password: options.PassWord);
_path = options.Path;
_reloadOnChange = options.ReloadOnChange;
}
/// <summary>
/// 重寫載入方法
/// </summary>
public override void Load()
{
//讀取資料
LoadData();
//資料發生變化是否重新載入
if (_reloadOnChange)
{
ReloadData();
}
}
private void LoadData()
{
//讀取Etcd裡的資料
string result = _etcdClient.GetValAsync(_path).GetAwaiter().GetResult();
if (string.IsNullOrEmpty(result))
{
return;
}
//轉換一下資料結構,這裡我使用的是json格式
//讀取的資料只要賦值到Data屬性上即可,IConfiguration真正讀取的資料就是儲存到Data的字典資料
Data = ConvertData(result);
}
private IDictionary<string,string> ConvertData(string result)
{
byte[] array = Encoding.UTF8.GetBytes(result);
MemoryStream stream = new MemoryStream(array);
//JsonConfigurationFileParser是將json資料轉換為Configuration可讀取的結構(複製JsonConfiguration類庫裡的???)
return JsonConfigurationFileParser.Parse(stream);
}
private void ReloadData()
{
WatchRequest request = new WatchRequest()
{
CreateRequest = new WatchCreateRequest()
{
//需要轉換一個格式,因為etcd v3版本的介面都包含在grpc的定義中
Key = ByteString.CopyFromUtf8(_path)
}
};
//監聽Etcd節點變化,獲取變更資料,更新配置
_etcdClient.Watch(request, rsp =>
{
if (rsp.Events.Any())
{
var @event = rsp.Events[0];
//需要轉換一個格式,因為etcd v3版本的介面都包含在grpc的定義中
Data = ConvertData(@event.Kv.Value.ToStringUtf8());
//需要呼叫ConfigurationProvider的OnReload方法觸發ConfigurationReloadToken通知
//這樣才能對使用Configuration的類傳送資料變更通知
//比如IOptionsMonitor就是通過ConfigurationReloadToken通知變更資料的
OnReload();
}
});
}
}
使用方式如下
builder.AddEtcd("http://127.0.0.1:2379", "service/mydemo", true);
順便給大家推薦一個Etcd視覺化管理工具ETCD Manager,以便更好的學習Etcd。
到這裡,基本上就結束了,是不是非常簡單。主要還是Configuration本身的設計思路比較清晰,所以實現起來也不費勁。
總結
以上程式碼都已經上傳了我的GitHub,該倉庫還擴充套件了其他資料來源的讀取比如Consul、Properties檔案、Yaml檔案的讀取,實現思路也都大致相似,有興趣的同學可以自行查閱。由於主要是講解實現思路,可能許多細節並未做處理還望見諒。如果有疑問或者更好的建議,歡迎評論區交流指導。