前言
Iteration 11[1] 从 4/9 开始到 4/22 完结,为期两周。
这个周期十分高兴,我造了一堆轮子来解决 Databend 的命令行应用体验问题:
- serde-bridge[2]:将一个值在不同的 serde 实现中进行转换
- serde-env[3]:反对将环境变量解析为嵌套的构造体
- serfig[4]:基于 serde 实现的多层配置零碎,反对从环境变量,配置文件,本身等多个中央读取并合并配置
最终实现的成果是 Databend 可能依照指定的程序顺次加载来自配置文件,环境变量和命令行参数中的配置:
pub fn load() -> Result<Self> { let arg_conf: Self = Config::parse(); let mut builder: serfig::Builder<Self> = serfig::Builder::default(); // Load from config file first. { let config_file = if !arg_conf.config_file.is_empty() { arg_conf.config_file.clone() } else if let Ok(path) = env::var("CONFIG_FILE") { path } else { "".to_string() }; builder = builder.collect(from_file(Toml, &config_file)); } // Then, load from env. builder = builder.collect(from_env()); // Finally, load from args. builder = builder.collect(from_self(arg_conf)); Ok(builder.build()?)}
背景
通过命令行参数配置:Databend 经验晚期的横蛮成长之后,当初终于有工夫能够略微打磨一下应用体验。首当其冲是简约而不成体系的配置项,以配置 S3 存储的 Bucket 为例:
通过命令行参数配置:
--bucket=abc
通过环境变量配置:
export S3_STORAGE_BUCKET=abc
通过配置文件配置:
[storage.s3]bucket = "abc"
呈现这种情况的一大起因是 clap 的不良设计导致用户应用中呈现的畸形姿态:
clap 的 Parser 不反对构造体,所有 args 都必须平铺,导致用户必须为所有的构造体加上 #[clap(flatten)]:
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Args)]#[serde(default)]pub struct StorageConfig { // azure storage blob config. #[clap(flatten)] pub azure_storage_blob: AzureStorageBlobConfig,}
更蹩脚的是,clap 依赖字段名来惟一辨别参数,这就要求整个构造体中不得呈现重名的字段。比方下列这样的代码能编译,然而无奈失常运行:
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Args)]#[serde(default)]pub struct StorageConfig { // S3 storage backend config. #[clap(flatten)] pub s3: S3StorageConfig, // azure storage blob config. #[clap(flatten)] pub azure_storage_blob: AzureStorageBlobConfig,}#[derive(Clone, PartialEq, Serialize, Deserialize, Args)]#[serde(default)]pub struct S3StorageConfig { /// <root> #[clap(long, default_value_t)] pub root: String,}#[derive(Clone, PartialEq, Serialize, Deserialize, Args)]#[serde(default)]pub struct AzureStorageBlobConfig { /// <root> #[clap(long, default_value_t)] pub root: String,}
所以大家开始写这样的代码:
#[derive(Clone, PartialEq, Serialize, Deserialize, Args)]pub struct MetaConfig { /// The dir to store persisted meta state for a embedded meta store #[clap(long, env = META_EMBEDDED_DIR, default_value = "./_meta_embedded")] pub meta_embedded_dir: String, #[clap(long, env = META_ADDRESS, default_value = "", help = "MetaStore backend address")] pub meta_address: String, #[clap(long, env = META_USERNAME, default_value = "", help = "MetaStore backend user name")] pub meta_username: String, #[clap(long, env = META_PASSWORD, default_value = "", help = "MetaStore backend user password")] pub meta_password: String,}
Args 与 Env 的关系曾经十分凌乱了,databend 还须要反对从配置文件中加载。为了保障正确的加载程序,社区甚至开始写宏来强行再次加载环境变量:
macro_rules! env_helper { ($config:expr, $struct: tt, $field:tt, $field_type: ty, $env:expr) => { let env_var = std::env::var_os($env) .unwrap_or($config.$struct.$field.to_string().into()) .into_string() .expect(format!("cannot convert {} to string", $env).as_str()); $config.$struct.$field = env_var .parse::<$field_type>() .expect(format!("cannot convert {} to {}", $env, stringify!($field_type)).as_str()); };}impl StorageConfig { pub fn load_from_env(mut_config: &mut Config) { env_helper!(mut_config, storage, storage_type, String, STORAGE_TYPE); env_helper!(mut_config, storage, storage_num_cpus, u64, STORAGE_NUM_CPUS); // DISK. env_helper!( mut_config.storage, fs, data_path, String, FS_STORAGE_DATA_PATH ); ... }}
思考
在入手改良之前,首先思考最现实的情况是怎么的:
• 正确的加载程序:同名字段会依照 config -> env -> args 的程序记录,后者笼罩前者
• 对立的命名体系:同一个字段在不同中央应用对立的命名格调,比如说 storage.s3.bucket, --storage-s3-bucket, STORAGE_S3_BUCKET
• 缩小冗余代码:尽可能减少维护者须要写的反复代码
社区在 Issue bug: config overwrite when specify --config and any other command line args.[5] 中奉献了一个 idea:将 config-rs[6] 与 clap 联合起来,让 clap 可能作为 config-rs 的一个 Source。我为 config-rs 提交了 proposal: Implement serde::Serializer and Source/AsyncSource for Value[7],然而在尝试实现 demo 的时候遇到了无奈解决的问题,以至于我开始感觉咱们须要新的办法和新的思路。
好,跳进去思考这个问题:
配置加载实际上就是依照程序从不同的中央加载数据,解析成咱们的 Config 构造体并进行合并的过程。所以咱们须要:
- 将环境变量解析为嵌套的构造体
- 一个对立的数据表示形式
- 将来自不同的中央的数据进行合并
实现
serde-env
最开始我尝试应用了 envy[8],然而它不反对将环境变量解析为嵌套的构造体,为此我开发了 serde-env[9]:
use serde::Deserialize;use serde_env::from_env;#[derive(Debug, Deserialize)]struct Cargo { home: String,}#[derive(Debug, Deserialize)]struct Test { home: String, cargo: Cargo,}fn main() { let t: Test = from_env().expect("deserialize from env"); assert!(!t.home.is_empty()); assert!(!t.cargo.home.is_empty()); println!("{:?}", t)}
思路其实很简略,serde-env 外部将环境变量示意为应用 _ 分隔的 tree,于是上述例子中的 Test.cargo.home 实际上就能转化为 CARGO_HOME。
连续这样的思路,serde-env 还可能反对形如这样的构造体:
#[derive(Debug, Deserialize)]struct Cargo { test: String,}#[derive(Debug, Deserialize)]struct Test { home: String, cargo: Cargo, cargo_home: String,}
无效解决了环境变量转化为构造体的问题。
serde-bridge
为了可能解决配置之间的合并,我开发了 serde-bridge[10]:
use anyhow::Result;use serde_bridge::{from_value, into_value, FromValue, IntoValue, Value};fn main() -> Result<()> { let v = bool::from_value(Value::Bool(true))?; assert!(v); let v: bool = from_value(Value::Bool(true))?; assert!(v); let v = true.into_value()?; assert_eq!(v, Value::Bool(true)); let v = into_value(true)?; assert_eq!(v, Value::Bool(true)); Ok(())}
它是一个到 serde API one-to-one 的 mapping,跟 serde-value[11] 类似,然而更加残缺,同时实现了 {De,S}erialize[r] 等类型。任何 serde 实现都能够基于 serde_bridge::Value 作为中间层来进行转换。
serfig
在上述库的反对下,serfig 通过 serde_bridge::Value 来合并配置并对外裸露 Builder 的接口:
use serde::{Deserialize, Serialize};use serfig::collectors::{from_env, from_file, from_self};use serfig::parsers::Toml;use serfig::Builder;#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]#[serde(default)]struct TestConfig { a: String, b: String, c: i64,}fn main() -> anyhow::Result<()> { let builder = Builder::default() .collect(from_env()) .collect(from_file(Toml, "config.toml")) .collect(from_self(TestConfig::default())); let t: TestConfig = builder.build()?; println!("{:?}", t); Ok(())}
跟 clap 的整合也非常容易,弱小的 serde_bridge::Value 使得咱们可能将构造体自身也作为一个数据源 from_self,以 Databend 为例:
pub fn load() -> Result<Self> { let arg_conf: Self = Config::parse(); let mut builder: serfig::Builder<Self> = serfig::Builder::default(); // Load from config file first. { let config_file = if !arg_conf.config_file.is_empty() { arg_conf.config_file.clone() } else if let Ok(path) = env::var("CONFIG_FILE") { path } else { "".to_string() }; builder = builder.collect(from_file(Toml, &config_file)); } // Then, load from env. builder = builder.collect(from_env()); // Finally, load from args. builder = builder.collect(from_self(arg_conf)); Ok(builder.build()?)}
咱们首先应用 Config::parse() 来加载命令参数,而后在最初应用 from_self(arg_conf) 来笼罩后面获取到的数据。
后续
目前的实现还有不少问题,咱们仍未解决 #[clap(flatten)] 导致的各种问题:
pub struct AzblobStorageConfig { /// Endpoint URL for Azblob /// /// # TODO(xuanwo) /// /// Clap doesn't allow us to use endpoint_url directly. #[clap(long = "storage-azblob-endpoint-url", default_value_t)] #[serde(rename = "endpoint_url")] pub azblob_endpoint_url: String, /// # TODO(xuanwo) /// /// Clap doesn't allow us to use root directly. #[clap(long = "storage-azblob-root", default_value_t)] #[serde(rename = "root")] pub azblob_root: String,}
• 雷同的字段还是会抵触
• 须要手动指定 clap 的 long 字段
将来可能会想方法自行实现 clap Parser 来彻底解决这些不统一的问题。
总结
高兴的造轮子周期,以至于这周始终在发 #明天用 而不是 #明天学,下个周期还是要多输出一些货色~
援用链接
[1]
Iteration 11: https://github.com/users/Xuan...[2]
serde-bridge: https://github.com/Xuanwo/ser...[3]
serde-env: https://github.com/Xuanwo/ser...[4]
serfig: https://github.com/Xuanwo/serfig[5]
bug: config overwrite when specify --config and any other command line args.: https://github.com/datafusela...[6]
config-rs: https://github.com/mehcode/co...[7]
proposal: Implement serde::Serializer and Source/AsyncSource for Value: https://github.com/mehcode/co...[8]
envy: https://github.com/softprops/...[9]
serde-env: https://github.com/Xuanwo/ser...[10]
serde-bridge: https://github.com/Xuanwo/ser...[11]
serde-value: https://github.com/arcnmx/ser...
对于 Databend
Databend 是一款开源、弹性、低成本,基于对象存储也能够做实时剖析的旧式数仓。期待您的关注,一起摸索云原生数仓解决方案,打造新一代开源 Data Cloud。
- Databend 文档:https://databend.rs/
- Twitter:https://twitter.com/Datafuse_...
- Slack:https://datafusecloud.slack.com/
- Wechat:Databend
- GitHub :https://github.com/datafusela...
文章首发于公众号:Databend