前置知识:etcd及配置中心

737
2023/07/03 16:12:20

本章我们将学习分布式KV存储:etcd的基础知识、单节点部署、集群部署、rust集成以及基于etcd实现的配置中心:写入配置、读取配置、配置的热加载。

etcd的安装

etcd 安装非常简单,只需要下载对应平台的预编译二进制程序即可。本章以 debian 11 x64 为例进行演示。

# 下载
wget https://github.com/etcd-io/etcd/releases/download/v3.4.26/etcd-v3.4.26-linux-amd64.tar.gz

# 解压
tar zxvf etcd-v3.4.26-linux-amd64.tar.gz -C /usr/local

# 加入环境变量 
export PATH=$PATH:/usr/local/etcd-v3.4.26-linux-amd64

# 测试是否安装成功
etcd -version # etcd 版本

为了便于操作,你可以将 export PATH=$PATH:/usr/local/etcd-v3.4.26-linux-amd64 加入到 ~/.bashrc

etcd 单机部署

单节点部署

单机单节点部署非常简单,你需要运行 etcd 命令即可:

# 开启单节点
etcd

# 添加/修改:key 为 `name`,值为 `axum.rs`
etcdctl put name 'axum.rs'

# 获取
etcdctl get name

多节点部署

# 节点1
etcd --name axumrs-1 --data-dir /var/etcd/axumrs \
--listen-client-urls http://127.0.0.1:2379  \
--advertise-client-urls http://127.0.0.1:2379 \
--listen-peer-urls http://127.0.0.1:2380 \
--initial-advertise-peer-urls http://127.0.0.1:2380 \
--initial-cluster-token axumrs-etcd-1 \
--initial-cluster 'axumrs-1=http://127.0.0.1:2380,axumrs-2=http://127.0.0.1:12380,axumrs-3=http://127.0.0.1:22380' \
--initial-cluster-state new

# 节点2
etcd --name axumrs-2 --data-dir /var/etcd/axumrs-2 \
--listen-client-urls http://127.0.0.1:12379  \
--advertise-client-urls http://127.0.0.1:12379 \
--listen-peer-urls http://127.0.0.1:12380 \
--initial-advertise-peer-urls http://127.0.0.1:12380 \
--initial-cluster-token axumrs-etcd-1 \
--initial-cluster 'axumrs-1=http://127.0.0.1:2380,axumrs-2=http://127.0.0.1:12380,axumrs-3=http://127.0.0.1:22380' \
--initial-cluster-state new

# 节点3
etcd --name axumrs-3 --data-dir /var/etcd/axumrs-3 \
--listen-client-urls http://127.0.0.1:22379  \
--advertise-client-urls http://127.0.0.1:22379 \
--listen-peer-urls http://127.0.0.1:22380 \
--initial-advertise-peer-urls http://127.0.0.1:22380 \
--initial-cluster-token axumrs-etcd-1 \
--initial-cluster 'axumrs-1=http://127.0.0.1:2380,axumrs-2=http://127.0.0.1:12380,axumrs-3=http://127.0.0.1:22380' \
--initial-cluster-state new

# 查看集群节点列表
etcdctl member list

# 结果
# 228fca496e60fa12, started, axumrs-1, http://127.0.0.1:2380, http://127.0.0.1:2379, false
# 3bc60b0a9a2095bc, started, axumrs-3, http://127.0.0.1:22380, http://127.0.0.1:22379, false
# f92431e540021d8c, started, axumrs-2, http://127.0.0.1:12380, http://127.0.0.1:12379, false

etcd 关键参数说明

参数说明
--name节点名
--data-dir节点数据存储目录
--listen-client-urls监听客户端请求的地址列表,多个地址用逗号分隔
--advertise-client-urls如果 --listen-client-urls 配置了多个地址,这个选项可以给出建议客户端使用的访问地址
--listen-peer-urls 服务端(集群)节点之间通讯的监听地址
--initial-advertise-peer-urls建议服务端(集群)节点之间通讯使用的地址
--initial-cluster-token初始化时,集群使用的 token
--initial-cluster初始化时,集群中的节点及地址列表:节点1=节点地址1[,节点2=节点地址2]...
--initial-cluster-state可选值:newexistingnew 表示新建集群;existing 表示加入已存在集群

