配置,幾乎所有的應用程式都離不開它。.Net Framework時代我們使用App.config、Web.config,到了.Net Core的時代我們使用appsettings.json,這些我們再熟悉不過了。然而到了容器化、微服務的時代,這些本地文件配置有的時候就不太合適了。當你把本地部署的服務 ...
配置,幾乎所有的應用程式都離不開它。.Net Framework時代我們使用App.config、Web.config,到了.Net Core的時代我們使用appsettings.json,這些我們再熟悉不過了。然而到了容器化、微服務的時代,這些本地文件配置有的時候就不太合適了。當你把本地部署的服務搬到docker上後,你會發現要修改一個配置文件變的非常麻煩。你不得不通過宿主機進入容器內部來修改文件,也許容器內還不帶vi等編輯工具,你連看都不能看,改都不能。更別說當你啟動多個容器實例來做分散式應用的時候,一個個去修改容器的配置,這簡直要命了。
因為這些原因,所以“配置中心”就誕生了。配置中心是微服務的基礎設施,它對配置進行集中的管理並對外暴露介面,當應用程式需要的時候通過介面讀取。配置通常為Key/Value模式,然後通過http介面暴露。好了,配置中心不多說了,感覺要偏了,這次是介紹怎麼自定義一個配置源從配置中心讀取配置。廢話不多說直接上代碼吧。
模擬配置中心
我們新建一個asp.net core webapi站點來模擬配置中心服務,埠配置到5000,並添加相應的controller來模擬配置中心對外的介面。
[Route("api/[controller]")]
[ApiController]
public class ConfigsController : ControllerBase
{
public List<KeyValuePair<string,string>> Get()
{
var configs = new List<KeyValuePair<string, string>>();
configs.Add(new KeyValuePair<string, string>("SecretKey","1238918290381923"));
configs.Add(new KeyValuePair<string, string>("ConnectionString", "user=123;password=123;server=."));
return configs;
}
}
添加一個configscontroller,並修改Get方法,返回2個配置鍵值對。
訪問下/api/configs看下返回是否正確
自定義配置源
從現在開始我們真正開始來定義一個自定義的配置源然後當程式啟動的時候從配置中心讀取配置文件信息,並提供給後面的代碼使用配置。
新建一個asp.net core mvc站點來模擬客戶端程式。
MyConfigProvider
public class MyConfigProvider : ConfigurationProvider
{
/// <summary>
/// 嘗試從遠程配置中心讀取配置信息
/// </summary>
public async override void Load()
{
var response = "";
try
{
var serverAddress = "http://localhost:5000";
var client = new HttpClient();
client.BaseAddress = new Uri(serverAddress);
response = await client.GetStringAsync("/api/configs");
}
catch (Exception ex)
{
//write err log
}
if (string.IsNullOrEmpty(response))
{
throw new Exception("Can not request configs from remote config center .");
}
var configs = JsonConvert.DeserializeObject<List<KeyValuePair<string, string>>>(response);
Data = new ConcurrentDictionary<string, string>();
configs.ForEach(c =>
{
Data.Add(c);
});
}
}
新建一個MyConfigProvider的類,這個類從ConfigurationProvider繼承,並重寫其中的Load方法。使用HttpClient從配置中心讀取信息後,進行反序列化,並把配置轉換為字典。這裡註意一下,雖然Data的類型為IDictionary<string,string>,但是這裡實例化對象的時候使用了ConcurrentDictionary<string, string>類,因為Dictionary<string,string>是非線程安全的,如果進行多線程讀寫會出問題。
MyConfigSource
public class MyConfigSource : IConfigurationSource
{
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new MyConfigProvider();
}
}
新建一個MyConfigSource的類,這個類實現IConfigurationSource介面,IConfigurationSource介面只有一個Build方法,返回值為IConfigurationProvider,我們剛纔定義的MyConfigProvider因為繼承自ConfigurationProvider所以已經實現了IConfigurationProvider,我們直接new一個MyConfigProvider並返回。
MyConfigBuilderExt
public static class MyConfigBuilderExt
{
public static IConfigurationBuilder AddMyConfig(
this IConfigurationBuilder builder
)
{
return builder.Add(new MyConfigSource());
}
}
給IConfigurationBuilder定義一個AddMyConfig的擴展方法,跟.Net Core自帶的幾個配置源使用風格保持一致。當調用AddMyConfig的時候給IConfigurationBuilder實例添加一個MyConfigSource的源。
使用配置源
在Program中添加MyConfigSource
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, configBuiler) =>
{
configBuiler.AddMyConfig();
})
.UseStartup<Startup>();
}
在ConfigureAppConfiguration的匿名委托方法中調用AddMyConfig擴展方法,這樣程式啟動的時候會自動使用MyConfigSource源並從配置中心讀取配置到本地應用程式。
修改HomeController
public class HomeController : Controller
{
IConfiguration _configuration;
public HomeController(IConfiguration configuration)
{
_configuration = configuration;
}
public IActionResult Index()
{
var secretKey = _configuration["SecretKey"];
var connectionString = _configuration["ConnectionString"];
ViewBag.SecretKey = secretKey;
ViewBag.ConnectionString = connectionString;
return View();
}
}
修改homecontroller,把IConfiguration通過構造函數註入進去,在Index Action方法中讀取配置,並賦值給ViewBag
修改Index視圖
@{
ViewData["Title"] = "Test my config";
}
<h3>
SecretKey: @ViewBag.SecretKey
</h3>
<h3>
ConnectionString: @ViewBag.ConnectionString
</h3>
修改Index視圖的代碼,把配置信息從ViewBag中讀取出來併在網頁上展示。
運行一下
先運行配置中心站點再運行一下網站,首頁出現了我們在配置中心定義的SecretKey跟ConnectionString信息,表示我們的程式成功的從配置中心讀取了配置信息。我們的自定義配置源已經能夠成功運行了。
改進
以上配置源雖然能夠成功運行,但是仔細看的話顯然它有2個比較大的問題。
- 配置中心的服務地址是寫死在類里的。我們的配置中心很有可能會修改ip或者功能變數名稱,寫死在代碼里顯然不是高明之舉,所以我們還是需要保留本地配置文件,把配置中心的服務地址寫到本地配置文件中。
- 配置中心作為微服務的基礎設施一旦故障會引發非常嚴重的後果,新啟動或者重啟的客戶端會無法正常啟動。如果我們在配置中心正常的時候冗餘一份配置在本地,當配置中心故障的時候從本地讀取配置,至少可以保證一部分客戶端程式能夠正常運行。
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"myconfigServer": "http://localhost:5000"
}
修改本地appsettings.json文件,添加myconfigServer的配置信息。
public class MyConfigProvider : ConfigurationProvider
{
private string _serverAddress;
public MyConfigProvider()
{
var jsonConfig = new JsonConfigurationSource();
jsonConfig.FileProvider = new PhysicalFileProvider(Directory.GetCurrentDirectory());
jsonConfig.Path = "appsettings.json";
var jsonProvider = new JsonConfigurationProvider(jsonConfig);
jsonProvider.Load();
jsonProvider.TryGet("myconfigServer", out string serverAddress);
if (string.IsNullOrEmpty(serverAddress))
{
throw new Exception("Can not find myconfigServer's address from appsettings.json");
}
_serverAddress = serverAddress;
}
/// <summary>
/// 嘗試從遠程配置中心讀取配置信息,當成功從配置中心讀取信息的時候把配置寫到本地的myconfig.json文件中,當配置中心無法訪問的時候嘗試從本地文件恢復配置。
/// </summary>
public async override void Load()
{
var response = "";
try
{
var client = new HttpClient();
client.BaseAddress = new Uri(_serverAddress);
response = await client.GetStringAsync("/api/configs");
WriteToLocal(response);
}
catch (Exception ex)
{
//write err log
response = ReadFromLocal();
}
if (string.IsNullOrEmpty(response))
{
throw new Exception("Can not request configs from remote config center .");
}
var configs = JsonConvert.DeserializeObject<List<KeyValuePair<string, string>>>(response);
Data = new ConcurrentDictionary<string, string>();
configs.ForEach(c =>
{
Data.Add(c);
});
}
private void WriteToLocal(string resp)
{
var file = Directory.GetCurrentDirectory() + "/myconfig.json";
File.WriteAllText(file,resp);
}
private string ReadFromLocal()
{
var file = Directory.GetCurrentDirectory() + "/myconfig.json";
return File.ReadAllText(file);
}
}
修改MyConfigProvider,修改構造函數,通過JsonConfigurationProvider從本地讀取appsettings.json中的myconfigServer配置信息。新增WriteToLocal方法把配置中心返回的json數據寫到本地文件中。新增ReadFromLocal方法,從本地文件讀取json信息。
再次運行
先運行配置中心站點,再運行客戶端網站,可以看到配置信息展示到首頁界面上。關閉配置中心客跟客戶端網站,並且重啟客戶端網站依然能夠展示配置信息,說明自定義配置源當配置中心故障的時候成功從本地文件恢復了配置。圖跟上面的圖是一致的,就不貼了。
總結
通過以上我們定義了一個比較簡單的自定義配置源,它能夠通過http從配置中心讀取配置,並且提供了同傳統json配置文件一致的使用風格,最大程度的復用舊代碼,減少因為引入配置中心而大規模改動代碼。我們從上面的代碼可以更清楚的知道.Net Core的配置源是如何工作的。ConfigurationSource只是ConfigurationProvider的建造器。真正完成配置載入、查找工作的是ConfigurationProvider。
以上代碼還是演示級別的代碼,還有很多改進的空間,比如http訪問失敗的重試,我們可以使用polly重構;比如支持定時從配置中心刷新配置等,有興趣可以自己去實踐一下。