
基于kubernetes 1.9版本源码 启动 入口地址在\cmd\kube-apiserver\apiserver.go\main() 函数很短,里面只有三句重要的代码 s := options.NewServerRunOptions() //新生成一个ServerRunOptions对象 s.AddFlags(pflag.CommandLine) //ServerRunOptions填值 ...... if err := app.Run(s, stopCh); err != nil { //生成一个API Server实例 NewServerRunOptions() 调用了\cmd\kube-apiserver\app\options\options.go\NewServerRunOptions()函数, 里面做的事件很简单, 就是初始化一个ServerRunOptions结构体, 这个是API Server中的关键结构体 type ServerRunOptions struct { GenericServerRunOptions *genericoptions.ServerRunOptions // 服务器通用的运行参数 Etcd *genericoptions.EtcdOptions SecureServing *genericoptions.SecureServingOptions InsecureServing *kubeoptions.InsecureServingOptions Audit *genericoptions.AuditOptions Features *genericoptions.FeatureOptions Admission *genericoptions.AdmissionOptions Authentication *kubeoptions.BuiltInAuthenticationOptions Authorization *kubeoptions.BuiltInAuthorizationOptions CloudProvider *kubeoptions.CloudProviderOptions StorageSerialization *kubeoptions.StorageSerializationOptions APIEnablement *kubeoptions.APIEnablementOptions AllowPrivileged bool // 是否配置超级权限,即允许Pod中运行的容器拥有系统特权 EnableLogsHandler bool EventTTL time.Duration // 事件留存事件, 默认1h KubeletConfig kubeletclient.KubeletClientConfig // K8S kubelet配置 KubernetesServiceNodePort int MaxConnectionBytesPerSec int64 ServiceClusterIPRange net.IPNet // TODO: make this a list ServiceNodePortRange utilnet.PortRange SSHKeyfile string // 指定的话,可以通过SSH指定的秘钥文件和用户名对Node进行访问 SSHUser string ProxyClientCertFile string ProxyClientKeyFile string EnableAggregatorRouting bool MasterCount int EndpointReconcilerType string } 上面的genericoptions定义在\staging\src\k8s.io\apiserver\pkg\server\options里面 初始化完成之后,最重要的任务就是启动实例了。所有的操作都是在run函数中执行,app.run()接口实现在cmd/kube-apiserver/app/server.go func Run(runOptions *options.ServerRunOptions, stopCh <-chan struct{}) error { // To help debugging, immediately log version glog.Infof("Version: %+v", version.Get()) server, err := CreateServerChain(runOptions, stopCh) if err != nil { return err } return server.PrepareRun().Run(stopCh) }
目前有2种方向, 一种是直接使用golang来编译出二进制包, 另外一种是通过容器来编译. 前提当然是本地需要下载k8s源码 Golang编译 直接编译也可以分成2种,一种是一次编译出来, 一种是每个模块单独编译 全编译 这种方法比较简单, 进入源码目录直接执行make命令即可, 不过对应的k8s版本, 对go语言版本有一些要求, 具体可看k8s网站或者直接编译会提示需要哪个版本go语言. 编译会显示如下信息 [root@SZD-L0113231 kubernetes-release-1.9]# make +++ [0830 08:50:54] Building the toolchain targets: k8s.io/kubernetes/hack/cmd/teststale k8s.io/kubernetes/vendor/github.com/jteeuwen/go-bindata/go-bindata +++ [0830 08:50:54] Generating bindata: test/e2e/generated/gobindata_util.go /home/czs/code/kubernetes-release-1.9 /home/czs/code/kubernetes-release-1.9/test/e2e/generated /home/czs/code/kubernetes-release-1.9/test/e2e/generated +++ [0830 08:50:55] Building go targets for linux/amd64: ./vendor/k8s.io/code-generator/cmd/deepcopy-gen +++ [0830 08:51:02] Building the toolchain targets: k8s.io/kubernetes/hack/cmd/teststale k8s.io/kubernetes/vendor/github.com/jteeuwen/go-bindata/go-bindata +++ [0830 08:51:02] Generating bindata: test/e2e/generated/gobindata_util.go /home/czs/code/kubernetes-release-1.9 /home/czs/code/kubernetes-release-1.9/test/e2e/generated /home/czs/code/kubernetes-release-1.9/test/e2e/generated +++ [0830 08:51:03] Building go targets for linux/amd64: ./vendor/k8s.io/code-generator/cmd/defaulter-gen +++ [0830 08:51:08] Building the toolchain targets: k8s.io/kubernetes/hack/cmd/teststale k8s.io/kubernetes/vendor/github.com/jteeuwen/go-bindata/go-bindata +++ [0830 08:51:08] Generating bindata: test/e2e/generated/gobindata_util.go /home/czs/code/kubernetes-release-1.9 /home/czs/code/kubernetes-release-1.9/test/e2e/generated /home/czs/code/kubernetes-release-1.9/test/e2e/generated +++ [0830 08:51:09] Building go targets for linux/amd64: ./vendor/k8s.io/code-generator/cmd/conversion-gen +++ [0830 08:51:14] Building the toolchain targets: k8s.io/kubernetes/hack/cmd/teststale k8s.io/kubernetes/vendor/github.com/jteeuwen/go-bindata/go-bindata +++ [0830 08:51:14] Generating bindata: test/e2e/generated/gobindata_util.go /home/czs/code/kubernetes-release-1.9 /home/czs/code/kubernetes-release-1.9/test/e2e/generated /home/czs/code/kubernetes-release-1.9/test/e2e/generated +++ [0830 08:51:15] Building go targets for linux/amd64: ./vendor/k8s.io/code-generator/cmd/openapi-gen +++ [0830 08:51:21] Building the toolchain targets: k8s.io/kubernetes/hack/cmd/teststale k8s.io/kubernetes/vendor/github.com/jteeuwen/go-bindata/go-bindata +++ [0830 08:51:21] Generating bindata: test/e2e/generated/gobindata_util.go /home/czs/code/kubernetes-release-1.9 /home/czs/code/kubernetes-release-1.9/test/e2e/generated /home/czs/code/kubernetes-release-1.9/test/e2e/generated +++ [0830 08:51:21] Building go targets for linux/amd64: cmd/kube-proxy cmd/kube-apiserver cmd/kube-controller-manager cmd/cloud-controller-manager cmd/kubelet cmd/kubeadm cmd/hyperkube vendor/k8s.io/kube-aggregator vendor/k8s.io/apiextensions-apiserver plugin/cmd/kube-scheduler cluster/gce/gci/mounter cmd/kubectl cmd/gendocs cmd/genkubedocs cmd/genman cmd/genyaml cmd/genswaggertypedocs cmd/linkcheck vendor/github.com/onsi/ginkgo/ginkgo test/e2e/e2e.test cmd/kubemark vendor/github.com/onsi/ginkgo/ginkgo test/e2e_node/e2e_node.test cmd/gke-certificates-controller 最后在 _output 里面生成二进制文件 编译指定模块 可以指定相关参数 make WHAT=cmd/kubelet 单独编译 把k8s源码放到如下路径/root/go/src/k8s.io/, 并且把源码文件夹名称改成kubernetes/ 然后进入/cmd/kubelet (只是以kubelet为例子) 执行go build -v命令,如果没出错,可以生成可执行文件 生成的可执行文件在当前文件夹下面 [root@SZD-L0113231 kubectl]# ls -l total 66592 drwxr-x--- 2 root root 37 Aug 29 13:09 app -rw-r----- 1 root root 1233 Aug 29 13:09 BUILD -rwxr-x--- 1 root root 68177868 Aug 30 17:28 kubectl -rw-r----- 1 root root 772 Aug 29 13:09 kubectl.go -rw-r----- 1 root root 54 Aug 29 13:09 OWNERS 镜像编译 可以通过编译镜像来编译, 不过编译镜像一般是被墙的,需要自己在dockerhub或其镜像网站先下载好, 需要注意的是, 每个对应的k8s, 对应的编译镜像有特别的版本要求, 这个可以在源代码中间的build-image/cross/VERSION里面可以看到对应的版本 docker pull googlecontainer/kube-cross:v1.9.3-1 docker pull googlecontainer/debian-iptables-amd64:v5 然后把tag改为k8s的tag, docker tag docker.io/googlecontainer/kube-cross:v1.9.3-1 gcr.io/google_containers/kube-cross:v1.9.3-1 docker tag docker.io/googlecontainer/debian-iptables-amd64:v5 gcr.io/googlecontainer/debian-iptables-amd64:v5 不要直接执行很多网上说的命令 ./build/release.sh , 实测会编译很多处理器(如arm,ppc之类的)的版本,直接把我60G的磁盘写满(写了27G),然后出错退出了。 执行 ./build/run.sh make , 只编译linux/amd64 最后会打印如下信息 +++ [0830 16:30:14] Building go targets for linux/amd64: cmd/kube-proxy cmd/kube-apiserver cmd/kube-controller-manager cmd/cloud-controller-manager cmd/kubelet cmd/kubeadm cmd/hyperkube vendor/k8s.io/kube-aggregator vendor/k8s.io/apiextensions-apiserver plugin/cmd/kube-scheduler cluster/gce/gci/mounter cmd/kubectl cmd/gendocs cmd/genkubedocs cmd/genman cmd/genyaml cmd/genswaggertypedocs cmd/linkcheck vendor/github.com/onsi/ginkgo/ginkgo test/e2e/e2e.test cmd/kubemark vendor/github.com/onsi/ginkgo/ginkgo test/e2e_node/e2e_node.test cmd/gke-certificates-controller Env for linux/amd64: GOOS=linux GOARCH=amd64 GOROOT=/usr/local/go CGO_ENABLED= CC= +++ [0830 16:36:23] Placing binaries +++ [0830 16:37:27] Syncing out of container +++ [0830 16:37:27] Stopping any currently running rsyncd container +++ [0830 16:37:27] Starting rsyncd container +++ [0830 16:37:28] Running rsync +++ [0830 16:38:47] Stopping any currently running rsyncd container 并且把生成的二进制放到 _output/dockerized/ 下面
配置文件 etcd配置文件位于/etc/etcd/etcd.conf,该配置文件一共有5个section 名称 作用 member 本节点的配置,包括监听服务端口、心跳时间等 cluster 集群配置,包括集群状态、集群名称以及本节点广播地址 proxy 用于网络自动发现服务 security 安全配置 logging 日志功能组件 具体配置采集可以见另一个文章《Etcd集群配置和使用》 初次看到配置文件,都会有一个疑问,为什么在members已经设置了监听服务地址,为什么在cluster还要再次设置一次广播地址呢? 原因:etcd主要的通信协议主要是http协议,对于http协议中所周知它是B/S结构,而非C/S结构,只能一端主动给另一端发消息而反过来则不可。所以对于集群来说,双方必须都要知道对方具体监听地址。 服务监听 我们都知道,建立socket服务端一共有5个基本步骤(C语言):1、创建socket套接字2、bind地址及端口3、listen监听服务44、accept接收客户端连接5、启动新线程为客户端服务。正所谓万变不离其宗,到了etcd中(etcd使用默认golang http模块)也是这些步骤,只不过是被封装了一下(语法糖) 启动流程见《Etcd源码分析:启动篇》 listener 当进入embed/etcd.go里面的StartEtcd()函数的时候 //为peer创建listener,socket三部曲只到了第二个步骤 if e.Peers, err = startPeerListeners(cfg); err != nil { return e, err } //为client创建listener,socket三部曲只到了第二个步骤 if e.sctxs, err = startClientListeners(cfg); err != nil { return e, err } 在创建了listener之后,开始创建EtcdServer //创建EtcdServer并且创建raftNode并运行raftNode if e.Server, err = etcdserver.NewServer(srvcfg); err != nil { return e, err } // buffer channel so goroutines on closed connections won't wait forever e.errc = make(chan error, len(e.Peers)+len(e.Clients)+2*len(e.sctxs)) ······ e.Server.Start() if err = e.servePeers(); err != nil { return e, err } if err = e.serveClients(); err != nil { return e, err } if err = e.serveMetrics(); err != nil { return e, err } serving = true Listener有两个分别为:peer listener和client listener,两者大同小异,这里拿peer listener做为分析对象。 func startPeerListeners(cfg *Config) (peers []*peerListener, err error) { ······ peers = make([]*peerListener, len(cfg.LPUrls)) ······ for i, u := range cfg.LPUrls { //循环遍历多个peer url if u.Scheme == "http" { if !cfg.PeerTLSInfo.Empty() { plog.Warningf("The scheme of peer url %s is HTTP while peer key/cert files are presented. Ignored peer key/cert files.", u.String()) } if cfg.PeerTLSInfo.ClientCertAuth { plog.Warningf("The scheme of peer url %s is HTTP while client cert auth (--peer-client-cert-auth) is enabled. Ignored client cert auth for this url.", u.String()) } } // 构造peerListener对象 监听2380 作为服务端模式 peers[i] = &peerListener{close: func(context.Context) error { return nil }} //调用接口,创建listener对象,返回来之后,socket套接字已经完成listener监听流程 peers[i].Listener, err = rafthttp.NewListener(u, &cfg.PeerTLSInfo) if err != nil { return nil, err } // once serve, overwrite with 'http.Server.Shutdown' peers[i].close = func(context.Context) error { return peers[i].Listener.Close() } plog.Info("listening for peers on ", u.String()) } return peers, nil } 下面调用关系为 startPeerListeners() [embed/etcd.go] -> rafthttp.NewListener() [rafthttp/util.go] -> transport.NewTimeoutListener() [pkg/transport/timeout_listener.go] -> newListener() [pkg/transport/listener.go] -> net.Listen() [golang net库函数] 服务监听 服务端socket需要调用Accept方法,我们来看一下serve方法。方法serve大致内容为:将每个服务放到gorouting中,也就是启动一个协程来监听服务。 先看看servePeers() func (e *Etcd) servePeers() (err error) { ph := etcdhttp.NewPeerHandler(e.Server) var peerTLScfg *tls.Config if !e.cfg.PeerTLSInfo.Empty() { if peerTLScfg, err = e.cfg.PeerTLSInfo.ServerConfig(); err != nil { return err } } for _, p := range e.Peers { gs := v3rpc.Server(e.Server, peerTLScfg) m := cmux.New(p.Listener) go gs.Serve(m.Match(cmux.HTTP2())) srv := &http.Server{ Handler: grpcHandlerFunc(gs, ph), ReadTimeout: 5 * time.Minute, ErrorLog: defaultLog.New(ioutil.Discard, "", 0), // do not log user error } go srv.Serve(m.Match(cmux.Any())) p.serve = func() error { return m.Serve() } p.close = func(ctx context.Context) error { // gracefully shutdown http.Server // close open listeners, idle connections // until context cancel or time-out stopServers(ctx, &servers{secure: peerTLScfg != nil, grpc: gs, http: srv}) return nil } } // start peer servers in a goroutine for _, pl := range e.Peers { go func(l *peerListener) { e.errHandler(l.serve()) }(pl) } return nil } 1、生成http.hander 用于处理peer请求;2、在for循环里面,起一些goroutine,调用Server()函数来接受Listener传入的连接。 我们来看看NewPeerHandler() func newPeerHandler(cluster api.Cluster, raftHandler http.Handler, leaseHandler http.Handler) http.Handler { mh := &peerMembersHandler{ cluster: cluster, } //将url和业务层handler注册到servemux中,也就是每一个url请求都会有其对应的handler进行处理 //初始化一个Serve Multiplexer结构 mux := http.NewServeMux() mux.HandleFunc("/", http.NotFound) mux.Handle(rafthttp.RaftPrefix, raftHandler) mux.Handle(rafthttp.RaftPrefix+"/", raftHandler) mux.Handle(peerMembersPrefix, mh) //处理请求/members handler是mh,即peerMembersHandler if leaseHandler != nil { mux.Handle(leasehttp.LeasePrefix, leaseHandler) mux.Handle(leasehttp.LeaseInternalPrefix, leaseHandler) } mux.HandleFunc(versionPath, versionHandler(cluster, serveVersion)) return mux } 应用层业务逻辑需要自己注册url和handler,这样才能保证每个http request都能够被处理。而每个handler都必须要实现对应接口ServeHTTP,例如peerMembersHandler,实现的ServeHTTP接口是用于返回集群成员列表 那么此处只是完成注册,那么在什么地方会调用此处handler? 答案是在ServeHTTP()里面
存储数据结构 Etcd存储在集群搭建和使用篇有简介,总结起来有如下特点: 采用kv型数据存储,一般情况下比关系型数据库快。 支持动态存储(内存)以及静态存储(磁盘)。 分布式存储,可集成为多节点集群。 存储方式,采用类似目录结构。 1、只有叶子节点才能真正存储数据,相当于文件。 2、叶子节点的父节点一定是目录,目录不能存储数据。 叶子节点数据结构位于 /store/store.go type store struct { Root *node WatcherHub *watcherHub CurrentIndex uint64 Stats *Stats CurrentVersion int ttlKeyHeap *ttlKeyHeap // need to recovery manually worldLock sync.RWMutex // stop the world lock clock clockwork.Clock readonlySet types.Set } 其父节点数据结构位于/store/node.go type node struct { Path string CreatedIndex uint64 ModifiedIndex uint64 Parent *node `json:"-"` // should not encode this field! avoid circular dependency. ExpireTime time.Time Value string // for key-value pair Children map[string]*node // for directory // A reference to the store this node is attached to. store *store } 其中Path即为key WAL 内存中的数据格式 +-------------------------------+ | F | Pad | | +-----------+ Length | | | +-------------------------------+ | type | +-------------------------------+ | CRC | +-------------------------------+ | Data | +-------------------------------+ 字段名称 含义 占用大小 文本1 文本2 文本3 F 是否存在补齐数据 0:表示没有补齐字段 1:表示存在补齐字段 1bit Pad 表示补齐长度。在F为1时有效 7bit Length 表示数据有效负载长度,不包括F、Pad自身长度、补齐字段。 56bit Type 类型 int64,8字节,有符号 CRC 校验 uint32,4字节,无符号 Data 私有数据 WAL文件数据格式 当我们持久化到文件系统中,数据格式并不是上面介绍,而是grpc格式。 WAL文件以小端序方式存储 启动一个全新etcd,默认会在目录:/var/lib/etcd/default.etcd/member/wal/中生成一个.wal文件。 WAL的定义和创建 定义在wal.go中, WAL日志文件遵循一定的命名规则,由walName()实现,格式为"序号--raft日志索引.wal"。 // 根据seq和index产生wal文件名 func walName(seq, index uint64) string { return fmt.Sprintf("%016x-%016x.wal", seq, index) } WAL对外暴露的创建接口就是Create()函数 // Create creates a WAL ready for appending records. The given metadata is // recorded at the head of each WAL file, and can be retrieved(检索) with ReadAll. func Create(dirpath string, metadata []byte) (*WAL, error) { if Exist(dirpath) { return nil, os.ErrExist } // 先在.tmp临时文件上做修改,修改完之后可以直接执行rename,这样起到了原子修改文件的效果 tmpdirpath := filepath.Clean(dirpath) + ".tmp" if fileutil.Exist(tmpdirpath) { if err := os.RemoveAll(tmpdirpath); err != nil { return nil, err } } if err := fileutil.CreateDirAll(tmpdirpath); err != nil { return nil, err } // dir/filename ,filename从walName获取 seq-index.wal p := filepath.Join(tmpdirpath, walName(0, 0)) // 对文件上互斥锁 f, err := fileutil.LockFile(p, os.O_WRONLY|os.O_CREATE, fileutil.PrivateFileMode) if err != nil { return nil, err } // 定位到文件末尾 if _, err = f.Seek(0, io.SeekEnd); err != nil { return nil, err } // 预分配文件,大小为SegmentSizeBytes(64MB) if err = fileutil.Preallocate(f.File, SegmentSizeBytes, true); err != nil { return nil, err } // 新建WAL结构 w := &WAL{ dir: dirpath, metadata: metadata,// metadata 可为nil } // 在这个wal文件上创建一个encoder w.encoder, err = newFileEncoder(f.File, 0) if err != nil { return nil, err } // 把这个上了互斥锁的文件加入到locks数组中 w.locks = append(w.locks, f) if err = w.saveCrc(0); err != nil { return nil, err } // 将metadataType类型的record记录在wal的header处 if err = w.encoder.encode(&walpb.Record{Type: metadataType, Data: metadata}); err != nil { return nil, err } // 保存空的snapshot if err = w.SaveSnapshot(walpb.Snapshot{}); err != nil { return nil, err } // 重命名,之前以.tmp结尾的文件,初始化完成之后重命名,类似原子操作 if w, err = w.renameWal(tmpdirpath); err != nil { return nil, err } // directory was renamed; sync parent dir to persist rename pdir, perr := fileutil.OpenDir(filepath.Dir(w.dir)) if perr != nil { return nil, perr } // 将上述的所有文件操作进行同步 if perr = fileutil.Fsync(pdir); perr != nil { return nil, perr } // 关闭目录 if perr = pdir.Close(); err != nil { return nil, perr } return w, nil } 其中, SaveSnapshot()是做walpb.Snapshot持久化的, 里面的内容略过, 不过里面有一行代码if err := w.encoder.encode(rec)表示一条Record需要先把序列化后才能持久化,这个是通过encode()函数完成的(encoder.go) 一个Record被序列化之后(这里为JOSN格式),会以一个Frame的格式持久化。Frame首先是一个长度字段(encodeFrameSize()完成,在encoder.go文件),64bit,其中MSB表示这个Frame是否有padding字节,接下来才是真正的序列化后的数据 WAL存储 当raft模块收到一个proposal时就会调用Save方法完成(定义在wal.go)持久化 func (w *WAL) Save(st raftpb.HardState, ents []raftpb.Entry) error { w.mu.Lock() // 上锁 defer w.mu.Unlock() // short cut(捷径), do not call sync // IsEmptyHardState returns true if the given HardState is empty. if raft.IsEmptyHardState(st) && len(ents) == 0 { return nil } // 是否需要同步刷新磁盘 mustSync := raft.MustSync(st, w.state, len(ents)) // TODO(xiangli): no more reference operator // 保存所有日志项 for i := range ents { if err := w.saveEntry(&ents[i]); err != nil { return err } } // 持久化HardState, HardState表示服务器当前状态,定义在raft.pb.go,主要包含Term、Vote、Commit if err := w.saveState(&st); err != nil { return err } // 获取最后一个LockedFile的大小(已经使用的) curOff, err := w.tail().Seek(0, io.SeekCurrent) if err != nil { return err } // 如果小于64MB if curOff < SegmentSizeBytes { if mustSync { // 如果需要sync,就执行sync return w.sync() } return nil } // 否则执行切割(也就是说明,WAL文件是可以超过64MB的) return w.cut() } snapshot snapshot比wal大小要小5倍左右,只有CRC和Data两个字段 etcd中对raft snapshot的定义如下(在文件raft.pb.go): type Snapshot struct { Data []byte `protobuf:"bytes,1,opt,name=data" json:"data,omitempty"` Metadata SnapshotMetadata `protobuf:"bytes,2,opt,name=metadata" json:"metadata"` XXX_unrecognized []byte `json:"-"` } Metadata则是snaoshot的元信息 // snapshot的元数据 type SnapshotMetadata struct { // 最后一次的配置状态 ConfState ConfState `protobuf:"bytes,1,opt,name=conf_state,json=confState" json:"conf_state"` // 被快照取代的最后的条目在日志中的索引值(appliedIndex) Index uint64 `protobuf:"varint,2,opt,name=index" json:"index"` // 该条目的任期号 Term uint64 `protobuf:"varint,3,opt,name=term" json:"term"` XXX_unrecognized []byte `json:"-"` } snapshot持久化使用func (s *Snapshotter) SaveSnap(snapshot raftpb.Snapshot) 过程比较简单, 略去, 里面可以看到snapshot文件的命名规则 // 将raft snapshot序列化后持久化到磁盘 func (s *Snapshotter) save(snapshot *raftpb.Snapshot) error { // 产生snapshot的时间 start := time.Now() // snapshot的文件名Term-Index.snap fname := fmt.Sprintf("%016x-%016x%s", snapshot.Metadata.Term, snapshot.Metadata.Index, snapSuffix) 动态存储 +--------------------+ +--------------------+ | etcdserver/raft.go | | raft/storge.go | | +<-------->+ | | startNode() | | NewMemoryStorge() | +---------^----------+ +--------------------+ | | | +---------v----------+ +--------------------+ | rafe/node.go | | rafe/node.go | | +<-------->+ | | StartNode() | | newRaft() | +---------^----------+ +--------------------+ | | | +---------v----------+ | raft/node.go | | | | run() | +--------------------+ 首先调用NewMemoryStorage进行初始化,然后在newRaft()中生成raftLog对象并且调用InitialState()进行状态初始化,最后在node中run方法接收数据。
Etcd集群搭建 环境信息 主机1 主机2 主机3 10.25.72.164 10.25.72.233 10.25.73.196 安装etcd yum install -y etcd 安装etcd 配置第一台 编辑etcd配置文件 vim /etc/etcd/etcd.conf ETCD_DATA_DIR="/var/lib/etcd/default.etcd" #etcd数据保存目录 ETCD_LISTEN_CLIENT_URLS="http://10.25.72.164:2379,http://localhost:2379" #供外部客户端使用的url ETCD_ADVERTISE_CLIENT_URLS="http://10.25.72.164:2379,http://localhost:2379" #广播给外部客户端使用的url ETCD_NAME="etcd1" #etcd实例名称 ETCD_LISTEN_PEER_URLS="http://10.25.72.164:2380" #集群内部通信使用的URL ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.25.72.164:2380" #广播给集群内其他成员访问的URL ETCD_INITIAL_CLUSTER="etcd1=http://10.25.72.164:2380,etcd2=http://10.25.72.233:2380,etcd3=http://10.25.73.196:2380" #初始集群成员列表 ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster" #集群的名称 ETCD_INITIAL_CLUSTER_STATE="new" #初始集群状态,new为新建集群 然后执行systemctl start etcd启动etcd进程 其他两台 etcd2和etcd3为加入etcd-cluster集群的实例,需要将其ETCD_INITIAL_CLUSTER_STATE设置为"exist" ETCD_DATA_DIR="/var/lib/etcd/default.etcd" ETCD_LISTEN_CLIENT_URLS="http://10.25.72.233:2379,http://localhost:2379" ETCD_ADVERTISE_CLIENT_URLS="http://10.25.72.233:2379,http://localhost:2379" ETCD_NAME="etcd2" ETCD_LISTEN_PEER_URLS="http://10.25.72.233:2380" ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.25.72.233:2380" ETCD_INITIAL_CLUSTER="etcd1=http://10.25.72.164:2380,etcd2=http://10.25.72.233:2380,etcd3=http://10.25.73.196:2380" ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster" ETCD_INITIAL_CLUSTER_STATE="exist" 搭建完毕,可以查看集群节点来确定有没有搭建成功 [root@SZD-L0110301 default.etcd]# etcdctl member list 4536e7d0bdb3b43c: name=etcd3 peerURLs=http://10.25.73.196:2380 clientURLs=http://10.25.73.196:2379,http://localhost:2379 isLeader=false c441e6c11a47ff3d: name=etcd1 peerURLs=http://10.25.72.164:2380 clientURLs=http://10.25.72.164:2379,http://localhost:2379 isLeader=true ddc007546c89f163: name=etcd2 peerURLs=http://10.25.72.223:2380 clientURLs=http://10.25.72.223:2379,http://localhost:2379 isLeader=false Etcd使用 集群数据操作命令 设键值 [root@SZD-L0110301 default.etcd]# etcdctl set /testdir/testkey "hello world" hello world key存在的方式和zookeeper类似,为 /路径/key 设置完之后,其他集群也可以查询到该值 如果dir和key不存在,该命令会创建对应的项 查看键值 切换到另外一个节点 [root@SZD-L0110303 etcd]# etcdctl get /testdir/testkey hello world 不存在的时候会报错 更新 当键不存在时,会报错 [root@SZD-L0110303 etcd]# etcdctl update /testdir/testkey "hello bruce" hello bruce 删除 [root@SZD-L0110303 etcd]# etcdctl rm /testdir/testkey PrevNode.Value: hello bruce 更多的操作命令省略,可以见help 查看API的版本 [root@SZD-L0072834 ~]# etcdctl -version etcdctl version: 3.1.10 API version: 2 切换API版本 export ETCDCTL_API=3 集群管理命令 查看API的版本 [root@SZD-L0072834 ~]# etcdctl -version etcdctl version: 3.1.10 API version: 2 切换API版本 export ETCDCTL_API=3 查看集群健康状态 [root@SZD-L0110301 default.etcd]# etcdctl cluster-health member 4536e7d0bdb3b43c is healthy: got healthy result from http://10.25.73.196:2379 member c441e6c11a47ff3d is healthy: got healthy result from http://10.25.72.164:2379 member ddc007546c89f163 is healthy: got healthy result from http://10.25.72.223:2379 cluster is healthy backup 备份 etcd 的数据,参数有: --data-dir etcd 的数据目录 --backup-dir 备份到指定路径 watch 监测一个键值的变化,一旦键值发生更新,就会输出最新的值并退出。 [root@SZD-L0110301 default.etcd]# etcdctl watch /testdir/testkey hello bruce [root@SZD-L0110303 etcd]# etcdctl update /testdir/testkey "hello bruce" 在第二个节点update之后,第一个watch的才有结果输出 watch会直接退出,如果不想退出可以设置 --forever参数, 这样就会一直监测,直到用户按 CTRL+C 退出 exec-watch 监测一个键值的变化,一旦键值发生更新,就执行给定命令。 [root@SZD-L0110301 default.etcd]# etcdctl exec-watch /testdir/testkey -- sh -c 'ls' member