etcd集群部署

我们将使用 3 台 Woiden 的 EU-2 数据中心的免费 VPS 进行演示:

节点IP
axumrs-12a01:*:3159:0001
axumrs-22a01:*:3206:0001
axumrs-32a01:*:31be:0001

由于实验环境是 ipv6,如果你使用的是 ipv4 不用紧张,把 http://[IPv6地址] 换成 http://ipv4地址 即可。

静态部署

静态部署和单机多节点部署非常类似,改成机器的公网IP即可:

# 节点1
etcd --name axumrs-1 --data-dir /var/etcd/axumrs \
--listen-client-urls http://[2a01:*:3159:0001]:2379,http://127.0.0.1:2379  \
--advertise-client-urls http://[2a01:*:3159:0001]:2379 \
--listen-peer-urls http://[2a01:*:3159:0001]:2380 \
--initial-advertise-peer-urls http://[2a01:*:3159:0001]:2380 \
--initial-cluster-token axumrs-etcd-1 \
--initial-cluster 'axumrs-1=http://[2a01:*:3159:0001]:2380,axumrs-2=http://[2a01:*:3206:0001]:2380,axumrs-3=http://[2a01:*:31be:0001]:2380' \
--initial-cluster-state new

# 节点2
etcd --name axumrs-2 --data-dir /var/etcd/axumrs \
--listen-client-urls http://[2a01:*:3206:0001]:2379,http://127.0.0.1:2379  \
--advertise-client-urls http://[2a01:*:3206:0001]:2379 \
--listen-peer-urls http://[2a01:*:3206:0001]:2380 \
--initial-advertise-peer-urls http://[2a01:*:3206:0001]:2380 \
--initial-cluster-token axumrs-etcd-1 \
--initial-cluster 'axumrs-1=http://[2a01:*:3159:0001]:2380,axumrs-2=http://[2a01:*:3206:0001]:2380,axumrs-3=http://[2a01:*:31be:0001]:2380' \
--initial-cluster-state new

# 节点3
etcd --name axumrs-3 --data-dir /var/etcd/axumrs \
--listen-client-urls http://[2a01:*:31be:0001]:2379,http://127.0.0.1:2379  \
--advertise-client-urls http://[2a01:*:31be:0001]:2379 \
--listen-peer-urls http://[2a01:*:31be:0001]:2380 \
--initial-advertise-peer-urls http://[2a01:*:31be:0001]:2380 \
--initial-cluster-token axumrs-etcd-1 \
--initial-cluster 'axumrs-1=http://[2a01:*:3159:0001]:2380,axumrs-2=http://[2a01:*:3206:0001]:2380,axumrs-3=http://[2a01:*:31be:0001]:2380' \
--initial-cluster-state new

# 集群节点列表
etcdctl member list
# 5789559cc124f2e1, started, axumrs-3, http://[2a01:*:31be:0001]:2380, http://[2a01:*:31be:0001]:2379, false
# d9c662f0b70e1866, started, axumrs-1, http://[2a01:*:3159:0001]:2380, http://[2a01:*:3159:0001]:2379, false
# f03d1d47f9d679de, started, axumrs-2, http://[2a01:*:3206:0001]:2380, http://[2a01:*:3206:0001]:2379, false

自动发现部署

# 获取专属的服务发现地址
# `size` 是预期的节点数
curl 'https://discovery.etcd.io/new?size=3'
# 结果:(请保存好,后面要用到)
# https://discovery.etcd.io/40c056239fc114368f2bd74f2660c809

# 节点1
etcd --name axumrs-1 --data-dir /var/etcd/axumrs \
--listen-client-urls http://[2a01:*:3159:0001]:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://[2a01:*:3159:0001]:2379 \
--listen-peer-urls http://[2a01:*:3159:0001]:2380 \
--initial-advertise-peer-urls http://[2a01:*:3159:0001]:2380 \
--discovery https://discovery.etcd.io/40c056239fc114368f2bd74f2660c809

