写点什么

莫想到有一天得重新写个 etcd client

作者:八苦-瞿昙
  • 2025-05-10
    中国台湾
  • 本文字数:3682 字

    阅读完需:约 12 分钟

莫想到有一天得重新写个 etcd client

其实 8 年前搞过一个,


不过经过 8 年时间,etcd 多了很多功能 ,原来的多半不行了


虽然暂时我也没啥需求,但是怕 kv 和 watch 有变化


而且其实通过 grpc api 访问 etcd 没啥技术难度,搞 client 也没啥意思的 (只要有.proto文件,可以直接生成 grpc client 代码, 参见微软文档


现成的也很多


所以本来不想写, 直接从 nuget 上找了个下载量最高的 https://www.nuget.org/packages/dotnet-etcd


最开始使用也 ok,能读能写


但是当我使用watch时, 居然没有任何变更触发


当然也有其他人发现这个 bug 对应 issue: https://github.com/shubhamranjan/dotnet-etcd/issues/238(不过我遇见问题时还没人提 issue,我看了源码,找到问题原因之后,这位同志已经写了 issue 了,)


如下是 issue 内容


using dotnet_etcd;using Etcdserverpb;
EtcdClient etcdClient = new EtcdClient("http://localhost:2379");etcdClient.Watch( "test", (WatchResponse response) => { if (!response.Events.Any()) return; var value = response.Events[0].Kv.Value.ToStringUtf8(); Console.WriteLine(value); });
复制代码


If you run this sample with 8.0.0, the watch callback never triggers if you modify the key test. If you run it with 6.0.1 it triggers correctly.


为什么 Watch 没用呢?


不是 etcd 版本问题, 不是网络访问有限制,不是你们第一直觉想到的问题


而是 作者没实现


https://github.com/shubhamranjan/dotnet-etcd/commit/4ba94d0a174fec42d190bffda733ded3a24055e0 这次代码提交中


watch 的 核心处理代码被删了, 但是 package 依然水灵灵提交到了 nuget 上, 所以 当然不可能监听变更啦 囧 (issue 上我也加了描述,以免其他人遇见迷茫)


然后查看其原来实现, 还发现有个问题: 根本没有处理异常重连


更别提 etcd mvcc 避免变更丢失时要特别注意的 revsion


然后作者记录显示,已经近一个月没有更新


所以也不可能指望作者修正问题了


算了,还是自己来吧,


没想到 8 年之后还得再次生成 etcd 的 grpc client


根据 最新的 etcd .proto文件 生成了最新的 grpc client


然后为 DependencyInjection 以及使用 简单封装了一下, 源码放在了 https://github.com/fs7744/etcdcsharp


如下是简单的使用文档

ETCD V3 Client

base code all Generate by grpc tools.

Quick start

Install package

  • Package Manager


Install-Package etcd.v3 -Version 0.0.2Install-Package etcd.v3.Configuration -Version 0.0.2
复制代码


  • .NET CLI


dotnet add package etcd.v3 --version 0.0.2dotnet add package etcd.v3.Configuration --version 0.0.2
复制代码

new client

new client with DI

 ServiceCollection services = new(); services.UseEtcdClient(); services.AddEtcdClient("test", new EtcdClientOptions() { Address = ["http://xxx:2379"] }); var p = services.BuildServiceProvider();
var client = p.GetRequiredKeyedService<IEtcdClient>("test");
// you also can create client by factory var factory = p.GetRequiredService<IEtcdClientFactory>();var client2 = factory.CreateClient(new EtcdClientOptions() { Address = ["http://xxx:2379"] });

// get all configforeach (var i in await client.GetRangeValueUtf8Async("/ReverseProxy/")){ Console.WriteLine($"{i.Key} : {i.Value}");}
// OR get client in ctorpublic class Testt{ private readonly IEtcdClient client;
public Testt([FromKeyedServices("test")] IEtcdClient client) { this.client = client; }}
复制代码

new client without DI

 var factory = EtcdClientFactory.Create();var client = factory.CreateClient(new EtcdClientOptions() { Address = ["http://xxx:2379"] });
// get all configforeach (var i in await client.GetRangeValueUtf8Async("/ReverseProxy/")){ Console.WriteLine($"{i.Key} : {i.Value}");}
复制代码

use with Configuration

var b = new ConfigurationBuilder();b.UseEtcd(new Etcd.Configuration.EtcdConfigurationOptions(){    Prefix = "/ReverseProxy/",    RemovePrefix = true,    EtcdClientOptions = new EtcdClientOptions() { Address = ["http://xxx:2379"] }});var c = b.Build();
// test watch changeTest(c);
private static void Test(IConfigurationRoot c){ foreach (var i in c.GetChildren()) { Console.WriteLine($"{i.Key} : {i.Value}"); } c.GetReloadToken().RegisterChangeCallback(i => { Test(i as IConfigurationRoot); }, c);}
复制代码

