搭建etcd集群机制静态etcd动态发现DNS发现常见参数 listenpeerurls 用于监听伙伴通讯的URL列表。这个标记告诉etcd在特定的scheme:IP:port组合上从它的伙伴接收进来的请求,http或者https 环境变量:ETCDLISTENPEERURLS listenclienturls 用于监听客户端通讯的URL列表。这个标记告诉etcd在特定的scheme:IP:port组合上从客户端接收进来的请求。scheme可是http或者https。 环境变量:ETCDLISTENCLIENTURLS initialadvertisepeerurls 列出这个成员的伙伴URL以便通告给集群的其他成员。这些地方用于在集群中通讯etcd数据。至少有一个必须对所有集群成员可以路由的。这些URL可以包含域名。 环境变量:ETCDINITIALADVERTISEPEERURLS initialcluster 为启动初始化集群配置。 环境变量:ETCDINITIALCLUSTER initialclusterstate 初始化集群状态(neworexisting)。在初始化静态(initialstatic)或者DNS启动(DNSbootstrapping)期间为所有成员设置为new。如果这个选项被设置为existing,etcd将试图加入已有的集群。如果设置为错误的值,etcd将尝试启动但安全失败。 环境变量:ETCDINITIALCLUSTERSTATE advertiseclienturls 列出这个成员的客户端URL,通告给集群中的其他成员。这些URL可以包含域名。 环境变量:ETCDADVERTISECLIENTURLS 图形化配置http:play。etcd。ioinstall 在kubernetes里部署https:bitnami。comstacketcdhelm 1。codehttps:gitee。comzhouguanyulovejiadanyangmygitblobk8ssts。yamlhttps:github。comkuberneteskubernetestreemasterteste2etestingmanifestsstatefulsetetcd headlesssvc,像DNSRRClusterIP:None kubectlnstg1getendpointsapiVersion:v1kind:Servicemetadata:name:etcdlabels:app:etcdspec:ports:port:2380name:etcdserverport:2379name:etcdclientclusterIP:Noneselector:app:etcdpublishNotReadyAddresses:true client怎么访问:kubectlportforwardsvcetcdclusterclient2379:2379 apiVersion:v1kind:Servicemetadata:labels:app:etcdname:etcdclusterclientspec:ports:name:etcdcluster2379port:2379protocol:TCPtargetPort:2379selector:app:etcdsessionAffinity:Nonetype:ClusterIP 2。配置文件PDBservicests 3。apply 官方的code有两个问题etcd版本老配置不对:2022012914:47:51。344590Eetcdmain:errorverifyingflags,expectedIPinURLforbinding(http:etcd0。etcd:2380)。Seeetcdhelp,暂时不要用3。4,3。5的image 本地访问kubectlportforwardsvcetcdclusterclient2379:2379 扩容etcdkubectlscalereplicas5stsetcdstatefulset。appsetcdscaledetcdkubectlgetpodNAMEREADYSTATUSRESTARTSAGEetcd011Running012metcd111Running012metcd211Running012metcd301Pending02s 利用反亲和性分布etcdpod到不同节点https:kubernetes。iozhdocsconceptsschedulingevictionassignpodnode常用命令集群状态exportETCDCTLAPI3etcdctlendpointsetcd0。etcd:2379,etcd1。etcd:2379,etcd2。etcd:2379endpointstatuswriteouttableENDPOINTIDVERSIONDBSIZEISLEADERRAFTTERMRAFTINDEXetcd0。etcd:2379c799a6ef06bc8c143。3。820kBfalse69etcd1。etcd:23799869f0647883a00d3。3。820kBtrue69etcd2。etcd:237942c8b94265b9b79a3。3。820kBfalse69查看memberbash3。2etcdctlmemberlist42c8b94265b9b79a,started,etcd2,http:etcd2。etcd。default。svc。cluster。local:2380,http:etcd2。etcd。default。svc。cluster。local:2379,false58996ec4dced38b4,started,etcd4,http:etcd4。etcd。default。svc。cluster。local:2380,http:etcd4。etcd。default。svc。cluster。local:2379,false9869f0647883a00d,started,etcd1,http:etcd1。etcd。default。svc。cluster。local:2380,http:etcd1。etcd。default。svc。cluster。local:2379,falseb4476a6cf0b4d72c,started,etcd3,http:etcd3。etcd。default。svc。cluster。local:2380,http:etcd3。etcd。default。svc。cluster。local:2379,falsec799a6ef06bc8c14,started,etcd0,http:etcd0。etcd。default。svc。cluster。local:2380,http:etcd0。etcd。default。svc。cluster。local:2379,false健康检查bash3。2etcdctlendpointhealth127。0。0。1:2379ishealthy:successfullycommittedproposal:took1。051829439s增删改查bash3。2etcdctlputtestkeyfmengOKbash3。2etcdctlgettestkeytestkeyfmengbash3。2etcdctlputtestkeyfmengisdashuaibOKbash3。2etcdctlgettestkeytestkeyfmengisdashuaib etcdctlgetprefixwatchbash3。2etcdctlwatchtestkeyrev1PUTtestkeyfmengPUTtestkeyfmengisdashuaib备份与还原备份命令exportETCDCTLAPI3etcdctlendpoints{ENDPOINTS}snapshotsavedataetcdbackupdiretcdsnapshot。dbetcdctlendpoints{ENDPOINTS}snapshotrestoresnapshot。db 原理架构 从etcd的架构图中我们可以看到,etcd主要分为四个部分。HTTPServer:用于处理用户发送的API请求以及其它etcd节点的同步与心跳信息请求。Store:用于处理etcd支持的各类功能的事务,包括数据索引、节点状态变更、监控与反馈、事件处理与执行等等,是etcd对用户提供的大多数API功能的具体实现。Raft:Raft强一致性算法的具体实现,是etcd的核心。WAL:WriteAheadLog(预写式日志),是etcd的数据存储方式。除了在内存中存有所有数据的状态以及节点的索引以外,etcd就通过WAL进行持久化存储。WAL中,所有的数据提交前都会事先记录日志。Snapshot是为了防止数据过多而进行的状态快照;Entry表示存储的具体日志内容。 etcd面向client和peer节点开放http服务以及grpc服务,对于像watch机制就是基于grpc的stream通信模式实现的;EtcdServer是etcd上层结构体,其负责对外提供服务,且负责应用层的实现,比如操作应用层存储器,管理leassor、watch;raftNode负责上层与raft层的衔接。其负责将应用的需求传递到raft中进行处理(通过Step函数)、在消息发送到其他节点前将消息保存到WAL中、调用传输器发送消息;raft是raft协议的承载者;raftLog用于存储状态机信息:memoryStorge保存稳定的记录,unstable保存不稳定的记录。v2v3 etcd目前支持V2和V3两个大版本,这两个版本在实现上有比较大的不同,一方面是对外提供接口的方式,另一方面就是底层的存储引擎,V2版本的实例是一个纯内存的实现,所有的数据都没有存储在磁盘上,而V3版本的实例就支持了数据的持久化。 v3默认boltdbhttps:github。comboltdbbolthttps:github。comboltdbboltdhttps:dbdb。iodbboltdb consortiumetcd2mysql存储 数据默认会存放在varlibetcddefault目录。我们会发现数据所在的目录,会被分为两个文件夹中,分别是snap和wal目录。snap存放快照数据,存储etcd的数据状态etcd防止WAL文件过多而设置的快照wal存放预写式日志最大的作用是记录了整个数据变化的全部历程在etcd中,所有数据的修改在提交前都要先写入到WAL中Raft算法 解决三个问题:节点选举、日志复制以及安全性。 每一个Raft集群中都包含多个服务器,在任意时刻,每一台服务器只可能处于Leader、Follower以及Candidate三种状态;在处于正常的状态时,集群中只会存在一个Leader状态,其余的服务器都是Follower状态。 所有的Follower节点都是被动的,它们不会主动发出任何的请求,只会响应Leader和Candidate发出的请求。对于每一个用户的可变操作,都会被路由给Leader节点进行处理,除了Leader和Follower节点之外,Candidate节点其实只是集群运行过程中的一个临时状态。 每一个服务器都会存储当前集群的最新任期,它就像是一个单调递增的逻辑时钟,能够同步各个节点之间的状态,当前节点持有的任期会随着每一个请求被传递到其他的节点上。Raft协议在每一个任期的开始时都会从一个集群中选出一个节点作为集群的Leader节点,这个节点会负责集群中的日志的复制以及管理工作。https:zhuanlan。zhihu。comp32052223 Watch 客户端通过监听指定的key可以迅速感知key的变化并作出相应处理,watch机制的实现依赖于资源版本号revision的设计,每一次key的更新都会使得revision原子递增,因此根据不同的版本号revision的对比就可以感知新事件的发生。etcdwatch机制有着广泛的应用,比如利用etcd实现分布式锁;k8s中监听各种资源的变化,从而实现各种controller逻辑等。 watch机制的实现主要可分为三个部分客户端gRPC调用server端gRPC处理从底层存储获取更新事件 client使用watchClient的watch接口发起watch请求,与server端建立一个gRPCStream连接。 server端会为每个client生成唯一一个watchid,并记录每个client也就是watcher监听的key或者keyrange,通过recvLoop接收client请求,通过sendLoop发送请求,server端只负责收发请求和响应。 主要的实现都放在了watchalbStore层,watchalbStore会监听key的变化,然后通过syncWatchersLoop和syncVictimsLoop两个处理流程将key的更新变化包装成event,通过channel发送给gRPCserver。MVCC MVCC(MultiversionConcurrencyControl)多版本并发控制机制多版本bash3。2etcdctlput奥特曼他很帅OKbash3。2bash3。2etcdctlput奥特曼他真的很帅吗?OKbash3。2etcdctldel奥特曼1bash3。2etcdctlwatch奥特曼rev1PUT奥特曼他很帅PUT奥特曼他真的很帅吗?DELETE奥特曼 多版本的本质就是把一个key的历史变化都存下来 多版本是watch机制的基石 并发控制 场景1:程序A:我要修改fmeng这个key的value数据库:好,你改吧,我加锁。程序B:我也要修改fmeng数据库:你等着,等A改好,你再来。具体看我的锁什么时候释放。 这就是悲观锁 悲观锁:悲观得认为并发事务会冲突,所以要先拿锁,拿到锁的作修改操作读写锁(可多读,写时互斥)、互斥锁(线程互斥)等控制粒度太大,高并发下大量事务会被阻塞。 场景2程序A:我要修改fmeng这个key的value数据库:好的,你修改value时候请将版本号设置为12程序A:改好了,帮我写回磁盘程序B:我修改fmeng的value数据库:好的,你修改value时候请将版本号设置为13程序B:改好了,帮我写回磁盘同时程序C:我修改fmeng的value数据库:好的,你修改value时候请将版本号设置为13程序C:改好了,帮我写回磁盘 数据库:写回磁盘,A写好了。哎,B和C都是version13,我咋写?算了,报错吧 就是乐观锁,默认不加锁,你尽管写,冲突我认怂!乐观锁其实不是锁,只是相对悲观锁来定义,适合读多写少。 乐观锁:乐观得认为数据不会冲突,但发生冲突时要能检测到。 场景3程序A:我要更新fmeng数据库:你干吧,你的版本是20程序B:我要更新fmeng数据库:你干吧,你的版本是22程序C:我要更新fmeng数据库:你干吧,你的版本是23 这就是MVCC,在MVCC数据库中,你更新一个keyvalue数据的时候,它并不会直接覆盖原数据,而是新增一个版本来存储新的数据,每个数据都有一个版本号,版本号是一个逻辑时钟,不会因为服务器时间的差异而受影响。 MVCC不等于乐观锁!https:juejin。cnpost6885290108480651272etcd中的MVCChttps:pkg。go。devgo。etcd。ioetcdserverv3mvcctyperevisionstruct{mainint64一个全局递增的主版本号,随puttxndelete事务递增,一个事务内的keymain版本号是一致的subint64一个事务内的子版本号,从0开始随事务内putdelete操作递增} rev查的是mainGoetcdctlwatch奥特曼rev1 在底层boltdb里,实际分布是这样的: 底层的key是revision,奥特曼是用户key,他很帅就是用户value 删除 之前有delete动作,但是依然有版本记录。为什么?etcdctldel奥特曼 删除这个动作,其实etcd是在blotdb里写了一条,删除用户奥特曼 此时有个问题:用户说我的确删除了啊,真的不要了!请把空间还给我啊! 回收compact(压缩) 为了防止空间不够用,必须定期释放一些用户已经声明删除的数据,这个动作就叫做compact。 etcdctlcompact{version} compact需要一个版本号。这个版本号就是写事务递增的那个版本号,compact12345,就是说把版本12345以前的标记删除了的数据释放掉,用户没删除的数据肯定不能回收。 如何压缩:etcd可以使用带有小时时间单位的autocompaction选项来设置为自动压缩键空间:etcdctl如下发起压缩工作:etcdctlcompact123456 开发篇CRUD 注意修改go。modreplacegithub。comcoreosbboltgo。etcd。iobboltv1。3。6replacegoogle。golang。orggrpcgoogle。golang。orggrpcv1。26。0packagemainimport(contextfmttimego。etcd。ioetcdclientv3)funcmain(){initclientcli,err:clientv3。New(clientv3。Config{Endpoints:〔〕string{127。0。0。1:2379},DialTimeout:5time。Second,})iferr!nil{fmt。Printf(connecttoetcdfailed,err:v,err)return}fmt。Println(connecttoetcdsuccess)defercli。Close()putctx,cancel:context。WithTimeout(context。Background(),time。Second),errcli。Put(ctx,奥特曼,大帅哥)cancel()iferr!nil{fmt。Printf(puttoetcdfailed,err:v,err)return}getctx,cancelcontext。WithTimeout(context。Background(),time。Second)resp,err:cli。Get(ctx,奥特曼)cancel()iferr!nil{fmt。Printf(getfrometcdfailed,err:v,err)return}for,ev:rangeresp。Kvs{fmt。Printf(s:s,ev。Key,ev。Value)}} Watchpackagemainimport(contextfmttimego。etcd。ioetcdclientv3)watchdemofuncmain(){cli,err:clientv3。New(clientv3。Config{Endpoints:〔〕string{127。0。0。1:2379},DialTimeout:5time。Second,})iferr!nil{fmt。Printf(connecttoetcdfailed,err:v,err)return}fmt。Println(connecttoetcdsuccess)defercli。Close()rch:cli。Watch(context。Background(),testkey)chanWatchResponseforwresp:rangerch{for,ev:rangewresp。Events{fmt。Printf(Type:sKey:sValue:s,ev。Type,ev。Kv。Key,ev。Kv。Value)}}}服务发现 服务发现要解决的也是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务,要如何才能找到对方并建立连接。本质上来说,服务发现就是想要了解集群中是否有进程在监听udp或tcp端口,并且通过名字就可以查找和连接。 需要实现的功能;服务发现:通过服务节点能查询到服务提供外部访问的IP和端口号。比如网关代理服务时能够及时的发现服务中新增节点、丢弃不可用的服务节点。服务注册:同一service的所有节点注册到相同目录下,节点启动后将自己的信息注册到所属服务的目录中。健康检查:服务节点定时进行健康检查。注册到服务目录中的信息设置一个较短的TTL,运行正常的服务节点每隔一段时间会去更新信息的TTL,从而达到健康检查效果。 discover。gopackagemainimport(contextlogsynctimegithub。comcoreosetcdmvccmvccpbgo。etcd。ioetcdclientv3)ServiceDiscovery服务发现typeServiceDiscoverystruct{cliclientv3。ClientetcdclientserverListmap〔string〕string服务列表locksync。Mutex}NewServiceDiscovery新建发现服务funcNewServiceDiscovery(endpoints〔〕string)ServiceDiscovery{cli,err:clientv3。New(clientv3。Config{Endpoints:endpoints,DialTimeout:5time。Second,})iferr!nil{log。Fatal(err)}returnServiceDiscovery{cli:cli,serverList:make(map〔string〕string),}}WatchService初始化服务列表和监视func(sServiceDiscovery)WatchService(prefixstring)error{根据前缀获取现有的keyresp,err:s。cli。Get(context。Background(),prefix,clientv3。WithPrefix())iferr!nil{returnerr}for,ev:rangeresp。Kvs{s。SetServiceList(string(ev。Key),string(ev。Value))}监视前缀,修改变更的servergos。watcher(prefix)returnnil}watcher监听前缀func(sServiceDiscovery)watcher(prefixstring){rch:s。cli。Watch(context。Background(),prefix,clientv3。WithPrefix())log。Printf(watchingprefix:snow。。。,prefix)forwresp:rangerch{for,ev:rangewresp。Events{switchev。Type{casemvccpb。PUT:修改或者新增s。SetServiceList(string(ev。Kv。Key),string(ev。Kv。Value))casemvccpb。DELETE:删除s。DelServiceList(string(ev。Kv。Key))}}}}SetServiceList新增服务地址func(sServiceDiscovery)SetServiceList(key,valstring){s。lock。Lock()defers。lock。Unlock()s。serverList〔key〕string(val)log。Println(putkey:,key,val:,val)}DelServiceList删除服务地址func(sServiceDiscovery)DelServiceList(keystring){s。lock。Lock()defers。lock。Unlock()delete(s。serverList,key)log。Println(delkey:,key)}GetServices获取服务地址func(sServiceDiscovery)GetServices()〔〕string{s。lock。Lock()defers。lock。Unlock()addrs:make(〔〕string,0)for,v:ranges。serverList{addrsappend(addrs,v)}returnaddrs}Close关闭服务func(sServiceDiscovery)Close()error{returns。cli。Close()}funcmain(){varendpoints〔〕string{localhost:2379}ser:NewServiceDiscovery(endpoints)deferser。Close()ser。WatchService(web)ser。WatchService(gRPC)for{select{casetime。Tick(10time。Second):log。Println(ser。GetServices())}}}resgister。gopackagemainimport(contextlogtimego。etcd。ioetcdclientv3)ServiceRegister创建租约注册服务typeServiceRegisterstruct{cliclientv3。ClientetcdclientleaseIDclientv3。LeaseID租约ID租约keepalieve相应chankeepAliveChanchanclientv3。LeaseKeepAliveResponsekeystringkeyvalstringvalue}NewServiceRegister新建注册服务funcNewServiceRegister(endpoints〔〕string,key,valstring,leaseint64)(ServiceRegister,error){cli,err:clientv3。New(clientv3。Config{Endpoints:endpoints,DialTimeout:5time。Second,})iferr!nil{log。Fatal(err)}ser:ServiceRegister{cli:cli,key:key,val:val,}申请租约设置时间keepaliveiferr:ser。putKeyWithLease(lease);err!nil{returnnil,err}returnser,nil}设置租约func(sServiceRegister)putKeyWithLease(leaseint64)error{设置租约时间resp,err:s。cli。Grant(context。Background(),lease)iferr!nil{returnerr}注册服务并绑定租约,errs。cli。Put(context。Background(),s。key,s。val,clientv3。WithLease(resp。ID))iferr!nil{returnerr}设置续租定期发送需求请求leaseRespChan,err:s。cli。KeepAlive(context。Background(),resp。ID)iferr!nil{returnerr}s。leaseIDresp。IDlog。Println(s。leaseID)s。keepAliveChanleaseRespChanlog。Printf(Putkey:sval:ssuccess!,s。key,s。val)returnnil}ListenLeaseRespChan监听续租情况func(sServiceRegister)ListenLeaseRespChan(){forleaseKeepResp:ranges。keepAliveChan{log。Println(续约成功,leaseKeepResp)}log。Println(关闭续租)}Close注销服务func(sServiceRegister)Close()error{撤销租约if,err:s。cli。Revoke(context。Background(),s。leaseID);err!nil{returnerr}log。Println(撤销租约)returns。cli。Close()}funcmain(){varendpoints〔〕string{localhost:2379}ser,err:NewServiceRegister(endpoints,webnode1,localhost:8000,5)iferr!nil{log。Fatalln(err)}监听续租相应changoser。ListenLeaseRespChan()select{casetime。After(20time。Second):ser。Close()}}面试题etcd的raft和zookeeper的paxos的区别etcd和redis区别etcd性能优化(存储,网络,数据分离)etcd是如何实现一致性的?etcd的存储是如何实现的?etcd的watch机制是如何实现的?etcd的key过期机制是如何实现的,lease乐观锁与悲观锁,MVCC 案例和扩展 eBaypaymenthttps:cloud。tencent。comdeveloperarticle1538019 ebaykubernetes控制面架构 https:time。geekbang。orgcolumnintro100069901 问题怎么看etcd最多支持多少个watch的链接啊CAPetcd放弃了哪个?