# 节点2
etcd --name axumrs-2 --data-dir /var/etcd/axumrs \
--listen-client-urls http://[2a01:*:3206:0001]:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://[2a01:*:3206:0001]:2379 \
--listen-peer-urls http://[2a01:*:3206:0001]:2380 \
--initial-advertise-peer-urls http://[2a01:*:3206:0001]:2380 \
--discovery https://discovery.etcd.io/40c056239fc114368f2bd74f2660c809

# 节点3
etcd --name axumrs-3 --data-dir /var/etcd/axumrs \
--listen-client-urls http://[2a01:*:31be:0001]:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://[2a01:*:31be:0001]:2379 \
--listen-peer-urls http://[2a01:*:31be:0001]:2380 \
--initial-advertise-peer-urls http://[2a01:*:31be:0001]:2380 \
--discovery https://discovery.etcd.io/40c056239fc114368f2bd74f2660c809

# 集群节点列表
etcdctl member list
# 5789559cc124f2e1, started, axumrs-3, http://[2a01:*:31be:0001]:2380, http://[2a01:*:31be:0001]:2379, false
# d9c662f0b70e1866, started, axumrs-1, http://[2a01:*:3159:0001]:2380, http://[2a01:*:3159:0001]:2379, false
# f03d1d47f9d679de, started, axumrs-2, http://[2a01:*:3206:0001]:2380, http://[2a01:*:3206:0001]:2379, false

与Rust集成

我们使用 etcd-rs 来集成 etcd。我们模拟使用 etcd 管理配置:写入配置、载入配置以及配置的热加载

依赖

[dependencies]
tokio={version="1", features=["full"]}
etcd-rs = "1.0"
serde = { version="1", features = ["derive"] }
serde_json = "1"

配置

etcd 操作

连接到etcd

let cli = Client::connect(ClientConfig::new(["http://localhost:2379".into()]))
        .await
        .unwrap();

将配置写入 etcd

async fn save_cfg(cli: &Client) {
    let cfg = config::Config {
        machine_id: 1,
        node_id: 1,
        addr: String::from("127.0.0.1:9527"),
    };
    let cfg_str = serde_json::to_string(&cfg).unwrap();
    cli.put((KEY, cfg_str)).await.unwrap();
}
  • 创建一个 Config 配置实例
  • 将其序列化为 JSON 字符串
  • 将 JSON 字符串写入 etcd 中

从 etcd 加载配置

async fn load_cfg(cli: &Client) {
    let res = cli.get(KEY).await.unwrap();
    let kvs = res.kvs;
    if kvs.is_empty() {
        println!("没有数据");
        return;
    }
    let cfg_str = kvs[0].value_str();
    let cfg: config::Config = serde_json::from_str(cfg_str).unwrap();
    println!("{:?}", cfg);
}
  • 从 etcd 中读取配置的 JSON 字符串
  • 将其反序列化为 Config 配置实例

配置的热加载

async fn hot_load(cli: &Client) {
    let (mut stream, cancel) = cli.watch(KeyRange::prefix(KEY)).await.unwrap();

    tokio::spawn(async move {
        tokio::time::sleep(tokio::time::Duration::from_secs(30)).await;
        cancel.cancel().await.unwrap();
    });

    loop {
        match stream.inbound().await {
            etcd_rs::WatchInbound::Ready(resp) => {
                let kv = &resp.events[0].kv;
                let cfg_str = kv.value_str();
                let cfg: config::Config = serde_json::from_str(cfg_str).unwrap();
                println!("配置发生变化:{:?}", cfg);
            }
            etcd_rs::WatchInbound::Interrupted(e) => {
                println!("发生错误:{:?}", e);
            }
            etcd_rs::WatchInbound::Closed => {
                println!("监视流已关闭");
                break;
            }
        }
    }
}
  • 热加载是通过 etcd 的 watch 来实现的
    • 注意,为了简化演示,本例假设监视的所有改变都是 PUT 操作
    • 实际上,PUTDELETE 操作都能监视到,实际开发中应该区分事件的类型
  • 为了演示取消监视功能,我们特意加了一个 tokio::spawn,让它休眠 30 秒后,取消监视