Address

Address just parse by GrpcChannel.ForAddress, so support


  • http://xxx:port

  • https://xxx:port

  • dns://xxx:port

KV

get one by key

string v = await client.GetValueUtf8Async("/ReverseProxy/");//or string v = (await client.GetAsync("/ReverseProxy/")).Kvs?.First().Value.ToStrUtf8();//orstring v = (await client.RangeAsync(new RangeRequest() { Key = ByteString.CopyFromUtf8("/ReverseProxy/") })).Kvs?.First().Value.ToStrUtf8();
复制代码
get all IDictionary<string, string>
foreach (var i in await client.GetRangeValueUtf8Async("/ReverseProxy/")){    Console.WriteLine($"{i.Key} : {i.Value}");}//orforeach (var i in (await client.GetRangeAsync("/ReverseProxy/")).Kvs){    Console.WriteLine($"{i.Key.ToStrUtf8()} : {i.Value.ToStrUtf8()}");}//orforeach (var i in (await client.RangeAsync(new RangeRequest() { Key = ByteString.CopyFromUtf8("/ReverseProxy/"), RangeEnd = ByteString.CopyFromUtf8("/ReverseProxy/".GetRangeEnd()) })).Kvs){    Console.WriteLine($"{i.Key.ToStrUtf8()} : {i.Value.ToStrUtf8()}");}
复制代码

Put

await client.PutAsync("/ReverseProxy/test", "1");//orawait client.PutAsync(new PutRequest() { Key = ByteString.CopyFromUtf8("/ReverseProxy/test"), Value = ByteString.CopyFromUtf8("1") });
复制代码

Delete one

await client.DeleteAsync("/ReverseProxy/test");//orawait client.DeleteRangeAsync(new DeleteRangeRequest() { Key = ByteString.CopyFromUtf8("/ReverseProxy/test") });
复制代码

Delete all

await client.DeleteRangeAsync("/ReverseProxy/test");//orawait client.DeleteRangeAsync(new DeleteRangeRequest() { Key = ByteString.CopyFromUtf8("/ReverseProxy/test"), RangeEnd = ByteString.CopyFromUtf8("/ReverseProxy/test".GetRangeEnd())) });
复制代码

Watch

 await client.WatchRangeBackendAsync("/ReverseProxy/", i => {     if (i.Events.Count > 0)     {         foreach (var item in i.Events)         {             Console.WriteLine($"{item.Type} {item.Kv.Key.ToStrUtf8()}");         }     }     return Task.CompletedTask; }, startRevision: 6, reWatchWhenException: true);
// orawait Task.Factory.StartNew(async () =>{ long startRevision = 6; while (true) { try { using var watcher = await client.WatchRangeAsync("/ReverseProxy/", startRevision: startRevision); await watcher.ForAllAsync(i => { startRevision = i.FindRevision(startRevision); foreach (var item in i.Events) { Console.WriteLine($"{item.Type} {item.Kv.Key.ToStrUtf8()}"); } return Task.CompletedTask; }); } catch (Exception ex) { Console.WriteLine($"Exception: {ex.Message}"); } }});
复制代码

all grpc client

if IEtcdClient Missing some grpc method , you can just use grpc client to do


public partial interface IEtcdClient{    public AuthClient AuthClient { get; }    public Cluster.ClusterClient ClusterClient { get; }    public ElectionClient ElectionClient { get; }    public KV.KVClient KVClient { get; }    public LeaseClient LeaseClient { get; }    public LockClient LockClient { get; }    public MaintenanceClient MaintenanceClient { get; }    public Watch.WatchClient WatchClient { get; }}
复制代码

api doc

Main api doc please see


https://fs7744.github.io/etcdcsharp/api/Etcd.htmlhttps://fs7744.github.io/etcdcsharp/api/Microsoft.Extensions.Configuration.EtcdConfigurationExtensions.html


All api doc ( include code generate by grpc tool ) please see


https://fs7744.github.io/etcdcsharp/api/index.html

发布于: 刚刚阅读数: 3
用户头像

八苦-瞿昙

关注

一个假和尚,不懂人情世故。 2018-11-23 加入

会点点技术,能写些代码,只爱静静。 g hub: https://github.com/fs7744 黑历史:https://www.cnblogs.com/fs7744

评论

发布
暂无评论
莫想到有一天得重新写个 etcd client_八苦-瞿昙_InfoQ写作社区