我曾在美团等一线互联网公司就职,现为源图信息有限公司架构负责人,负责公司整体的系统架构工作。我也是拉勾教育专栏《Go 微服务实战 38 讲》作者,这是我的第 2 个专栏。

我很早就关注了微服务架构,对云原生、微服务、容器化、分布式中间件等都有过深入的研究,同时带领公司团队从零开始,基于 Kubernetes 搭建较为完善的开发、运维部署和容器调度的平台。目前公司的整体业务都在基于这一套架构运行,这让我在 etcd 作为服务注册与发现中心、分布式键值对存储等场景中,积累了大量的实践经验。

etcd 在分布式架构和云原生时代的落地

互联网应用经历了从早期单一架构到垂直架构,再到分布式架构的技术发展过程。在业务体系不断发展变化,用户体量和性能要求远非传统行业所能比拟的当下,越来越多的公司跨入了分布式、云原生架构的行列,分布式架构成为主流趋势。

但分布式架构系统面临着一些与生俱来的问题,比如部署复杂、响应时间慢、运维复杂等,其中最根本的是多个节点之间的数据共享问题。面对这个问题,你可以选择自己实现一个可靠的共享存储来同步信息,或者是依赖一个可靠的共享存储服务。

至于可靠的共享存储服务,etcd 是一个优秀的可选项。etcd 是一款分布式存储中间件,使用 Go 语言编写,并通过 Raft 一致性算法处理和确保分布式一致性,解决了分布式系统中数据一致性的问题。

而且作为一款分布式、可靠的键值存储组件,etcd 常用于微服务架构中的服务注册与发现中心,相较于 ZooKeeper 部署更简单,而且具有数据持久化、支持 SSL 客户端安全认证的独特优势。

此外,由于 etcd 中涉及了数据一致性、多版本并发控制、watch 监控、磁盘 IO 读写等知识点,深入学习 etcd 可以帮助我们从开源项目中学习底层原理,进一步提高分布式架构设计的能力。

除了分布式架构中的应用,etcd 还是目前非常热门的云原生存储组件,它自 2018 年底作为孵化项目加入 CNCF(云原生计算基金会) ,并于 2020 年 11 月成功毕业。

我们都知道,上 “云” 的过程必然是曲折的。以我所在的在线教育行业为例,从原有的单体业务改造到逐步替换成云原生架构,其中花费的人力、时间成本都很大,这不仅与实际的业务复杂度、升级的决心有关,更关乎技术复杂度,在线课程直播场景甚至要求架构实现高性能、高并发和高可用性,这些都远远超出传统单体应用的设计和开发要求。

etcd 作为云原生架构中重要的基础组件,各个微服务之间通过 etcd 保证调用的可用性和正确性。它的成功,在其他许多知名项目(包括 Kubernetes、CoreDNS 和 TiKV 等)也都依赖 etcd 来实现可靠的分布式数据存储上,可见一斑。

IBM 开放技术高级软件工程师兼 etcd 维护者 Sahdev Zala 也指出:“etcd 在提供分布式键 - 值存储方面发挥着关键作用。其存储功能不仅具有很高的可用性,而且能够满足大规模 Kubernetes 集群所提出的强一致性要求。”

etcd 不断提高的普及率、开放的治理、完善的功能成熟度,这使它在云原生时代大放异彩,也因此被越来越多的公司在系统服务中引入,甚至替代原有的类似组件(如 ZooKeeper、Consul、Eureka 等)。目前,etcd 已被许多公司用于生产,包括阿里巴巴、亚马逊、百度、Google 等。

为什么有这门课?

当然,作为分布式开发从业者,你可能会在学习和实践分布式组件的过程中遇到各类问题:

  • 不知道如何进行不同场景下技术组件的选型,比如如何选择对应的分布式组件来实现分布式锁;
  • 本身缺少分布式场景的实践环境,知道一些理论概念,但不知道如何深入学习分布式组件;
  • 仅仅是了解简单的基础用法,对于深层次的原理缺乏理解,工作中遇到问题不知道从何处开始排查;
  • 分布式知识点多且复杂,其中包括进程间通信、文件 IO、数据结构算法和事务并发控制等,对其中的一块不熟悉就容易产生畏难情绪。

遇到这些问题不用担心,就像我在前面提到的,etcd 是分布式和云原生架构下的重要组件,从学习 etcd 开始,在其基础上学习分布式,可以帮助你快速熟悉分布式系统实现的一些细节和原理。

现如今,企业对分布式开发人才的需求越来越多,通过搜索招聘网站你可以了解到这些岗位都属于稀缺的高薪岗位。正因为这样,掌握相关分布式组件和技能将为你在日常工作和面试晋升的考核中加分,甚至有助于提升你合理设计业务系统架构的能力。

软考论文 论云原生架构及其应用 云原生架构进阶实战_gateway

(数据来自拉勾网招聘)

etcd 作为一个可信赖的分布式键值存储服务,它能够为整个分布式集群存储一些关键数据,协助分布式集群的正常运转。而如何正确部署和运维 etcd 集群,并对集群进行优化,以及在开发层面如何正确调用 etcd 客户端 API 接口实现一致性存储等功能,变得越来越重要。

因此,我和拉勾教育平台一起合作了这门课程,希望带你深入学习 etcd 原理与实战。

课程设计

本课程从基础知识点到底层原理全面深入地展开介绍,分为 3 个模块,合计 21 讲。

基础概念与操作篇:介绍 etcd 是一款什么样的组件、etcd 相关的特性、应用场景、单机和集群部署的方式,还包括了客户端命令行工具的使用,以及 etcd 通信加密 TLS。初步了解 etcd 的这些基本使用,可以为你后面的学习打下基础。

除此之外,我还将介绍集群的动态重配置、参数调优、故障恢复及 etcd 网关模式等。生产环境中为了高可用,不可能单机部署 etcd 这样的重要组件,因此 etcd 集群的部署和应用非常重要。通过进阶学习 etcd 的这些知识点,会使我们的系统更加健壮。

etcd 实现原理及关键技术篇:介绍 etcd 的工作方式及内部实现原理,并重点介绍 etcd 的 etcd-raft 模块、WAL 日志与快照备份、多版本控制 MVCC、backend 存储、事务实现、watch 和 lease 机制等,最后梳理 etcd server 的启动流程,以及如何处理客户端请求。

通过这一模块的学习,你可以从原理层面深入了解 etcd 的工作机制以及整体架构,同时将有助于你后续二次开发或者排查遇到的问题。

实践案例篇:在掌握了 etcd 相关知识点的情况下,在应用实践部分我会带你学习 etcd ClientV3 的具体应用,包括如何基于 etcd 实现分布式锁、etcd 主从选举在统一定时任务中的应用,以及如何在微服务中集成 etcd 作为服务注册与发现中心。最后我将带你分析在 Kubernetes 中如何基于 etcd 完成容器的调度。

讲师寄语

当前,云原生架构逐步成为系统架构的主流,由于可以大大提升产品的开发迭代效率、降低运维和硬件成本,企业要不要上 “云” 已不再是一个艰难的选择题,而成为必然趋势。etcd 作为分布式架构下的一款优秀组件,在云原生时代更是大放光彩,成为 Kubernetes 平台默认的容器注册与发现组件。

如果你具有分布式基础,且正在从事分布式系统开发的工作,这个专栏的内容很适合你学习;对于微服务开发者、分布式系统的架构师和运维人员,特别是基于 etcd 进行相关实践的工程师来说,你将因此在工作中获益;而对云原生架构感兴趣的同学,也可以在这里领悟分布式系统的原理与实践。

我希望你通过 etcd 学习分布式组件的 “道”,掌握学习之道会在你后续的自我提升中发挥长期价值。无论在将来的面试还是开发中,你都能够切中分布式系统开发的要点,将原理和应用结合起来,充分体现个人的核心竞争力,更好地实现个人价值。

教学相长,希望在 etcd 以及分布式开发的学习成长过程中,我们一起进步!我在留言区等你。

近几年,云原生越来越火,你在各种大会或博客的标题里都可以见到 “云原生” 的字样,我们这次要学习的 etcd 也是云原生架构中重要的基础组件,因为 etcd 项目是 Kubernetes 内部的一大关键组件,目前有很多项目都依赖 etcd 进行可靠的分布式数据存储。

etcd 是 CoreOS 团队于 2013 年 6 月发起的开源项目,2018 年底正式加入云原生计算基金会(CNCF)。etcd 组件基于 Go 语言实现,目前最新版本为 V3.4.9。

为什么需要 etcd

在具体讲解 etcd 前,我们还是先谈谈分布式系统存在的问题。

从本质上来讲,云原生中的微服务应用属于分布式系统的一种落地实践。在分布式环境中,由于网络的复杂性、不确定性以及节点故障等情况,会产生一系列的问题。最常见的、最大的难点就是数据存储不一致的问题,即多个服务实例自身的数据或者获取到的数据各不相同。因此我们需要基于一致性的存储组件构建可靠的分布式系统。

分布式中的 CAP 理论

CAP 原理是描述分布式系统下节点数据同步的基本定理,分别指 Consistency(一致性)、Availability(可用性)和 Partition tolerance(分区容错性),这三个要素最多只能同时实现两点,不能三者兼顾。

基于分布式系统的基本特质,P(分区容错性)是必须要满足的,所以接下来需要考虑满足 C(一致性)还是 A(可用性)。

在类似银行之类对金额数据要求强一致性的系统中,要优先考虑满足数据一致性;而在大众网页之类的系统中,用户对网页版本的新旧不会有特别的要求,在这种场景下服务可用性会高于数据一致性。

了解了分布式系统中的问题,接下来让我们结合官网中的定义,看看为什么在分布式系统中需要 etcd?

etcd 是什么

根据 etcd 官网的介绍,我找到了如下定义:

A highly-available key value store for shared configuration and service discovery.
即一个用于配置共享和服务发现的键值存储系统。

软考论文 论云原生架构及其应用 云原生架构进阶实战_grpc_02

从定义上你也可以发现,etcd 归根结底是一个存储组件,且可以实现配置共享和服务发现。

在分布式系统中,各种服务配置信息的管理共享和服务发现是一个很基本也是很重要的问题,无论你调用服务还是调度容器,都需要知道对应的服务实例和容器节点地址信息。etcd 就是这样一款实现了元数据信息可靠存储的组件。

etcd 可集中管理配置信息。服务端将配置信息存储于 etcd,客户端通过 etcd 得到服务配置信息,etcd 监听配置信息的改变,发现改变通知客户端。

而 etcd 满足 CAP 理论中的 CP(一致性和分区容错性) 指标,由此我们知道,etcd 解决了分布式系统中一致性存储的问题。

etcd 中常用的术语

为了我们接下来更好地学习 etcd,我在这里给你列举了常用的 etcd 术语,尽快熟悉它们也会对接下来的学习有所助益。

软考论文 论云原生架构及其应用 云原生架构进阶实战_etcd_03

下面我们具体了解一下 etcd 的相关特性、架构和使用场景。

etcd 的特性

etcd 可以用来构建高可用的分布式键值数据库,总结来说有如下特点。

  • 简单:etcd 的安装简单,且为用户提供了 HTTP API,使用起来也很简单。
  • 存储:etcd 的基本功能,数据分层存储在文件目录中,类似于我们日常使用的文件系统。
  • Watch 机制:Watch 指定的键、前缀目录的更改,并对更改时间进行通知。
  • 安全通信:支持 SSL 证书验证。
  • 高性能:etcd 单实例可以支持 2K/s 读操作,官方也有提供基准测试脚本。
  • 一致可靠:基于 Raft 共识算法,实现分布式系统内部数据存储、服务调用的一致性和高可用性。

etcd 是一个实现了分布式一致性键值对存储的中间件,支持跨平台,拥有活跃用户的技术社区。etcd 集群中的节点基于 Raft 算法进行通信,Raft 算法保证了微服务实例或机器集群所访问的数据的可靠一致性。

在分布式系统或者 Kubernetes 集群中,etcd 可以作为服务注册与发现和键值对存储组件。不管是简单应用程序,还是复杂的容器集群,都可以很方便地从 etcd 中读取数据,满足了各种场景的需求。

etcd 的应用场景

etcd 在稳定性、可靠性和可伸缩性上表现极佳,同时也为云原生应用系统提供了协调机制。etcd 经常用于服务注册与发现的场景,此外还有键值对存储、消息发布与订阅、分布式锁等场景。

  • 键值对存储

etcd 是一个用于键值存储的组件,存储是 etcd 最基本的功能,其他应用场景都建立在 etcd 的可靠存储上。比如 Kubernetes 将一些元数据存储在 etcd 中,将存储状态数据的复杂工作交给 etcd,Kubernetes 自身的功能和架构就能更加稳定。

etcd 基于 Raft 算法,能够有力地保证分布式场景中的一致性。各个服务启动时注册到 etcd 上,同时为这些服务配置键的 TTL 时间。注册到 etcd 上面的各个服务实例通过心跳的方式定期续租,实现服务实例的状态监控。

  • 消息发布与订阅

在分布式系统中,服务之间还可以通过消息通信,即消息的发布与订阅,如下图所示:

软考论文 论云原生架构及其应用 云原生架构进阶实战_gateway_04

消息发布与订阅流程图

通过构建 etcd 消息中间件,服务提供者发布对应主题的消息,消费者则订阅他们关心的主题,一旦对应的主题有消息发布,就会产生订阅事件,消息中间件就会通知该主题所有的订阅者。

  • 分布式锁

分布式系统中涉及多个服务实例,存在跨进程之间资源调用,对于资源的协调分配,单体架构中的锁已经无法满足需要,需要引入分布式锁的概念。etcd 基于 Raft 算法,实现分布式集群的一致性,存储到 etcd 集群中的值必然是全局一致的,因此基于 etcd 很容易实现分布式锁。

etcd 的核心架构

etcd 作为一个如此重要的部件,我们只有深入理解其架构设计才能更好地学习。下面还是先来看看 etcd 总体的架构图。

软考论文 论云原生架构及其应用 云原生架构进阶实战_etcd_05

etcd 总体架构图

从上图可知,etcd 有 etcd Server、gRPC Server、存储相关的 MVCC 、Snapshot、WAL,以及 Raft 模块。

其中:

  • etcd Server 用于对外接收和处理客户端的请求;
  • gRPC Server 则是 etcd 与其他 etcd 节点之间的通信和信息同步;
  • MVCC,即多版本控制,etcd 的存储模块,键值对的每一次操作行为都会被记录存储,这些数据底层存储在 BoltDB 数据库中;
  • WAL,预写式日志,etcd 中的数据提交前都会记录到日志;
  • Snapshot 快照,以防 WAL 日志过多,用于存储某一时刻 etcd 的所有数据;
  • Snapshot 和 WAL 相结合,etcd 可以有效地进行数据存储和节点故障恢复等操作。

虽然 etcd 内部实现机制复杂,但对外提供了简单的 API 接口,方便客户端调用。我们可以通过 etcdctl 客户端命令行操作和访问 etcd 中的数据,或者通过 HTTP API 接口直接访问 etcd。

etcd 中的数据结构很简单,它的数据存储其实就是键值对的有序映射。etcd 还提供了一种键值对监测机制,即 Watch 机制,客户端通过订阅相关的键值对,获取其更改的事件信息。Watch 机制实时获取 etcd 中的增量数据更新,使数据与 etcd 同步。

etcd 目前有 V2.x 和 V3.x 两个大版本。etcd V2 和 V3 是在底层使用同一套 Raft 算法的两个独立应用,但相互之间实现原理和使用方法上差别很大,接口不一样、存储不一样,两个版本的数据互相隔离。

至于由 etcd V2 升级到 etcd V3 的情况,原有数据只能通过 etcd V2 接口访问,V3 接口创建的数据只能通过新的 V3 的接口访问。我们的专栏重点讲解当前常用且主流的 V3 版本。

小结

这一讲我主要介绍了 etcd 相关的概念。关于 etcd 你需要记住以下三点:

  • etcd 是云原生架构中的存储基石,可以有效保证存储数据的一致性和可靠性;
  • etcd 内部实现机制复杂,但是对外提供了简单直接的 API 接口;
  • 使用 etcd 的常见分布式场景包括键值对存储、服务注册与发现、消息订阅与发布、分布式锁等。

软考论文 论云原生架构及其应用 云原生架构进阶实战_grpc_06

下一讲我将开始讲解 etcd 的安装部署,手把手教你玩转 etcd 搭建。

在学习今天的内容之前,你有没有使用过 etcd,你理解的 etcd 功能是什么样的?欢迎你在留言区和我分享。

这一讲我们将一同学习 etcd 部署的几种方式,以及如何进行 TLS 加密实践,以保证 etcd 的通信安全。

etcd 单机安装部署

etcd 的安装有多种方式,这里我以 CentOS 7 为例,可以通过yum install etcd进行安装。然而通过系统工具安装的 etcd 版本比较滞后,如果需要安装最新版本的 etcd ,我们可以通过二进制包、源码编译以及 Docker 容器安装。

二进制安装

目前最新的 etcd API 版本为 v3.4,我们基于 3.4.4 版本进行实践,API 版本与最新版保持一致,在 CentOS 7 上面使用如下脚本进行安装:

ETCD_VER=v3.4.4


GITHUB_URL=https:


DOWNLOAD_URL=${GITHUB_URL}


rm -f /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz


rm -rf /tmp/etcd-download-test && mkdir -p /tmp/etcd-download-test


curl -L ${DOWNLOAD_URL}/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz -o /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz


tar xzvf /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz -C /tmp/etcd-download-test --strip-components=1


rm -f /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz


/tmp/etcd-download-test/etcd --version


/tmp/etcd-download-test/etcdctl version

下载可能比较慢,执行后,查看 etcd 版本的结果如下:

etcd Version: 3.4.4


Git SHA: e784ba73c


Go Version: go1.12.12


Go OS/Arch: linux/amd64

根据上面的执行结果可以看到,我们在 Linux 上安装成功,macOS 的二进制安装也类似,这里不重复演示了。关于 Windows 系统的安装比较简单,下载好安装包后,直接执行。其中 etcd.exe 是服务端,etcdctl.exe 是客户端,如下图所示:

软考论文 论云原生架构及其应用 云原生架构进阶实战_grpc_07

源码安装

使用源码安装,首先你需要确保本地的 Go 语言环境。如未安装,请参考 https://golang.org/doc/install 安装 Go 语言环境。我们需要 Go 版本为 1.13+,来构建最新版本的 etcd。如果你想尝试最新版本,也可以从 master 分支构建 etcd。

首先查看一下我们本地的 Go 版本:

$ go version


Go version go1.13.6 linux/amd64

经检测,本地的 Go 版本满足要求。将制定版本的 etcd 项目 clone 到本地之后,在项目文件夹构建 etcd。构建好之后,执行测试命令,确保 etcd 编译安装成功:

$ ./etcdctl version


etcdctl version: 3.4.4


API version: 3.4

经过上述步骤,我们已经通过源码编译成功安装 etcd。

除此之外,还可以使用 Docker 容器安装,这种方式更为简单,但这种方式有一个弊端:启动时会将 etcd 的端口暴露出来。

etcd 集群安装部署

刚刚我们介绍了 etcd 的单机安装,但是在实际生产环境中,为了整个集群的高可用,etcd 通常都会以集群方式部署,以避免单点故障。下面我们看看如何进行 etcd 集群部署。

引导 etcd 集群的启动有以下三种方式:

  • 静态启动
  • etcd 动态发现
  • DNS 发现

其中静态启动 etcd 集群的方式要求每个成员都知道集群中的其他成员。然而很多场景中,群集成员的 IP 可能未知。因此需要借助发现服务引导 etcd 群集启动。下面我们将会分别介绍这几种方式。

静态方式启动 etcd 集群

如果我们想在一台机器上实践 etcd 集群的搭建,可以通过 goreman 工具。

goreman 是一个 Go 语言编写的多进程管理工具,是对 Ruby 下广泛使用的 Foreman 的重写。下面我们使用 goreman 来演示在一台物理机上面以静态方式启动 etcd 集群。

前面我们已经确认过 Go 语言的安装环境,现在可以直接执行:

go get github.com/mattn/goreman

编译后的文件放在$GOPATH/bin中,$GOPATH/bin目录已经添加到了系统$PATH中,所以我们可以方便地执行命令goreman命令。

下面就是编写 Procfile 脚本,我们启动三个 etcd,具体对应如下:

软考论文 论云原生架构及其应用 云原生架构进阶实战_云原生_08

Procfile 脚本中 infra1 启动命令如下:

etcd1: etcd --name infra1 --listen-client-urls http:

infra2 和 infra3 的启动命令类似。下面我们看一下其中各配置项的说明。

  • –name:etcd 集群中的节点名,这里可以随意,方便区分且不重复即可。
  • –listen-client-urls:监听用于客户端通信的 url,同样可以监听多个。
  • –advertise-client-urls:建议使用的客户端通信 url,该值用于 etcd 代理或 etcd 成员与 etcd 节点通信。
  • –listen-peer-urls:监听用于节点之间通信的 url,可监听多个,集群内部将通过这些 url 进行数据交互 (如选举、数据同步等)。
  • –initial-advertise-peer-urls:建议用于节点之间通信的 url,节点间将以该值进行通信。
  • –initial-cluster-token: etcd-cluster-1,节点的 token 值,设置该值后集群将生成唯一 ID,并为每个节点也生成唯一 ID。当使用相同配置文件再启动一个集群时,只要该 token 值不一样,etcd 集群就不会相互影响。
  • –initial-cluster:集群中所有的 initial-advertise-peer-urls 的合集。
  • –initial-cluster-state:new,新建集群的标志。

注意上面的脚本,etcd 命令执行时需要根据本地实际的安装地址进行配置。下面我们使用goreman命令启动 etcd 集群:

goreman -f /opt/procfile start

启动完成后查看集群内的成员:

$ etcdctl --endpoints=http:


8211f1d0f64f3269, started, infra1, http:


91bc3c398fb3c146, started, infra2, http:


fd422379fda50e48, started, infra3, http:

现在我们的 etcd 集群已经搭建成功。需要注意的是:在集群启动时,我们通过静态的方式指定集群的成员。但在实际环境中,集群成员的 IP 可能不会提前知道,此时需要采用动态发现的机制。

动态发现启动 etcd 集群

Discovery Service,即发现服务,帮助新的 etcd 成员使用共享 URL 在集群引导阶段发现所有其他成员。

Discovery Service 使用已有的 etcd 集群来协调新集群的启动,主要操作如下:

  • 首先,所有新成员都与发现服务交互,并帮助生成预期的成员列表;
  • 之后,每个新成员使用此列表引导其服务器,该列表执行与--initial-cluster标志相同的功能,即设置所有集群的成员信息。

我们的实验中启动三个 etcd 实例,具体对应如下:

软考论文 论云原生架构及其应用 云原生架构进阶实战_grpc_09

下面就开始以动态发现的方式来启动 etcd 集群,具体步骤如下。

获取 discovery 的 token

首先需要生成标识新集群的唯一令牌。该令牌将用于键空间中的唯一前缀,比较简单的方法是使用 uuidgen 生成 UUID:

指定集群的大小

获取令牌时,必须指定群集大小。发现服务使用该数值来确定组成集群的所有成员:

我们需要把该 url 作为--discovery参数来启动 etcd,节点会自动使用该路径对应的目录进行 etcd 的服务注册与发现。

公共发现服务

当我们本地没有可用的 etcd 集群,etcd 官网提供了一个可以公网访问的 etcd 存储地址。你可以通过如下命令得到 etcd 服务的目录,并把它作为--discovery参数使用。

公共发现服务discovery.etcd.io以相同的方式工作,但是有一层修饰,在此之上仍然使用 etcd 群集作为数据存储。

以动态发现方式启动集群

etcd 发现模式下,启动 etcd 的命令如下:

# etcd1 启动


$ /opt/etcd/bin/etcd  --name etcd1 --initial-advertise-peer-urls http://192.168.202.128:2380 \


  --listen-peer-urls http://192.168.202.128:2380 \


  --data-dir /opt/etcd/data \


  --listen-client-urls http://192.168.202.128:2379,http://127.0.0.1:2379 \


  --advertise-client-urls http://192.168.202.128:2379 \


  --discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de

etcd2 和 etcd3 启动类似,替换 listen-peer-urls 和 advertise-client-urls 即可。需要注意的是:在我们完成了集群的初始化后,--discovery就失去了作用。当需要增加节点时,需要使用 etcdctl 进行操作。

为了安全,每次启动新 etcd 集群时,都会使用新的 discovery token 进行注册。当出现初始化启动的节点超过了指定的数量时,多余的节点会自动转化为 Proxy 模式的 etcd(在后面课时会详细介绍)。

结果验证

集群启动后,进行验证,我们看一下集群节点的健康状态:

$ /opt/etcd/bin/etcdctl  --endpoints="http://192.168.202.128:2379,http://192.168.202.129:2379,http://192.168.202.130:2379"  endpoint  health


# 结果如下


    http://192.168.202.128:2379 is healthy: successfully committed proposal: took = 3.157068ms


    http://192.168.202.130:2379 is healthy: successfully committed proposal: took = 3.300984ms


    http://192.168.202.129:2379 is healthy: successfully committed proposal: took = 3.263923ms

可以看到,集群中的三个节点都是健康的正常状态,可以说明以动态发现方式启动集群成功。

除此之外,etcd 还支持使用 DNS SRV 记录启动 etcd 集群。使用 DNSmasq 创建 DNS 服务,实际上是利用 DNS 的 SRV 记录不断轮训查询实现,DNS SRV 是 DNS 数据库中支持的一种资源记录的类型,它记录了计算机与所提供服务信息的对应关系。

至此,我们就介绍完了 etcd 安装部署的两种方式,对于可靠的系统来说,还需要考虑 etcd 集群通信的安全性,为我们的数据安全增加防护。

etcd 通信安全

etcd 支持通过 TLS 协议进行的加密通信,TLS 通道可用于对等体(指 etcd 集群中的服务实例)之间的加密内部群集通信以及加密的客户端流量。这一节我们看一下客户端 TLS 设置群集的实现。

想要实现数据 HTTPS 加密协议访问、保障数据的安全,就需要 SSL 证书,TLS 是 SSL 与 HTTPS 安全传输层协议名称。

进行 TLS 加密实践

为了进行实践,我们将安装一些实用的命令行工具,这包括 CFSSL 、cfssljson。

CFSSL 是 CloudFlare 的 PKI/TLS 加密利器。它既是命令行工具,也可以用于签名、验证和捆绑 TLS 证书的 HTTP API 服务器,需要 Go 1.12+ 版本才能构建。

环境配置

软考论文 论云原生架构及其应用 云原生架构进阶实战_etcd_10

安装 CFSSL

$ ls ~/Downloads/cfssl


cfssl-certinfo_1.4.1_linux_amd64 cfssl_1.4.1_linux_amd64          cfssljson_1.4.1_linux_amd64


chmod +x cfssl_1.4.1_linux_amd64 cfssljson_1.4.1_linux_amd64 cfssl-certinfo_1.4.1_linux_amd64


mv cfssl_1.4.1_linux_amd64 /usr/local/bin/cfssl


mv cfssljson_1.4.1_linux_amd64 /usr/local/bin/cfssljson


mv cfssl-certinfo_1.4.1_linux_amd64 /usr/bin/cfssl-certinfo

安装完成后,查看版本信息如下所示:

$ cfssl version


Version: 1.4.1


Runtime: go1.12.12

配置 CA 并创建 TLS 证书

我们将使用 CloudFlare’s PKI 工具 CFSSL 来配置 PKI 安全策略,然后用它创建 Certificate Authority(CA,即证书机构),并为 etcd 创建 TLS 证书。

首先创建 SSL 配置目录:

mkdir /opt/etcd/{bin,cfg,ssl} -p


cd /opt/etcd/ssl/

接下来完善 etcd CA 配置,写入 ca-config.json 如下的配置:

cat << EOF | tee ca-config.json


{


"signing": {


"default": {


"expiry": "87600h"


    },


"profiles": {


"etcd": {


"expiry": "87600h",


"usages": [


"signing",


"key encipherment",


"server auth",


"client auth"


        ]


      }


    }


  }


}


EOF

生成获取 etcd ca 证书,需要证书签名的请求文件,因此在 ca-csr.json 写入如下的配置:

cat << EOF | tee ca-csr.json


{


"CN": "etcd CA",


"key": {


"algo": "rsa",


"size": 2048


    },


"names": [


        {


"C": "CN",


"L": "Shanghai",


"ST": "Shanghai"


        }


    ]


}


EOF

根据上面的配置,生成 CA 凭证和私钥:

$ cfssl gencert -initca ca-csr.json | cfssljson -bare ca

生成 etcd server 证书,写入 server-csr.json 如下的配置:

cat << EOF | tee server-csr.json


{


"CN": "etcd",


"hosts": [


"192.168.202.128",


"192.168.202.129",


"192.168.202.130"


    ],


"key": {


"algo": "rsa",


"size": 2048


    },


"names": [


        {


"C": "CN",


"L": "Beijing",


"ST": "Beijing"


        }


    ]


}


EOF

最后就可以生成 server 证书了:

cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=etcd server-csr.json | cfssljson -bare server

启动 etcd 集群,配置如下:

#etcd1 启动


$ /opt/etcd/bin/etcd --name etcd1 --initial-advertise-peer-urls https://192.168.202.128:2380 \


     --listen-peer-urls https://192.168.202.128:2380 \


     --listen-client-urls https://192.168.202.128:2379,https://127.0.0.1:2379 \


     --advertise-client-urls https://192.168.202.128:2379 \


     --initial-cluster-token etcd-cluster-1 \


     --initial-cluster etcd1=https://192.168.202.128:2380, etcd2=https://192.168.202.129:2380, etcd3=https://192.168.202.130:2380 \


     --initial-cluster-state new \


     --client-cert-auth --trusted-ca-file=/opt/etcd/ssl/ca.pem \


     --cert-file=/opt/etcd/ssl/server.pem --key-file=/opt/etcd/ssl/server-key.pem \


     --peer-client-cert-auth --peer-trusted-ca-file=/opt/etcd/ssl/ca.pem \


     --peer-cert-file=/opt/etcd/ssl/server.pem --peer-key-file=/opt/etcd/ssl/server-key.pem

etcd2 和 etcd3 启动类似,注意替换 listen-peer-urls 和 advertise-client-urls。通过三台服务器的控制台可以知道,集群已经成功建立。

下面我们进行验证:

$ /opt/etcd/bin/etcdctl --cacert=/opt/etcd/ssl/ca.pem --cert=/opt/etcd/ssl/server.pem --key=/opt/etcd/ssl/server-key.pem --endpoints="https://192.168.202.128:2379,https://192.168.202.129:2379,https://192.168.202.130:2379"  endpoint health


# 输出如下:


https://192.168.202.129:2379 is healthy: successfully committed proposal: took = 9.492956ms


https://192.168.202.130:2379 is healthy: successfully committed proposal: took = 12.805109ms


https://192.168.202.128:2379 is healthy: successfully committed proposal: took = 13.036091ms

查看三个节点的健康状况,endpoint health,输出的结果符合我们的预期。

经过 TLS 加密的 etcd 集群,在进行操作时,需要加上认证相关的信息。

自动证书

如果集群需要加密的通信但不需要经过身份验证的连接,可以将 etcd 配置为自动生成其密钥。在初始化时,每个成员都基于其通告的 IP 地址和主机创建自己的密钥集。

在每台机器上,etcd 将使用以下标志启动:

$ etcd --name etcd1 --initial-advertise-peer-urls https:


  --listen-peer-urls https:


  --listen-client-urls https:


  --advertise-client-urls https:


  --initial-cluster-token etcd-cluster-1 \


  --initial-cluster infra0=https:


  --initial-cluster-state new \


  --auto-tls \


  --peer-auto-tls

由于自动签发证书并不能认证身份,直接 curl 会返回错误,需要使用 curl 的-k命令屏蔽对证书链的校验。

小结

这一讲我们主要介绍了 etcd 的安装部署方式,包括单机和集群的部署。为了高可用,在生产环境中我们一般选择使用集群的方式部署 etcd。在集群部署的时候,有静态和动态两种方式发现集群中的成员。静态的方式是直接在启动时指定各个成员的地址,但在实际环境中,集群成员的 IP 可能不会提前知道,这时候就需要采用动态发现的机制。

软考论文 论云原生架构及其应用 云原生架构进阶实战_gateway_11

Discovery Service 用于生成集群的发现令牌,需要注意的是,该令牌仅用于集群引导阶段,不能用于运行时重新配置或集群监视。一个发现令牌只能代表一个 etcd 集群,只要此令牌上的发现协议启动,即使它中途失败,也不能用于引导另一个 etcd 集群。我在这一讲最后也介绍了 etcd 集群通过 TLS 协议进行的加密通信,来保证 etcd 通信的安全。

下一讲我们将学习如何使用 etcdctl 客户端与 etcd 服务端进行交互,以及操作 etcd 服务端。有任何疑问欢迎在留言区和我互动,我们下一讲再见!

上一讲我们介绍了 etcd 的几种安装部署方式以及 TLS 安全加密等知识点。安装好 etcd 后,我们将开始体验如何使用 etcd。这一讲,我将会基于 etcd 自带的客户端工具——etcdctl 来演示 etcd 常用的一些操作,帮助你快速入手 etcd。

etcdctl 客户端

etcdctl 是一个命令行客户端,便于我们进行服务测试或手动修改数据库内容,我们刚开始熟悉 etcd 功能时可以通过 etdctl 客户端熟悉相关操作。etcdctl 在两个不同的 etcd 版本(v2 和 v3)下的功能和使用方式也完全不同。一般通过如下方式来指定使用 etcd 的版本:

export ETCDCTL_API=2


export ETCDCTL_API=3

我们的专栏课程主要讲解 API 3。etcd 项目二进制发行包中已经包含了 etcdctl 工具,通过 etcd 安装包中的 etcdctl 可执行文件可以进行调用。下面我们先来看看 etcd 的常用命令有哪些,并进行实践应用。

常用命令介绍

我们首先来看下 etcdctl 支持哪些命令,通过etcdctl -h命令查看:

$ etcdctl -h


NAME:


	etcdctl - A simple command line client for etcd3.


USAGE:


	etcdctl [flags]


VERSION:


3.4.4


API VERSION:


3.4

COMMANDS:

软考论文 论云原生架构及其应用 云原生架构进阶实战_grpc_12

OPTIONS:

软考论文 论云原生架构及其应用 云原生架构进阶实战_云原生_13

etcdctl 支持的命令大体上分为数据库操作和非数据库操作两类。其中数据库的操作命令是最常用的命令,我们将在下面具体介绍。其他的命令如用户、角色、授权、认证相关,你可以根据语法自己尝试一下。

数据库操作

数据库操作基本围绕着对键值和目录的 CRUD 操作(即增删改查),及其对应的生命周期管理。我们上手这些操作其实很方便,因为这些操作是符合 REST 风格的一套 API 操作。

etcd 在键的组织上采用了类似文件系统中目录的概念,即层次化的空间结构,我们指定的键可以作为键名,如:testkey,实际上,此时键值对放于根目录 / 下面。我们也可以为键的存储指定目录结构,如 /cluster/node/key;如果不存在 /cluster/node 目录,则 etcd Server 将会创建相应的目录结构。

下面我们基于键操作、watch、lease 三类分别介绍 etcdctl 的使用与实践。

键操作

键操作包括最常用的增删改查操作,包括 PUT、GET、DELETE 等命令。

PUT 设置或者更新某个键的值。例如:

$ etcdctl put /test/foo1 "Hello world"


$ etcdctl put /test/foo2 "Hello world2"


$ etcdctl put /test/foo3 "Hello world3"

成功写入三对键值,/test/foo1、/test/foo2 和 /test/foo3。

GET 获取指定键的值。例如获取 /testdir/testkey 对应的值:

$ etcdctl get /testdir/testkey


Hello world

除此之外, etcdctl 的 GET 命令还提供了根据指定的键(key),获取其对应的十六进制格式值,即以十六进制格式返回:

$ etcdctl get /test/foo1 --hex


\x2f\x74\x65\x73\x74\x64\x69\x72\x2f\x74\x65\x73\x74\x6b\x65\x79 #键


\x48\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64 #值

加上--print-value-only可以读取对应的值。十六进制在 etcd 中有多处使用,如租约 ID 也是十六进制。

GET 范围内的值:

$ etcdctl get /test/foo1 /test/foo3


/test/foo1


Hello world


/test/foo2


Hello world2

可以看到,上述操作获取了大于等于 /test/foo1,且小于 /test/foo3 的键值对。foo3 不在范围之内,因为范围是半开区间 [foo1, foo3),不包含 foo3。

获取某个前缀的所有键值对,通过 --prefix 可以指定前缀:

$ etcdctl get --prefix /test/foo


/test/foo1


Hello world


/test/foo2


Hello world2


/test/foo3


Hello world3

这样就能获取所有以 /test/foo 开头的键值对,当前缀获取的结果过多时,还可以通过 --limit=2 限制获取的数量:

etcdctl get --prefix --limit=2 /test/foo

读取键过往版本的值,应用可能想读取键的被替代的值。

例如,应用可能想通过访问键的过往版本回滚到旧的配置。或者,应用可能想通过多个请求得到一个覆盖多个键的统一视图,而这些请求可以通过访问键历史记录而来。因为 etcd 集群上键值存储的每个修改都会增加 etcd 集群的全局修订版本,应用可以通过提供旧有的 etcd 修改版本来读取被替代的键。现有如下键值对:

foo = bar         # revision = 2


foo1 = bar2       # revision = 3


foo = bar_new     # revision = 4


foo1 = bar1_new   # revision = 5

以下是访问以前版本 key 的示例:

$ etcdctl get --prefix foo # 访问最新版本的 key


foo


bar_new


foo1


bar1_new


$ etcdctl get --prefix --rev=4 foo # 访问第 4 个版本的 key


foo


bar_new


foo1


bar1


$ etcdctl get --prefix --rev=3 foo #  访问第 3 个版本的 key


foo


bar


foo1


bar1


$ etcdctl get --prefix --rev=2 foo #  访问第 3 个版本的 key


foo


bar


$ etcdctl get --prefix --rev=1 foo #  访问第 1 个版本的 key

应用可能想读取大于等于指定键的 byte 值的键。假设 etcd 集群已经有如下列键:

读取大于等于键 b 的 byte 值的键的命令:

$ etcdctl get --from-key b


b


456


z


789

DELETE 键,应用可以从 etcd 集群中删除一个键或者特定范围的键。

假设 etcd 集群已经有下列键:

foo = bar


foo1 = bar1


foo3 = bar3


zoo = val


zoo1 = val1


zoo2 = val2


a = 123


b = 456


z = 789

删除键 foo 的命令:

$ etcdctl del foo


1 # 删除了一个键

删除从 foo 到 foo9 范围的键的命令:

$ etcdctl del foo foo9


2 # 删除了两个键

删除键 zoo 并返回被删除的键值对的命令:

$ etcdctl del --prev-kv zoo


1   # 一个键被删除


zoo # 被删除的键


val # 被删除的键的值

删除前缀为 zoo 的键的命令:

$ etcdctl del --prefix zoo


2 # 删除了两个键

删除大于等于键 b 的 byte 值的键的命令:

$ etcdctl del --from-key b


2 # 删除了两个键
watch 键值对的改动

etcd 的 watch 功能是一个常用的功能,我们来看看通过 etcdctl 如何实现 watch 指定的键值对。

watch 监测一个键值的变化,一旦键值发生更新,就会输出最新的值并退出。例如:用户更新 testkey 键值为 Hello watch。

$ etcdctl watch testkey


# 在另外一个终端: etcdctl put testkey Hello watch


testkey


Hello watch

从 foo to foo9 范围内键的命令:

$ etcdctl watch foo foo9


# 在另外一个终端: etcdctl put foo bar


PUT


foo


bar


# 在另外一个终端: etcdctl put foo1 bar1


PUT


foo1


bar1

以 16 进制格式在键 foo 上进行观察的命令:

$ etcdctl watch foo --hex


# 在另外一个终端: etcdctl put foo bar


PUT


\x66\x6f\x6f          # 键


\x62\x61\x72          # 值

观察多个键 foo 和 zoo 的命令:

$ etcdctl watch -i


$ watch foo


$ watch zoo


# 在另外一个终端: etcdctl put foo bar


PUT


foo


bar


# 在另外一个终端: etcdctl put zoo val


PUT


zoo


val

查看 key 的历史改动,应用可能想观察 etcd 中键的历史改动。

例如,应用服务想要获取某个键的所有修改。如果应用客户端一直与 etcd 服务端保持连接,使用 watch 命令就能够实现了。但是当应用或者 etcd 实例出现异常,该键的改动可能发生在出错期间,这样导致了应用客户端没能实时接收这个更新。因此,应用客户端必须观察键的历史变动,为了做到这点,应用客户端可以在观察时指定一个历史修订版本。

首先我们需要完成下述序列的操作:

$ etcdctl put foo bar         # revision = 2


OK


$ etcdctl put foo1 bar1       # revision = 3


OK


$ etcdctl put foo bar_new     # revision = 4


OK


$ etcdctl put foo1 bar1_new   # revision = 5


OK

观察历史改动:

# 从修订版本 2 开始观察键 `foo` 的改动


$ etcdctl watch --rev=2 foo


PUT


foo


bar


PUT


foo


bar_new

从上一次历史修改开始观察:

# 在键 `foo` 上观察变更并返回被修改的值和上个修订版本的值


$ etcdctl watch --prev-kv foo


# 在另外一个终端: etcdctl put foo bar_latest


PUT


foo         # 键


bar_new     # 在修改前键 foo 的上一个值


foo         # 键


bar_latest  # 修改后键 foo 的值

压缩修订版本。

参照上述内容,etcd 保存修订版本以便应用客户端可以读取键的历史版本。但是,为了避免积累无限数量的历史数据,需要对历史的修订版本进行压缩。经过压缩,etcd 删除历史修订版本,释放存储空间,且在压缩修订版本之前的数据将不可访问。

下述命令实现了压缩修订版本:

$ etcdctl compact 5


compacted revision 5 #在压缩修订版本之前的任何修订版本都不可访问


$ etcdctl get --rev=4 foo


{"level":"warn","ts":"2020-05-04T16:37:38.020+0800","caller":"clientv3/retry_interceptor.go:62","msg":"retrying of unary invoker failed","target":"endpoint://client-c0d35565-0584-4c07-bfeb-034773278656/127.0.0.1:2379","attempt":0,"error":"rpc error: code = OutOfRange desc = etcdserver: mvcc: required revision has been compacted"}


Error: etcdserver: mvcc: required revision has been compacted
lease(租约)

lease 意为租约,类似于 Redis 中的 TTL(Time To Live)。etcd 中的键值对可以绑定到租约上,实现存活周期控制。在实际应用中,常用来实现服务的心跳,即服务在启动时获取租约,将租约与服务地址绑定,并写入 etcd 服务器,为了保持心跳状态,服务会定时刷新租约。

授予租约

应用客户端可以为 etcd 集群里面的键授予租约。当键被附加到租约时,它的存活时间被绑定到租约的存活时间,而租约的存活时间相应的被 TTL 管理。在授予租约时,每个租约的最小 TTL 值由应用客户端指定。一旦租约的 TTL 到期,租约就会过期并且所有附带的键都将被删除。

# 授予租约,TTL 为 100 秒


$ etcdctl lease grant 100


lease 694d71ddacfda227 granted with TTL(10s)


# 附加键 foo 到租约 694d71ddacfda227


$ etcdctl put --lease=694d71ddacfda227 foo10 bar


OK

在实际的操作中,建议 TTL 时间设置久一点,避免来不及操作而出现如下错误:

{"level":"warn","ts":"2020-12-04T17:12:27.957+0800","caller":"clientv3/retry_interceptor.go:62","msg":"retrying of unary invoker failed","target":"endpoint://client-f87e9b9e-a583-453b-8781-325f2984cef0/127.0.0.1:2379","attempt":0,"error":"rpc error: code = NotFound desc = etcdserver: requested lease not found"}

撤销租约

应用通过租约 ID 可以撤销租约。撤销租约将删除所有附带的 key。

我们进行下列操作:

$ etcdctl lease revoke 694d71ddacfda227


lease 694d71ddacfda227 revoked


$ etcdctl get foo10

刷新租期

应用程序可以通过刷新其 TTL 保持租约存活,因此不会过期。

$ etcdctl lease keep-alive 694d71ddacfda227


lease 694d71ddacfda227 keepalived with TTL(100)


lease 694d71ddacfda227 keepalived with TTL(100)


...

查询租期

应用客户端可以查询租赁信息,检查续订或租赁的状态,是否存在或者是否已过期。应用客户端还可以查询特定租约绑定的 key。

我们进行下述操作:

$ etcdctl lease grant 300


lease 694d71ddacfda22c granted with TTL(300s)


$ etcdctl put --lease=694d71ddacfda22c foo10 bar


OK

获取有关租赁信息以及哪些 key 绑定了租赁信息:

$ etcdctl lease timetolive 694d71ddacfda22c


lease 694d71ddacfda22c granted with TTL(300s), remaining(282s)


$ etcdctl lease timetolive --keys 694d71ddacfda22c


lease 694d71ddacfda22c granted with TTL(300s), remaining(220s), attached keys([foo10])

小结

这一讲我们主要介绍了 etcdctl 相关命令的说明以及数据库命令的使用实践。etcdctl 为用户提供一些简洁的命令,用户通过 etcdctl 可以直接与 etcd 服务端交互。etcdctl 客户端提供的操作与 HTTP API 基本上是对应的,甚至可以替代 HTTP API 的方式。通过 etcdctl 客户端工具的学习,对于我们快速熟悉 etcd 组件的功能和入门使用非常有帮助。

本讲内容如下:

软考论文 论云原生架构及其应用 云原生架构进阶实战_云原生_14

学完这一讲内容,想必你对 etcd 的常用功能已经有了一个整体的了解,但是如果在 etcd 集群信息变更的情况下,etcdctl 如何稳定地访问 etcd 服务实例,非 gRPC 客户端又该如何访问 etcd 服务端呢?这也是我们下一讲的主要内容,希望你能提前思考,也欢迎你在留言区和我交流,我们下一讲再见。

上一讲我们介绍了 etcd 的几种安装部署方式以及 TLS 安全加密等知识点。安装好 etcd 后,我们将开始体验如何使用 etcd。这一讲,我将会基于 etcd 自带的客户端工具——etcdctl 来演示 etcd 常用的一些操作,帮助你快速入手 etcd。

etcdctl 客户端

etcdctl 是一个命令行客户端,便于我们进行服务测试或手动修改数据库内容,我们刚开始熟悉 etcd 功能时可以通过 etdctl 客户端熟悉相关操作。etcdctl 在两个不同的 etcd 版本(v2 和 v3)下的功能和使用方式也完全不同。一般通过如下方式来指定使用 etcd 的版本:

export ETCDCTL_API=2


export ETCDCTL_API=3

我们的专栏课程主要讲解 API 3。etcd 项目二进制发行包中已经包含了 etcdctl 工具,通过 etcd 安装包中的 etcdctl 可执行文件可以进行调用。下面我们先来看看 etcd 的常用命令有哪些,并进行实践应用。

常用命令介绍

我们首先来看下 etcdctl 支持哪些命令,通过etcdctl -h命令查看:

$ etcdctl -h


NAME:


	etcdctl - A simple command line client for etcd3.


USAGE:


	etcdctl [flags]


VERSION:


3.4.4


API VERSION:


3.4

COMMANDS:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vURe4GIj-1657696928598)(https://s2.loli.net/2022/07/13/SLjwZvNzMC6VEHg.png)]

OPTIONS:

软考论文 论云原生架构及其应用 云原生架构进阶实战_云原生_13

etcdctl 支持的命令大体上分为数据库操作和非数据库操作两类。其中数据库的操作命令是最常用的命令,我们将在下面具体介绍。其他的命令如用户、角色、授权、认证相关,你可以根据语法自己尝试一下。

数据库操作

数据库操作基本围绕着对键值和目录的 CRUD 操作(即增删改查),及其对应的生命周期管理。我们上手这些操作其实很方便,因为这些操作是符合 REST 风格的一套 API 操作。

etcd 在键的组织上采用了类似文件系统中目录的概念,即层次化的空间结构,我们指定的键可以作为键名,如:testkey,实际上,此时键值对放于根目录 / 下面。我们也可以为键的存储指定目录结构,如 /cluster/node/key;如果不存在 /cluster/node 目录,则 etcd Server 将会创建相应的目录结构。

下面我们基于键操作、watch、lease 三类分别介绍 etcdctl 的使用与实践。

键操作

键操作包括最常用的增删改查操作,包括 PUT、GET、DELETE 等命令。

PUT 设置或者更新某个键的值。例如:

$ etcdctl put /test/foo1 "Hello world"


$ etcdctl put /test/foo2 "Hello world2"


$ etcdctl put /test/foo3 "Hello world3"

成功写入三对键值,/test/foo1、/test/foo2 和 /test/foo3。

GET 获取指定键的值。例如获取 /testdir/testkey 对应的值:

$ etcdctl get /testdir/testkey


Hello world

除此之外, etcdctl 的 GET 命令还提供了根据指定的键(key),获取其对应的十六进制格式值,即以十六进制格式返回:

$ etcdctl get /test/foo1 --hex


\x2f\x74\x65\x73\x74\x64\x69\x72\x2f\x74\x65\x73\x74\x6b\x65\x79 #键


\x48\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64 #值

加上--print-value-only可以读取对应的值。十六进制在 etcd 中有多处使用,如租约 ID 也是十六进制。

GET 范围内的值:

$ etcdctl get /test/foo1 /test/foo3


/test/foo1


Hello world


/test/foo2


Hello world2

可以看到,上述操作获取了大于等于 /test/foo1,且小于 /test/foo3 的键值对。foo3 不在范围之内,因为范围是半开区间 [foo1, foo3),不包含 foo3。

获取某个前缀的所有键值对,通过 --prefix 可以指定前缀:

$ etcdctl get --prefix /test/foo


/test/foo1


Hello world


/test/foo2


Hello world2


/test/foo3


Hello world3

这样就能获取所有以 /test/foo 开头的键值对,当前缀获取的结果过多时,还可以通过 --limit=2 限制获取的数量:

etcdctl get --prefix --limit=2 /test/foo

读取键过往版本的值,应用可能想读取键的被替代的值。

例如,应用可能想通过访问键的过往版本回滚到旧的配置。或者,应用可能想通过多个请求得到一个覆盖多个键的统一视图,而这些请求可以通过访问键历史记录而来。因为 etcd 集群上键值存储的每个修改都会增加 etcd 集群的全局修订版本,应用可以通过提供旧有的 etcd 修改版本来读取被替代的键。现有如下键值对:

foo = bar         # revision = 2


foo1 = bar2       # revision = 3


foo = bar_new     # revision = 4


foo1 = bar1_new   # revision = 5

以下是访问以前版本 key 的示例:

$ etcdctl get --prefix foo # 访问最新版本的 key


foo


bar_new


foo1


bar1_new


$ etcdctl get --prefix --rev=4 foo # 访问第 4 个版本的 key


foo


bar_new


foo1


bar1


$ etcdctl get --prefix --rev=3 foo #  访问第 3 个版本的 key


foo


bar


foo1


bar1


$ etcdctl get --prefix --rev=2 foo #  访问第 3 个版本的 key


foo


bar


$ etcdctl get --prefix --rev=1 foo #  访问第 1 个版本的 key

应用可能想读取大于等于指定键的 byte 值的键。假设 etcd 集群已经有如下列键:

读取大于等于键 b 的 byte 值的键的命令:

$ etcdctl get --from-key b


b


456


z


789

DELETE 键,应用可以从 etcd 集群中删除一个键或者特定范围的键。

假设 etcd 集群已经有下列键:

foo = bar


foo1 = bar1


foo3 = bar3


zoo = val


zoo1 = val1


zoo2 = val2


a = 123


b = 456


z = 789

删除键 foo 的命令:

$ etcdctl del foo


1 # 删除了一个键

删除从 foo 到 foo9 范围的键的命令:

$ etcdctl del foo foo9


2 # 删除了两个键

删除键 zoo 并返回被删除的键值对的命令:

$ etcdctl del --prev-kv zoo


1   # 一个键被删除


zoo # 被删除的键


val # 被删除的键的值

删除前缀为 zoo 的键的命令:

$ etcdctl del --prefix zoo


2 # 删除了两个键

删除大于等于键 b 的 byte 值的键的命令:

$ etcdctl del --from-key b


2 # 删除了两个键
watch 键值对的改动

etcd 的 watch 功能是一个常用的功能,我们来看看通过 etcdctl 如何实现 watch 指定的键值对。

watch 监测一个键值的变化,一旦键值发生更新,就会输出最新的值并退出。例如:用户更新 testkey 键值为 Hello watch。

$ etcdctl watch testkey


# 在另外一个终端: etcdctl put testkey Hello watch


testkey


Hello watch

从 foo to foo9 范围内键的命令:

$ etcdctl watch foo foo9


# 在另外一个终端: etcdctl put foo bar


PUT


foo


bar


# 在另外一个终端: etcdctl put foo1 bar1


PUT


foo1


bar1

以 16 进制格式在键 foo 上进行观察的命令:

$ etcdctl watch foo --hex


# 在另外一个终端: etcdctl put foo bar


PUT


\x66\x6f\x6f          # 键


\x62\x61\x72          # 值

观察多个键 foo 和 zoo 的命令:

$ etcdctl watch -i


$ watch foo


$ watch zoo


# 在另外一个终端: etcdctl put foo bar


PUT


foo


bar


# 在另外一个终端: etcdctl put zoo val


PUT


zoo


val

查看 key 的历史改动,应用可能想观察 etcd 中键的历史改动。

例如,应用服务想要获取某个键的所有修改。如果应用客户端一直与 etcd 服务端保持连接,使用 watch 命令就能够实现了。但是当应用或者 etcd 实例出现异常,该键的改动可能发生在出错期间,这样导致了应用客户端没能实时接收这个更新。因此,应用客户端必须观察键的历史变动,为了做到这点,应用客户端可以在观察时指定一个历史修订版本。

首先我们需要完成下述序列的操作:

$ etcdctl put foo bar         # revision = 2


OK


$ etcdctl put foo1 bar1       # revision = 3


OK


$ etcdctl put foo bar_new     # revision = 4


OK


$ etcdctl put foo1 bar1_new   # revision = 5


OK

观察历史改动:

# 从修订版本 2 开始观察键 `foo` 的改动


$ etcdctl watch --rev=2 foo


PUT


foo


bar


PUT


foo


bar_new

从上一次历史修改开始观察:

# 在键 `foo` 上观察变更并返回被修改的值和上个修订版本的值


$ etcdctl watch --prev-kv foo


# 在另外一个终端: etcdctl put foo bar_latest


PUT


foo         # 键


bar_new     # 在修改前键 foo 的上一个值


foo         # 键


bar_latest  # 修改后键 foo 的值

压缩修订版本。

参照上述内容,etcd 保存修订版本以便应用客户端可以读取键的历史版本。但是,为了避免积累无限数量的历史数据,需要对历史的修订版本进行压缩。经过压缩,etcd 删除历史修订版本,释放存储空间,且在压缩修订版本之前的数据将不可访问。

下述命令实现了压缩修订版本:

$ etcdctl compact 5


compacted revision 5 #在压缩修订版本之前的任何修订版本都不可访问


$ etcdctl get --rev=4 foo


{"level":"warn","ts":"2020-05-04T16:37:38.020+0800","caller":"clientv3/retry_interceptor.go:62","msg":"retrying of unary invoker failed","target":"endpoint://client-c0d35565-0584-4c07-bfeb-034773278656/127.0.0.1:2379","attempt":0,"error":"rpc error: code = OutOfRange desc = etcdserver: mvcc: required revision has been compacted"}


Error: etcdserver: mvcc: required revision has been compacted
lease(租约)

lease 意为租约,类似于 Redis 中的 TTL(Time To Live)。etcd 中的键值对可以绑定到租约上,实现存活周期控制。在实际应用中,常用来实现服务的心跳,即服务在启动时获取租约,将租约与服务地址绑定,并写入 etcd 服务器,为了保持心跳状态,服务会定时刷新租约。

授予租约

应用客户端可以为 etcd 集群里面的键授予租约。当键被附加到租约时,它的存活时间被绑定到租约的存活时间,而租约的存活时间相应的被 TTL 管理。在授予租约时,每个租约的最小 TTL 值由应用客户端指定。一旦租约的 TTL 到期,租约就会过期并且所有附带的键都将被删除。

# 授予租约,TTL 为 100 秒


$ etcdctl lease grant 100


lease 694d71ddacfda227 granted with TTL(10s)


# 附加键 foo 到租约 694d71ddacfda227


$ etcdctl put --lease=694d71ddacfda227 foo10 bar


OK

在实际的操作中,建议 TTL 时间设置久一点,避免来不及操作而出现如下错误:

{"level":"warn","ts":"2020-12-04T17:12:27.957+0800","caller":"clientv3/retry_interceptor.go:62","msg":"retrying of unary invoker failed","target":"endpoint://client-f87e9b9e-a583-453b-8781-325f2984cef0/127.0.0.1:2379","attempt":0,"error":"rpc error: code = NotFound desc = etcdserver: requested lease not found"}

撤销租约

应用通过租约 ID 可以撤销租约。撤销租约将删除所有附带的 key。

我们进行下列操作:

$ etcdctl lease revoke 694d71ddacfda227


lease 694d71ddacfda227 revoked


$ etcdctl get foo10

刷新租期

应用程序可以通过刷新其 TTL 保持租约存活,因此不会过期。

$ etcdctl lease keep-alive 694d71ddacfda227


lease 694d71ddacfda227 keepalived with TTL(100)


lease 694d71ddacfda227 keepalived with TTL(100)


...

查询租期

应用客户端可以查询租赁信息,检查续订或租赁的状态,是否存在或者是否已过期。应用客户端还可以查询特定租约绑定的 key。

我们进行下述操作:

$ etcdctl lease grant 300


lease 694d71ddacfda22c granted with TTL(300s)


$ etcdctl put --lease=694d71ddacfda22c foo10 bar


OK

获取有关租赁信息以及哪些 key 绑定了租赁信息:

$ etcdctl lease timetolive 694d71ddacfda22c


lease 694d71ddacfda22c granted with TTL(300s), remaining(282s)


$ etcdctl lease timetolive --keys 694d71ddacfda22c


lease 694d71ddacfda22c granted with TTL(300s), remaining(220s), attached keys([foo10])

小结

这一讲我们主要介绍了 etcdctl 相关命令的说明以及数据库命令的使用实践。etcdctl 为用户提供一些简洁的命令,用户通过 etcdctl 可以直接与 etcd 服务端交互。etcdctl 客户端提供的操作与 HTTP API 基本上是对应的,甚至可以替代 HTTP API 的方式。通过 etcdctl 客户端工具的学习,对于我们快速熟悉 etcd 组件的功能和入门使用非常有帮助。

本讲内容如下:

软考论文 论云原生架构及其应用 云原生架构进阶实战_云原生_14

学完这一讲内容,想必你对 etcd 的常用功能已经有了一个整体的了解,但是如果在 etcd 集群信息变更的情况下,etcdctl 如何稳定地访问 etcd 服务实例,非 gRPC 客户端又该如何访问 etcd 服务端呢?这也是我们下一讲的主要内容,希望你能提前思考,也欢迎你在留言区和我交流,我们下一讲再见。

gRPC proxy 是在 gRPC 层(L7)运行的无状态 etcd 反向代理,旨在减少核心 etcd 集群上的总处理负载。gRPC proxy 合并了监视和 Lease API 请求,实现了水平可伸缩性。同时,为了保护集群免受滥用客户端的侵害,gRPC proxy 实现了键值对的读请求缓存。

下面我们将围绕 gRPC proxy 基本应用、客户端端点同步、可伸缩的 API、命名空间的实现和其他扩展功能展开介绍。

gRPC proxy 基本应用

首先我们来配置 etcd 集群,集群中拥有如下的静态成员信息:

软考论文 论云原生架构及其应用 云原生架构进阶实战_云原生_17

使用etcd grpc-proxy start的命令开启 etcd 的 gRPC proxy 模式,包含上表中的静态成员:

$ etcd grpc-proxy start --endpoints=http:


{"level":"info","ts":"2020-12-13T01:41:57.561+0800","caller":"etcdmain/grpc_proxy.go:320","msg":"listening for gRPC proxy client requests","address":"192.168.10.7:12379"}


{"level":"info","ts":"2020-12-13T01:41:57.561+0800","caller":"etcdmain/grpc_proxy.go:218","msg":"started gRPC proxy","address":"192.168.10.7:12379"}

可以看到,etcd gRPC proxy 启动后在192.168.10.7:12379监听,并将客户端的请求转发到上述三个成员其中的一个。通过下述客户端读写命令,经过 proxy 发送请求:

$ ETCDCTL_API=3 etcdctl --endpoints=192.168.10.7:12379 put foo bar


OK


$ ETCDCTL_API=3 etcdctl --endpoints=192.168.10.7:12379 get foo


foo


bar

我们通过 grpc-proxy 提供的客户端地址进行访问,proxy 执行的结果符合预期,在使用方法上和普通的方式完全相同。

客户端端点同步

gRPC 代理是 gRPC 命名的提供者,支持在启动时通过写入相同的前缀端点名称进行注册。这样可以使客户端将其端点与具有一组相同前缀端点名的代理端点同步,进而实现高可用性。

下面我们来启动两个 gRPC 代理,在启动时指定自定义的前缀___grpc_proxy_endpoint来注册 gRPC 代理:

$ etcd grpc-proxy start --endpoints=localhost:12379   --listen-addr=127.0.0.1:23790   --advertise-client-url=127.0.0.1:23790   --resolver-prefix="___grpc_proxy_endpoint"   --resolver-ttl=60


{"level":"info","ts":"2020-12-13T01:46:04.885+0800","caller":"etcdmain/grpc_proxy.go:320","msg":"listening for gRPC proxy client requests","address":"127.0.0.1:23790"}


{"level":"info","ts":"2020-12-13T01:46:04.885+0800","caller":"etcdmain/grpc_proxy.go:218","msg":"started gRPC proxy","address":"127.0.0.1:23790"}


2020-12-13 01:46:04.892061 I | grpcproxy: registered "127.0.0.1:23790" with 60-second lease


$ etcd grpc-proxy start --endpoints=localhost:12379 \


>   --listen-addr=127.0.0.1:23791 \


>   --advertise-client-url=127.0.0.1:23791 \


>   --resolver-prefix="___grpc_proxy_endpoint" \


>   --resolver-ttl=60


{"level":"info","ts":"2020-12-13T01:46:43.616+0800","caller":"etcdmain/grpc_proxy.go:320","msg":"listening for gRPC proxy client requests","address":"127.0.0.1:23791"}


{"level":"info","ts":"2020-12-13T01:46:43.616+0800","caller":"etcdmain/grpc_proxy.go:218","msg":"started gRPC proxy","address":"127.0.0.1:23791"}


2020-12-13 01:46:43.622249 I | grpcproxy: registered "127.0.0.1:23791" with 60-second lease

在上面的启动命令中,将需要加入的自定义端点--resolver-prefix设置为___grpc_proxy_endpoint。启动成功之后,我们来验证下,gRPC 代理在查询成员时是否列出其所有成员作为成员列表,执行如下的命令:

ETCDCTL_API=3 etcdctl --endpoints=http:

通过下图,可以看到,通过相同的前缀端点名完成了自动发现所有成员列表的操作。

软考论文 论云原生架构及其应用 云原生架构进阶实战_gateway_18

同样地,客户端也可以通过 Sync 方法自动发现代理的端点,代码实现如下:

cli, err := clientv3.New(clientv3.Config{


    Endpoints: []string{"http://localhost:23790"},


})


if err != nil {


    log.Fatal(err)


}


defer cli.Close()


if err := cli.Sync(context.Background()); err != nil {


    log.Fatal(err)


}

相应地,如果配置的代理没有配置前缀,gRPC 代理启动命令如下:

$ ./etcd grpc-proxy start --endpoints=localhost:12379 \


>   --listen-addr=127.0.0.1:23792 \


>   --advertise-client-url=127.0.0.1:23792


# 输出结果


{"level":"info","ts":"2020-12-13T01:49:25.099+0800","caller":"etcdmain/grpc_proxy.go:320","msg":"listening for gRPC proxy client requests","address":"127.0.0.1:23792"}


{"level":"info","ts":"2020-12-13T01:49:25.100+0800","caller":"etcdmain/grpc_proxy.go:218","msg":"started gRPC proxy","address":"127.0.0.1:23792"}

我们来验证下 gRPC proxy 的成员列表 API 是不是只返回自己的advertise-client-url

ETCDCTL_API=3 etcdctl --endpoints=http:

通过下图,可以看到,结果如我们预期:当我们没有配置代理的前缀端点名时,获取其成员列表只会显示当前节点的信息,也不会包含其他的端点。

软考论文 论云原生架构及其应用 云原生架构进阶实战_etcd_19

可伸缩的 watch API

如果客户端监视同一键或某一范围内的键,gRPC 代理可以将这些客户端监视程序(c-watcher)合并为连接到 etcd 服务器的单个监视程序(s-watcher)。当 watch 事件发生时,代理将所有事件从 s-watcher 广播到其 c-watcher。

假设 N 个客户端监视相同的 key,则 gRPC 代理可以将 etcd 服务器上的监视负载从 N 减少到 1。用户可以部署多个 gRPC 代理,进一步分配服务器负载。

如下图所示,三个客户端监视键 A。gRPC 代理将三个监视程序合并,从而创建一个附加到 etcd 服务器的监视程序。

软考论文 论云原生架构及其应用 云原生架构进阶实战_etcd_20

为了有效地将多个客户端监视程序合并为一个监视程序,gRPC 代理在可能的情况下将新的 c-watcher 合并为现有的 s-watcher。由于网络延迟或缓冲的未传递事件,合并的 s-watcher 可能与 etcd 服务器不同步。

如果没有指定监视版本,gRPC 代理将不能保证 c-watcher 从最近的存储修订版本开始监视。例如,如果客户端从修订版本为 1000 的 etcd 服务器监视,则该监视者将从修订版本 1000 开始。如果客户端从 gRPC 代理监视,则可能从修订版本 990 开始监视。

类似的限制也适用于取消。取消 watch 后,etcd 服务器的修订版可能大于取消响应修订版。

对于大多数情况,这两个限制一般不会引起问题,未来也可能会有其他选项强制观察者绕过 gRPC 代理以获得更准确的修订响应。

可伸缩的 lease API

为了保持客户端申请租约的有效性,客户端至少建立一个 gRPC 连接到 etcd 服务器,以定期发送心跳信号。如果 etcd 工作负载涉及很多的客户端租约活动,这些流可能会导致 CPU 使用率过高。为了减少核心集群上的流总数,gRPC 代理支持将 lease 流合并。

假设有 N 个客户端正在更新租约,则单个 gRPC 代理将 etcd 服务器上的流负载从 N 减少到 1。在部署的过程中,可能还有其他 gRPC 代理,进一步在多个代理之间分配流。

在下图示例中,三个客户端更新了三个独立的租约(L1、L2 和 L3)。gRPC 代理将三个客户端租约流(c-stream)合并为连接到 etcd 服务器的单个租约(s-stream),以保持活动流。代理将客户端租约的心跳从 c-stream 转发到 s-stream,然后将响应返回到相应的 c-stream。

软考论文 论云原生架构及其应用 云原生架构进阶实战_etcd_21

除此之外,gRPC 代理在满足一致性时会缓存请求的响应。该功能可以保护 etcd 服务器免遭恶意 for 循环中滥用客户端的攻击。

命名空间的实现

上面我们讲到 gRPC proxy 的端点可以通过配置前缀,自动发现。而当应用程序期望对整个键空间有完全控制,etcd 集群与其他应用程序共享的情况下,为了使所有应用程序都不会相互干扰地运行,代理可以对 etcd 键空间进行分区,以便客户端大概率访问完整的键空间。

当给代理提供标志--namespace时,所有进入代理的客户端请求都将转换为在键上具有用户定义的前缀。普通的请求对 etcd 集群的访问将会在我们指定的前缀(即指定的 --namespace 的值)下,而来自代理的响应将删除该前缀;而这个操作对于客户端来说是透明的,根本察觉不到前缀。

下面我们给 gRPC proxy 命名,只需要启动时指定--namespace标识:

$ ./etcd grpc-proxy start --endpoints=localhost:12379 \


>   --listen-addr=127.0.0.1:23790 \


>   --namespace=my-prefix/


{"level":"info","ts":"2020-12-13T01:53:16.875+0800","caller":"etcdmain/grpc_proxy.go:320","msg":"listening for gRPC proxy client requests","address":"127.0.0.1:23790"}


{"level":"info","ts":"2020-12-13T01:53:16.876+0800","caller":"etcdmain/grpc_proxy.go:218","msg":"started gRPC proxy","address":"127.0.0.1:23790"}

此时对代理的访问会在 etcd 群集上自动地加上前缀,对于客户端来说没有感知。我们通过 etcdctl 客户端进行尝试:

$ ETCDCTL_API=3 etcdctl --endpoints=localhost:23790 put my-key abc


# OK


$ ETCDCTL_API=3 etcdctl --endpoints=localhost:23790 get my-key


# my-key


# abc


$ ETCDCTL_API=3 etcdctl --endpoints=localhost:2379 get my-prefix/my-key


# my-prefix/my-key


# abc

上述三条命令,首先通过代理写入键值对,然后读取。为了验证结果,第三条命令通过 etcd 集群直接读取,不过需要加上代理的前缀,两种方式得到的结果完全一致。因此,使用 proxy 的命名空间即可实现 etcd 键空间分区,对于客户端来说非常便利。

其他扩展功能

gRPC 代理的功能非常强大,除了上述提到的客户端端点同步、可伸缩 API、命名空间功能,还提供了指标与健康检查接口和 TLS 加密中止的扩展功能。

指标与健康检查接口

gRPC 代理为--endpoints定义的 etcd 成员公开了/health和 Prometheus 的/metrics接口。我们通过浏览器访问这两个接口:

软考论文 论云原生架构及其应用 云原生架构进阶实战_云原生_22

访问 metrics 接口的结果

软考论文 论云原生架构及其应用 云原生架构进阶实战_etcd_23

访问 health 接口的结果

通过代理访问/metrics端点的结果如上图所示 ,其实和普通的 etcd 集群实例没有什么区别,同样也会结合一些中间件进行统计和页面展示,如 Prometheus 和 Grafana 的组合。

除了使用默认的端点访问这两个接口,另一种方法是定义一个附加 URL,该 URL 将通过 --metrics-addr 标志来响应/metrics/health端点。命令如下所示 :

$ ./etcd grpc-proxy start \


  --endpoints http:


  --metrics-addr http:


  --listen-addr 127.0.0.1:23790 \

在执行如上启动命令时,会有如下的命令行输出,提示我们指定的 metrics 监听地址为 http://0.0.0.0:6633。

{"level":"info","ts":"2021-01-30T18:03:45.231+0800","caller":"etcdmain/grpc_proxy.go:456","msg":"gRPC proxy listening for metrics","address":"http://0.0.0.0:6633"}
TLS 加密的代理

通过使用 gRPC 代理 etcd 集群的 TLS,可以给没有使用 HTTPS 加密方式的本地客户端提供服务,实现 etcd 集群的 TLS 加密中止,即未加密的客户端与 gRPC 代理通过 HTTP 方式通信,gRPC 代理与 etcd 集群通过 TLS 加密通信。下面我们进行实践:

$ etcd --listen-client-urls https:

上述命令使用 HTTPS 启动了单个成员的 etcd 集群,然后确认 etcd 集群以 HTTPS 的方式提供服务:

# fails


$ ETCDCTL_API=3 etcdctl --endpoints=http://localhost:2379 endpoint status


# works


$ ETCDCTL_API=3 etcdctl --endpoints=https://localhost:2379 --cert=client.crt --key=client.key --cacert=ca.crt endpoint status

显然第一种方式不能访问。

接下来通过使用客户端证书连接到 etcd 端点https://localhost:2379,并在 localhost:12379 上启动 gRPC 代理,命令如下:

$ etcd grpc-proxy start --endpoints=https:

启动后,我们通过 gRPC 代理写入一个键值对测试:

$ ETCDCTL_API=3 etcdctl --endpoints=http://localhost:12379 put abc def


# OK

可以看到,使用 HTTP 的方式设置成功。

回顾上述操作,我们通过 etcd 的 gRPC 代理实现了代理与实际的 etcd 集群之间的 TLS 加密,而本地的客户端通过 HTTP 的方式与 gRPC 代理通信。因此这是一个简便的调试和开发手段,你在生产环境需要谨慎使用,以防安全风险。

小结

这一讲我们主要介绍了 etcd 中的 gRPC proxy。本讲主要内容如下:

软考论文 论云原生架构及其应用 云原生架构进阶实战_grpc_24

gRPC 代理用于支持多个 etcd 服务器端点,当代理启动时,它会随机选择一个 etcd 服务器端点来使用,该端点处理所有请求,直到代理检测到端点故障为止。如果 gRPC 代理检测到端点故障,它将切换到其他可用的端点,对客户端继续提供服务,并且隐藏了存在问题的 etcd 服务端点。

关于 gRPC 代理,你有什么经验和踩坑的经历,欢迎在留言区和我分享你的经验。

集群的部署并不是一劳永逸的事情,在我们日常的工作中经常会遇到集群的调整。下一讲,我们将会介绍如何动态配置 etcd 集群。我们下一讲再见。

今天我和你分享的主题与 etcd 集群动态调整相关。

etcd 集群部署之后,动态调整集群是经常发生的情况,比如增加 etcd 节点、移除某个 etcd 节点,或者是更新 etcd 节点的信息。这些情况都需要我们动态调整 etcd 集群。

这一讲我将介绍 etcd 如何进行常见的集群运行时重配置操作,etcd 运行时重配置命令的设计以及需要注意的内容。

这部分内容官方文档有所提及,我在写这篇内容时也和编辑有过沟通,但我认为还是有必要再讲一下的,因为集群运行时重配置是一个风险比较高的操作,仅仅阅读官方文档恐怕难以完全理解如何实践操作。通过这一讲你可以学习到集群运行时重配置的详细实战及讲解,这也是官方文档不能带给你的。

集群运行时重配置

集群运行时重配置的前提条件是只有在大多数集群成员都在正常运行时,etcd 集群才能处理重配置请求。

从两个成员的集群中删除一个成员是不安全的,因为两个成员的集群中的大多数也是两个,如果在删除过程中出现故障,集群就可能无法运行,需要从多数故障中重新启动。因此 etcd 官方建议:生产环境的集群大小始终大于两个节点。

使用场景介绍

集群的动态重新配置一般的使用场景,如下图所示:

软考论文 论云原生架构及其应用 云原生架构进阶实战_gateway_25

集群动态调整的场景图

如上所述场景中的大多数,都会涉及添加或移除成员。这些操作一般都会使用到 etcd 自带的 etcdctl 命令行工具,命令如下所示:

member add          已有集群中增加成员


member remove       移除已有集群中的成员


member update       更新集群中的成员


member list         集群成员列表

除了使用 etcdctl 修改成员,还可以使用 etcd v3 gRPC members API。

下面我基于 etcdctl 具体介绍 etcd 集群如何进行更新成员、删除成员和增加新成员等运维操作。

更新成员

首先我们来看更新成员的实践。更新成员有两种情况:client URLs 和 peer URLs。

我们回想下这两个配置的功能:

  • client URLs 用于客户端的 URL,也就是对外服务的 URL;
  • peer URLs 用作监听 URL,用于与其他节点通讯。

更新 client URLs

为了更新成员的 client URLs,只需要使用更新后的 client URL 标记(即 --advertise-client-urls)或者环境变量来重启这个成员(ETCD_ADVERTISE_CLIENT_URLS)。重启后的成员将自行发布更新后的 URL,错误更新的 client URL 将不会影响 etcd 集群的健康。

更新 peer URLs

要更新成员的 peer URLs,首先通过成员命令更新它,然后重启成员,因为更新 peer URL 修改了集群范围配置并能影响 etcd 集群的健康。

当我们要更新某个成员的 peer URL,需要找到该目标成员的 ID,使用 etcdctl 列出所有成员:

//设置环境变量


$ ENDPOINTS=http://localhost:22379


// 查询所有的集群成员


$ etcdctl  --endpoints=$ENDPOINTS  member list -w table


+------------------+---------+--------+------------------------+------------------------+------------+


|        ID        | STATUS  |  NAME  |       PEER ADDRS       |      CLIENT ADDRS      | IS LEARNER |


+------------------+---------+--------+------------------------+------------------------+------------+


| 8211f1d0f64f3269 | started | infra1 | http://127.0.0.1:12380 | http://127.0.0.1:12379 |      false |


| 91bc3c398fb3c146 | started | infra2 | http://127.0.0.1:22380 | http://127.0.0.1:22379 |      false |


| fd422379fda50e48 | started | infra3 | http://127.0.0.1:32380 | http://127.0.0.1:32379 |      false |


+------------------+---------+--------+------------------------+------------------------+------------+

在这个例子中,我们启动了三个节点的 etcd 集群。更新 8211f1d0f64f3269 成员 ID 并修改它的 peer URLs 值为 http://127.0.0.1:2380。

$ etcdctl   --endpoints=http:


Member 8211f1d0f64f3269 updated in cluster ef37ad9dc622a7c4

可以看到,集群中 8211f1d0f64f3269 对应的成员信息更新成功。更新之后,集群的成员列表如下所示:

软考论文 论云原生架构及其应用 云原生架构进阶实战_gateway_26

集群列表

随后使用新的配置重启 infra 1,即可完成 etcd 集群成员的 peer URLs 更新。

删除成员

基于上面三个节点的集群,假设我们要删除 ID 为 8211f1d0f64f3269 的成员,可使用 remove 命令来执行成员的删除:

$ etcdctl --endpoints=$ENDPOINTS member remove 8211f1d0f64f3269


Member 8211f1d0f64f3269 removed from cluster ef37ad9dc622a7c4

可以看到已经成功执行移除集群中 8211f1d0f64f3269 对应的成员 etcd 1,检查下成员列表进行确认:

软考论文 论云原生架构及其应用 云原生架构进阶实战_etcd_27

软考论文 论云原生架构及其应用 云原生架构进阶实战_云原生_28

此时目标成员将会自行关闭服务,并在日志中打印出移除信息:

13:14:54 etcd1 | {"level":"warn","ts":"2020-10-18T13:14:54.368+0800","caller":"rafthttp/peer_status.go:68","msg":"peer became inactive (message send to peer failed)","peer-id":"fd422379fda50e48","error":"failed to dial fd422379fda50e48 on stream Message (the member has been permanently removed from the cluster)"}


13:14:54 etcd1 | {"level":"warn","ts":"2020-10-18T13:14:54.368+0800","caller":"etcdserver/server.go:1084","msg":"server error","error":"the member has been permanently removed from the cluster"}

这种方式可以安全地移除 leader 和其他成员。如果是移除 leader 的场景,新 leader 被选举时集群将处于不活动状态(inactive),且持续时间通常由选举超时时间和投票过程决定。

添加新成员

当我们新起节点时,需要加入现有的 etcd 集群中。添加新成员的过程有如下两个步骤:

  1. 通过 HTTP members API 添加新成员到集群,gRPC members API 或者 etcdctl member add 命令;
  2. 使用新的集群配置启动新成员,包括更新后的成员列表(现有成员加上新成员):
$ etcdctl --endpoints=http:


Member 574399926694aee9 added to cluster ef37ad9dc622a7c4


ETCD_


ETCD_INITIAL_CLUSTER="infra4=http://localhost:2380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380"


ETCD_INITIAL_ADVERTISE_PEER_URLS="http://localhost:2380"


ETCD_INITIAL_CLUSTER_STATE="existing"

如上的命令使用 etcdctl 指定 name 和 advertised peer URLs 来添加新的成员到集群。我们新增了名为 infra 4 的节点,其启动标志了 --peer-urls=http://localhost:2380。

通过命令行的输出,可以看到添加成员执行成功。成员 574399926694aee9 添加到集群 ef37ad9dc622a7c4,并在下方输出了集群现有的信息,这些信息很重要。

下面步骤就是基于新的集群配置启动刚刚添加的成员,我们主要是直接使用 etcd 启动的方式:

etcd --name infra4 --listen-client-urls http:

虽然在启动命令中指定了集群的成员、集群的标志、集群状态等信息,但是会出现如下的报错:

Members:[&{ID:18d3ac4dcf19552b RaftAttributes:{PeerURLs:[http://localhost:2380] IsLearner:false} Attributes:{Name: ClientURLs:[]}} &{ID:91bc3c398fb3c146 RaftAttributes:{PeerURLs:[http://127.0.0.1:22380] IsLearner:false} Attributes:{Name:infra2 ClientURLs:[http://127.0.0.1:22379]}} &{ID:fd422379fda50e48 RaftAttributes:{PeerURLs:[http://127.0.0.1:32380] IsLearner:false} Attributes:{Name:infra3 ClientURLs:[http://127.0.0.1:32379]}}] RemovedMemberIDs:[]}: unmatched member while checking PeerURLs (\"http://127.0.0.1:32380\"(resolved from \"http://127.0.0.1:32380\") != \"http://127.0.0.1:2380\"(resolved from \"http://127.0.0.1:2380\"))","stacktrace":"go.etcd.io/etcd/etcdmain.startEtcdOrProxyV2\n\t/tmp/etcd-release-3.4.5/etcd/release/etcd/etcdmain/etcd.go:271\ngo.etcd.io/etcd/etcdmain.Main\n\t/tmp/etcd-release-3.4.5/etcd/release/etcd/etcdmain/main.go:46\nmain.main\n\t/tmp/etcd-release-3.4.5/etcd/release/etcd/main.go:28\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:200"}

根据报错可以知道,这种方式使得启动的新节点也是集群的方式,peer URLs 不匹配,导致了启动报错。

我们需要知道 etcdctl 添加成员时已经给出关于新成员的集群信息,并打印出成功启动它需要的环境变量。因此使用关联的标记为新的成员启动 etcd 进程:

$ export ETCD_


$ export ETCD_INITIAL_CLUSTER="infra4=http://localhost:2380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380"


$ export ETCD_INITIAL_CLUSTER_STATE=existing


$ etcd --listen-client-urls http:

如上述的命令执行完成,新成员将作为集群的一部分运行并立即开始同步集群的其他成员。如果添加多个成员,官方推荐的做法是每次配置单个成员,并在添加更多新成员前验证它正确启动。

我们此时查看集群的状态如下:

$ etcdctl --endpoints=http://localhost:22379   member list -w table


+------------------+---------+--------+------------------------+------------------------+------------+


|        ID        | STATUS  |  NAME  |       PEER ADDRS       |      CLIENT ADDRS      | IS LEARNER |


+------------------+---------+--------+------------------------+------------------------+------------+


| 18d3ac4dcf19552b | started | infra4 | http://localhost:2380  | http://localhost:2379  |      false |


| 91bc3c398fb3c146 | started | infra2 | http://127.0.0.1:22380 | http://127.0.0.1:22379 |      false |


| fd422379fda50e48 | started | infra3 | http://127.0.0.1:32380 | http://127.0.0.1:32379 |      false |


+------------------+---------+--------+------------------------+------------------------+------------+

软考论文 论云原生架构及其应用 云原生架构进阶实战_gateway_29

除此之外,如果添加新成员到一个节点的集群,在新成员启动前集群无法继续工作,因为它需要两个成员作为 galosh 才能在一致性上达成一致。这种情况仅会发生在 etcdctl member add 影响集群和新成员成功建立连接到已有成员的时间内。

运行时重配置的设计及注意点

我上面介绍了 etcd 集群重配置的常见操作。运行时重配置是分布式系统中难点之一,也很容易出错,我们需要了解运行时重配置命令的设计和注意点。

两阶段配置变更设计

在 etcd 中,出于安全考虑,每个 etcd 集群节点进行运行时重配置都必须经历两个阶段:通知集群新配置、加入新成员。

上面介绍的几种集群操作都是按照这两个步骤进行的。以添加新成员为例,两阶段描述如下。

  • 阶段一:通知集群新配置

将成员添加到 etcd 集群中,需要通过调用 API 将新成员添加到集群中。当集群同意配置修改时,API 调用返回。

  • 阶段二:加入新成员

要将新的 etcd 成员加入现有集群,需要指定正确的initial-cluster并将initial-cluster-state设置为 existing。成员启动时,它首先与现有集群通信,并验证当前集群配置是否与initial-cluster中指定的预期配置匹配。当新成员成功启动时,集群已达到预期的配置。

将过程分为两个独立的阶段,运维人员需要了解集群成员身份的变化,这实际上为我们提供了更大的灵活性,也更容易理解这个过程。

我们通过上面的实践可以发现,进行集群运行重配置时,每一阶段都会确认集群成员的数量和状态,当第一阶段没有问题时才会进行下一阶段的操作。这是为了第一阶段的状态不正常时,我们可以及时进行修正,从而避免因为第一阶段的配置问题,导致集群进入无序和混乱的状态。

集群重配置注意点

我们在前面进行了集群运行时重配置的介绍与实践,但有两点你在重配置时要特别注意。

  • 集群永久失去它的大多数成员,需要从旧数据目录启动新集群来恢复之前的状态。

集群永久失去它的大多数成员的情况下,完全有可能从现有集群中强制删除发生故障的成员来完成恢复。但是,etcd 不支持该方法,因为它绕过了不安全的常规共识提交阶段。

如果要删除的成员实际上并没有挂掉或通过同一集群中的不同成员强行删除,etcd 最终会得到具有相同 clusterID 的分散集群。这种方式将会导致后续很难调试和修复。

  • 运行时重配置禁止使用公用发现服务

公共发现服务应该仅用于引导集群。成功引导集群后,成员的 IP 地址都是已知的。若要将新成员加入现有集群,需使用运行时重新配置 API。

如果依靠公共发现服务会存在一些问题,如公共发现服务自身存在的网络问题、公共发现服务后端是否能够支撑访问负载等。

通过以上的介绍你知道了 etcd 公共发现服务的种种问题。如果要使用运行时重配置的发现服务,你最好选择构建一个私有服务。

小结

这一讲我主要介绍了 etcd 运行时重配置集群的常见操作以及 etcd 是如何设计运行时重配置、使用的注意点。

本讲内容如下:

软考论文 论云原生架构及其应用 云原生架构进阶实战_云原生_30

分布式系统中,运行时集群重配置是一个难点。运行时重配置会涉及集群的稳定性和可用性,因此需要慎之又慎,尽可能避免运行时集群重配置。如果你必须重配置 etcd 集群,你需要遵循两阶段配置变更的思想,平稳可靠地进行重配置操作。

关于动态重配置,你有什么经验和踩坑的经历,欢迎你在留言区和我分享。

我们在日常工作中经常会遇到各种服务调优,同样,对于 etcd 集群来说,也需要对其进行调优,使其处于最佳的状态。

这一讲我将通过分析 etcd 的架构,结合其核心部分对 etcd 集群进行优化。

etcd 整体分析

在对 etcd 进行调优之前,我们先来看看 etcd 集群的架构图,如下图所示:

软考论文 论云原生架构及其应用 云原生架构进阶实战_云原生_31

etcd 集群架构图

上图是一个简化了的 etcd 集群。完整的 etcd 的架构中包括 API 通信层、Raft 算法层、业务逻辑层(包括鉴权、租约等)和 Storage 存储层。

我在图中只标识出了 Raft 层,Raft 层是实现 etcd 数据一致性的关键,etcd 节点之间通过 Raft 实现一致性通信。Raft 同步数据需要通过网络,因此网络延迟和网络带宽会影响 etcd 的性能。

还有 Storage 层,Storage 层依赖 BoltDB 作为底层,用以持久化键值对。Storage 层还有 WAL 日志、快照模块。当然,谈起存储势必要提到磁盘 IO 的性能,WAL 日志受到磁盘 IO 写入速度影响,fdatasync 延迟也会影响 etcd 性能。BoltDB Tx 的锁以及 BoltDB 本身的性能也将影响 etcd 的性能。上述这些因素都有可能造成 etcd 的性能损失。

推荐的服务器配置

接下来,我们来看下部署 etcd 集群服务器的配置,这也是我们优化需要首先考虑的内容 。

etcd 在开发或测试的场景下,对硬件要求不高,而且也能运行良好。比如我们在笔记本电脑或低配置服务器上就可使用 etcd 进行开发测试。然而在实际生产环境中运行 etcd 集群时,对于性能等方面的要求就变得很高了,比如 etcd 集群对外提供服务时要求的高可用性和可靠性。因此,匹配的硬件环境是进行生产部署的良好开端。下面我就从 CPU 处理器、内存、磁盘和网络几个方面,具体介绍 etcd 官方推荐的生产环境配置。

CPU 处理器

大部分情况下,etcd 的部署对 CPU 处理器的要求不高。一般的集群只需要双核到四核的 CPU 就能平稳运行。如果 etcd 集群负载的客户端达到数千个,每秒的请求数可能是成千上万个,这种情况就需要增加 CPU 的配置,通常需要八到十六个专用内核。

内存大小

etcd 对内存的需求同样也不是很高,etcd 服务端内存占用相对较小。当然,即使这样我们也得分配足够的内存给 etcd,通常 8GB 大小的内存就足够了。etcd 服务器会缓存键值数据,其余大部分内存用于跟踪 watch 监视器。因此,对于具有数千个 watch 监视器或者数百万键值对的大型部署,我们需要相应地将内存扩展到 16GB 以上。

磁盘

磁盘 IO 速度是影响 etcd 集群性能和稳定性的最关键因素。IO 速度慢的磁盘会增加 etcd 请求的延迟,并有可能影响集群的稳定性。etcd 的一致性共识算法 Raft 依赖元数据,持久存储在日志中,因此大多数 etcd 集群成员须将请求写入磁盘。

另外,etcd 还将以增量的方式将检查点写入磁盘中,以便截断该日志。如果这些写入花费的时间太长,心跳可能会超时并触发选举,进而破坏集群的稳定性。通常,可以使用基准测试工具判断磁盘的速度是否适合 etcd,为了测量实际的顺序 IOPS,建议使用磁盘基准测试工具,例如 DiskBench 或者 fio。

etcd 对磁盘写入延迟非常敏感,通常需要 7200 RPM 转速的磁盘。对于负载较重的集群,官方建议使用 SSD 固态硬盘。etcd 仅需要适度的磁盘带宽,但是当故障成员需要赶上集群时,更大的磁盘带宽可以缩短恢复时间。通常,10MB/s 的带宽可以在 15 秒内恢复 100MB 数据,对于大型集群,建议使用 100MB/s 或更高的速度在 15 秒内恢复 1GB 数据。

在条件允许的情况下,一般使用 SSD 作为 etcd 的存储。与机械硬盘相比,SSD 写入延迟较低,能够提高 etcd 的稳定性和可靠性。如果使用机械硬盘,尽量使用转速达到 15,000 RPM 的磁盘。对于机械磁盘和 SSD,使用 RAID 0 也是提高磁盘速度的有效方法。由于 etcd 的一致复制已经获得了高可用性,至少三个集群成员不需要 RAID 的镜像和磁盘阵列。

网络

多个成员的 etcd 集群部署得益于快速可靠的网络。为了使 etcd 既能实现一致性,又能够实现容忍分区性,需要网络保证低延迟和高带宽。低延迟使得 etcd 成员可以快速通信,高带宽可以减少恢复故障 etcd 成员的时间,具有分区中断的不可靠网络将导致 etcd 集群的可用性降低。1GbE 对于常见的 etcd 部署就足够了,对于大型 etcd 集群,10GbE 的网络可以缩短平均恢复时间。

我们还可以通过规避在多个数据中心部署 etcd 成员的方式来减少网络开销,单个数据中心内部署 etcd 成员可以避免延迟开销,提升 etcd 集群的可用性。

etcd 调优

上面我们介绍了部署 etcd 推荐的硬件配置,当硬件配置固定时,我们看看如何优化 etcd 服务。

etcd 启动时的默认设置适用于网络低延迟的场景,网络延迟较高的场景下,如网络跨域数据中心,心跳间隔和选举超时的设置就需要优化。每一次超时时间应该包含一个请求从发出到响应成功的时间,当然网络慢不仅是延迟导致的,还可能受到 etcd 集群成员的低速磁盘 IO 影响。

磁盘

etcd 集群对磁盘的延迟非常敏感。因为 etcd 需要存储变更日志,多个进程同时操作磁盘可能引起更高的 fsync 延迟。IO 的延迟问题可能引起 etcd 丢失心跳、请求超时或者 Leader 临时丢失,可以通过提高 etcd 进程的磁盘优先级来解决磁盘延迟问题。

在 Linux 系统中,etcd 的磁盘优先级可以通过 Ionic 去配置,我们来看下 Ionice 的命令:

[root@etcd1 ~]# ionice -h


ionice - sets or gets process io scheduling class and priority.


Usage:


  ionice [OPTION] -p PID [PID...]


  ionice [OPTION] COMMAND


Options:


  -c, --class <class>   scheduling class name or number


                           0: none, 1: realtime, 2: best-effort, 3: idle


  -n, --classdata <num> scheduling class data


                           0-7 for realtime and best-effort classes


  -p, --pid=PID         view or modify already running process


  -t, --ignore          ignore failures


  -V, --version         output version information and exit


  -h, --help            display this help and exit

根据 Ionice 的提示,我们知道 Ionice 用来获取或设置程序的 IO 调度与优先级。因此,我们可以执行如下的命令:

$ sudo ionice -c2 -n0 -p `pgrep etcd`

上述命令指定-c2尽最大努力的调度策略,即操作系统将会尽最大努力设置 etcd 进程为最高优先级。

网络调优

如果 etcd 集群的 Leader 实例拥有大量并发客户端连接,网络延迟可能会导致 Follower 成员与 Leader 之间通信的请求处理被延迟。在 Follower 的 Send Buffer 中能看到错误的列表,类似如下的错误:

dropped MsgProp to 917ad13ee8235c3a since streamMsg's sending buffer is full


dropped MsgAppResp to 917ad13ee8235c3a since streamMsg's sending buffer is full

面对这种情况,你可以通过提高 Leader 的网络优先级来提高 Follower 的请求的响应。在 Linux 系统中,你可以使用流量控制机制来确定对等流量的优先级。流量控制器 TC(Traffic Control)用于 Linux 内核的流量控制,其实现主要是通过在输出端口处建立一个队列来实现流量控制。

tc qdisc add dev ens192 root handle 1: prio bands 3


tc filter add dev ens192 parent 1: protocol ip prio 1 u32 match ip sport 2380 0xffff flowid 1:1


tc filter add dev ens192 parent 1: protocol ip prio 1 u32 match ip dport 2380 0xffff flowid 1:1


tc filter add dev ens192 parent 1: protocol ip prio 2 u32 match ip sport 2379 0xffff flowid 1:1


tc filter add dev ens192 parent 1: protocol ip prio 2 u32 match ip dport 2379 0xffff flowid 1:1

如上的五条命令中,protocol ip表示该过滤器应该检查报文分组的协议字段。prio 1表示它们对报文处理的优先级,对于不同优先级的过滤器,系统将按照从小到大的优先级排序。其中第一条命令,建立一个优先级队列,并将该队列绑定到网络物理设备 ens192 上,其编号为 1:0。我们可以查看本地网卡的名称,以我的 Centos 7 为例,可以观察到本地的网卡名称为 ens192。

软考论文 论云原生架构及其应用 云原生架构进阶实战_软考论文 论云原生架构及其应用_32

接着有四条过滤器的命令,过滤器主要服务于分类。通过上述代码,可以观察到:用于成员间通信的 2380 端口的命令优先级高于 2379 端口。每一个端口有两条命令,分别对应:sport 和 dport。依次执行过滤器,对于相同的优先级,系统将按照命令的先后顺序执行。这几个过滤器还用到了 u32 选择器 (命令中 u32 后面的部分) 来匹配不同的数据流。

第二条和第三条命令,判断的是 dport 和 sport 字段,表示出去或者进来的不同类数据包。如果该字段与Oxffff进行与操作的结果是 2380,则flowid 1:1表示将把该数据流分配给类别 1:1。通过 TC 命令你能够提高 Leader 与 etcd 集群成员之间的网络优先级,使得 etcd 集群处于一个可靠的状态。更加详细的有关 TC 的用法这里我就不再赘述了,你可以参考 TC 的手册页

快照

etcd 追加所有键值对的变更到日志中,这些日志每一行记录一个 key 的变更,日志规模在不断增长。当简单使用 etcd 时,这些日志增长不会有问题,但集群规模比较大的时候,问题就会显现,日志就会越来越多且数据量也会变得越来越大。

为了避免大量日志,etcd 会定期生成快照。这些快照通过将当前状态的修改保存到日志,并移除旧的日志,以实现日志的压缩。

创建快照对于 etcd v2 版本来说开销比较大,所以只有当更改记录操作达到一定数量后,才会制作快照。在 etcd 中,默认创建快照的配置是每 10000 次更改才会保存快照,如果 etcd 的内存和磁盘使用率过高,也可以降低这个阈值,命令如下所示:

$ etcd --snapshot-count=5000


#或者使用环境变量的方式


$ ETCD_SNAPSHOT_COUNT=5000 etcd

使用如上两种方式,都可以实现 etcd 实例修改达到 5000 次就会保存快照。

时间参数

基本的分布式一致性协议依赖于两个单独的时间参数,分别是心跳间隔和选举超时:

  • 心跳间隔(Heartbeat Interval),该参数通常用来保活,代表 Leader 通知所有的 Follower,它还活着,仍然是 Leader,该参数被设置为节点之间网络往返时间,etcd 默认心跳间隔是 100ms;
  • 选举超时(Election Timeout),它表示 Follower 在多久后还没有收到 Leader 的心跳,它就自己尝试重新发起选举变成 Leader,一般为了避免脑裂发生,这个时间会稍微长一点,etcd 的默认选举超时是 1000ms,当然如果时间太长也会导致数据一致性的问题。

一个 etcd 集群中的所有节点应该设置一样的心跳间隔和选举超时。如果设置不一样可能导致集群不稳定。默认值可以通过命令行参数或环境参数覆盖,如下所示,单位是 ms。

# 令行参数:


$ etcd --heartbeat-interval=100 --election-timeout=500


# 环境参数:


$ ETCD_HEARTBEAT_INTERVAL=100 ETCD_ELECTION_TIMEOUT=500 etcd

我们在命令中设置了心跳间隔为 100ms,选举超时为 500ms。对应的环境变量设置在下方,较为方便。

当然,你在实际调整参数时需要做一些权衡,需要考虑网络、服务硬件、负载、集群的规模等因素。心跳间隔推荐设置为节点之间的最大 RTT,一般可设置为 RTT 的 0.5-1.5 倍。如果心跳间隔太短,etcd 实例会频繁发送没必要的心跳,增加 CPU 和网络的使用率。另外,过长的心跳间隔也会延长选举超时时间,一旦选举超时过长,还会导致需要更长的时间才能发现 Leader 故障。测量 RTT 最简单方法就是用 PING 工具。

对于选举超时的时间,应该基于心跳间隔和节点的平均 RTT 去设置。选举超时应该至少是 RTT 的 10 倍,这样才能视为在该网络中容错。例如,节点间的 RTT 是 10ms,那么超时时间至少应该是 100ms。

选举超时时间最大限制是 50000ms(即 50s),只有 etcd 被部署在全球范围内时,才应该使用这个值。如果出现不均匀的网络性能或者常规的网络延迟和丢失,会引起多次 etcd 网络重试,所以 5s 是一个安全的 RTT 最高值。只有心跳间隔为 5s 时,超时时间才应该设置为 50s。

小结

这一讲我们主要介绍了 etcd 集群优化的几个方法,首先介绍了 etcd 的核心模块,其次 etcd 物理机的硬件参数也会影响 etcd 的性能,因此介绍了官方推荐的硬件配置,然后介绍了磁盘、网络、快照以及时间参数的调优。

本讲内容总结如下:

软考论文 论云原生架构及其应用 云原生架构进阶实战_gateway_33

除了服务端的优化,我们在日常使用过程中还要注意客户端的使用,正确的用法对于一个组件来说很重要。从实践角度来说,etcd 多用于读多写少的场景,读写的开销不一样,我们应该尽量避免频繁更新键值对数据。除此之外,我们还应该尽可能地复用 lease,避免重复创建 lease。对于相同 TTL 失效时间的键值对,绑定到相同的 lease 租约上也可以避免大量重复创建 lease。

对于 etcd 集群调优,你还有哪些踩坑的经验,欢迎你在留言区和我分享。接下来,我们将开始第二模块——实现原理及关键技术的学习,下一讲就让我们从 etcd 的整体架构开始学习,从整体上了解 etcd 到底是一个什么样的架构。

从这一讲开始,我们将深入 etcd 组件的内部,进一步了解其架构以及核心功能实现的原理。了解 etcd 的实现原理,能够帮助我们日常使用 etcd 时更加得心应手,遇到问题能更快地排查定位。

今天我们先从整体上介绍 etcd 组件的架构,了解 etcd 内部各个模块之间的交互,总览 etcd。

etcd 项目结构

在介绍 etcd 整体的架构之前,我们先来看一下 etcd 项目代码的目录结构:

$ tree


.


├── auth


├── build


├── client


├── clientv3


├── contrib


├── embed


├── etcdctl


├── etcdmain


├── etcdserver


├── functional


├── hack


├── integration


├── lease


├── logos


├── mvcc


├── pkg


├── proxy


├── raft


├── scripts


├── security


├── tests


├── tools


├── vendor


├── version


└── wal

可以看到,etcd 的包还是挺多的,有二十多个。接下来我们来具体分析下其中每一个包的职责定义,整理之后如下表所示:

软考论文 论云原生架构及其应用 云原生架构进阶实战_软考论文 论云原生架构及其应用_34

etcd 核心的模块有 lease、mvcc、raft、etcdserver,其余都是辅助的功能。其中 etcdserver 是其他模块的整合。

etcd 整体架构

接下来,我们看看 etcd 的整体架构。我在上面的 etcd 项目总览中提到了 etcd 中核心的几个模块,我们使用分层的方式来描绘 etcd 的架构图,如下所示:

软考论文 论云原生架构及其应用 云原生架构进阶实战_软考论文 论云原生架构及其应用_35

  • 客户端层:包括 clientv3 和 etcdctl 等客户端。用户通过命令行或者客户端调用提供了 RESTful 风格的 API,降低了 etcd 的使用复杂度。除此之外,客户端层的负载均衡(etcd V3.4 版本的客户端默认使用的是 Round-robin,即轮询调度)和节点间故障转移等特性,提升了 etcd 服务端的高可用性。需要注意的是,etcd V3.4 之前版本的客户端存在负载均衡的 Bug,如果第一个节点出现异常,访问服务端时也可能会出现异常,建议进行升级。
  • API 接口层:API 接口层提供了客户端访问服务端的通信协议和接口定义,以及服务端节点之间相互通信的协议,我将会在下一讲重点讲解 etcd 的通信接口。etcd 有 V3 和 V2 两个版本。etcd V3 使用 gRPC 作为消息传输协议;对于之前的 V2 版本,etcd 默认使用 HTTP/1.x 协议。对于不支持 gRPC 的客户端语言,etcd 提供 JSON 的 grpc-gateway。通过 grpc-gateway 提供 RESTful 代理,转换 HTTP/JSON 请求为 gRPC 的 Protocol Buffer 格式的消息。这部分内容我们在 05 讲 “gRPC 代理模式:实现可伸缩的 etcd API” 具体做了讲解,这里我们就不再展开了。
  • etcd Raft 层:负责 Leader 选举和日志复制等功能,除了与本节点的 etcd Server 通信之外,还与集群中的其他 etcd 节点进行交互,实现分布式一致性数据同步的关键工作。
  • 逻辑层:etcd 的业务逻辑层,包括鉴权、租约、KVServer、MVCC 和 Compactor 压缩等核心功能特性。
  • etcd 存储:实现了快照、预写式日志 WAL(Write Ahead Log)。etcd V3 版本中,使用 BoltDB 来持久化存储集群元数据和用户写入的数据。

下面我们看一下 etcd 各个模块之间的交互过程。

etcd 交互总览

我们通过学习 etcd 服务端处理客户端的写请求的过程,展示 etcd 内部各个模块之间的交互。首先通过命令行工具 etcdctl 写入键值对:

etcdctl --endpoints http:

下图中展示了 etcd 处理一个客户端请求涉及的模块和流程。

软考论文 论云原生架构及其应用 云原生架构进阶实战_gateway_36

从上至下依次为客户端 → API 接口层 → etcd Server → etcd raft 算法库。我们根据请求处理的过程,将 etcd Server 和 etcd raft 算法库单独说明。

  • etcd Server:接收客户端的请求,在上述的 etcd 项目代码中对应 etcdserver 包。请求到达 etcd Server 之后,经过 KVServer 拦截,实现诸如日志、Metrics 监控、请求校验等功能。etcd Server 中的 raft 模块,用于与 etcd-raft 库进行通信。applierV3 模块封装了 etcd V3 版本的数据存储;WAL 用于写数据日志,WAL 中保存了任期号、投票信息、已提交索引、提案内容等,etcd 根据 WAL 中的内容在启动时恢复,以此实现集群的数据一致性。
  • etcdraft:etcd 的 raft 库。raftLog 用于管理 raft 协议中单个节点的日志,都处于内存中。raftLog 中还有两种结构体 unstable 和 storage,unsable 中存储不稳定的数据,表示还没有 commit,而 storage 中都是已经被 commit 了的数据。这两种结构体分别用于不同步骤的存储,我们将在下面的交互流程中介绍。除此之外,raft 库更重要的是负责与集群中的其他 etcd Server 进行交互,实现分布式一致性。

在上图中,客户端请求与 etcd 集群交互包括如下两个步骤:

  • 首先是写数据到 etcd 节点中;
  • 其次是当前的 etcd 节点与集群中的其他 etcd 节点之间进行通信,确认存储数据成功之后回复客户端。

请求流程可划分为以下的子步骤:

软考论文 论云原生架构及其应用 云原生架构进阶实战_grpc_37

  • 客户端通过负载均衡算法选择一个 etcd 节点,发起 gRPC 调用;
  • etcd Server 收到客户端请求;
  • 经过 gRPC 拦截、Quota 校验,Quota 模块用于校验 etcd db 文件大小是否超过了配额;
  • 接着 KVServer 模块将请求发送给本模块中的 raft,这里负责与 etcd raft 模块进行通信;
  • 发起一个提案,命令为put foo bar,即使用 put 方法将 foo 更新为 bar;
  • 在 raft 中会将数据封装成 raft 日志的形式提交给 raft 模块;
  • raft 模块会首先保存到 raftLog 的 unstable 存储部分;
  • raft 模块通过 raft 协议与集群中其他 etcd 节点进行交互。

需要注意的是,在 raft 协议中写入数据的 etcd 必定是 leader 节点,如果客户端提交数据到非 leader 节点时,该节点需要将请求转发到 etcd leader 节点处理。

我们继续来看相应的应答步骤,流程如下:

软考论文 论云原生架构及其应用 云原生架构进阶实战_grpc_38

  • 提案通过 RaftHTTP 网络模块转发,集群中的其他节点接收到该提案;
  • 在收到提案之后,集群中其他节点向 leader 节点应答 “我已经接收这条日志数据”;
  • Leader 收到应答之后,统计应答的数量,当满足超过集群半数以上节点,应答接收成功;
  • etcd raft 算法模块构造 Ready 结构体,用来通知 etcd Server 模块,该日志数据已经被 commit;
  • etcd Server 中的 raft 模块(交互图中有标识),收到 Ready 消息后,会将这条日志数据写入到 WAL 模块中;
  • 正式通知 etcd Server 该提案已经被 commit;
  • etcd Server 调用 applierV3 模块,将日志写入持久化存储中;
  • etcd Server 应答客户端该数据写入成功;
  • etcd Server 调用 etcd raft 库,将这条日志写入到 raftLog 模块中的 storage。

上述过程中,提案经过网络转发,当多数 etcd 节点持久化日志数据成功并进行应答,提案的状态会变成已提交。

在应答某条日志数据是否已经 commit 时,为什么 etcd raft 模块首先写入到 WAL 模块中?这是因为该过程仅仅添加一条日志,一方面开销小,速度会很快;另一方面,如果在后面 applierV3 写入失败,etcd 服务端在重启的时候也可以根据 WAL 模块中的日志数据进行恢复。etcd Server 从 raft 模块获取已提交的日志条目,由 applierV3 模块通过 MVCC 模块执行提案内容,更新状态机。

整个过程中,etcd raft 模块中的 raftLog 数据在内存中存储,在服务重启后失效;客户端请求的数据则被持久化保存到 WAL 和 applierV3 中,不会在重启之后丢失。

小结

这一讲我们主要介绍了 etcd 整体的架构。首先通过 etcd 项目结构,介绍了各个包的用途,并介绍了其中核心的包;接着基于分层的方式,绘制了 etcd 分层架构图,结合图介绍了各个模块的作用;最后通过客户端写入 etcd 服务端的请求,理解 etcd 各个模块交互的过程。

本讲内容总结如下:

软考论文 论云原生架构及其应用 云原生架构进阶实战_软考论文 论云原生架构及其应用_39

本课时通过总览 etcd 的架构,将其重要的模块标识出来,如 etcd raft 模块、WAL、applierV3、Quota 等模块,也为我们下面具体介绍 etcd 的原理做一个铺垫。

学习完本课时,从上述 etcd 各个模块的交互过程,你知道有哪些方式保证了 etcd 写请求保证分布式一致性?欢迎你在留言区和我分享你的学习收获。

下一讲,我们将开始学习 etcd 的 API 接口层,看看 etcd 定义了哪些 API 接口并进行实践。

理模式:实现可伸缩的 etcd API”](https://kaiwu.lagou.com/course/courseInfo.htm?courseId=613&sid=20-h5Url-0#/detail/pc?id=6399) 具体做了讲解,这里我们就不再展开了。

  • etcd Raft 层:负责 Leader 选举和日志复制等功能,除了与本节点的 etcd Server 通信之外,还与集群中的其他 etcd 节点进行交互,实现分布式一致性数据同步的关键工作。
  • 逻辑层:etcd 的业务逻辑层,包括鉴权、租约、KVServer、MVCC 和 Compactor 压缩等核心功能特性。
  • etcd 存储:实现了快照、预写式日志 WAL(Write Ahead Log)。etcd V3 版本中,使用 BoltDB 来持久化存储集群元数据和用户写入的数据。

下面我们看一下 etcd 各个模块之间的交互过程。

etcd 交互总览

我们通过学习 etcd 服务端处理客户端的写请求的过程,展示 etcd 内部各个模块之间的交互。首先通过命令行工具 etcdctl 写入键值对:

etcdctl --endpoints http:

下图中展示了 etcd 处理一个客户端请求涉及的模块和流程。

[外链图片转存中…(img-QreKdfKp-1657696928622)]

从上至下依次为客户端 → API 接口层 → etcd Server → etcd raft 算法库。我们根据请求处理的过程,将 etcd Server 和 etcd raft 算法库单独说明。

  • etcd Server:接收客户端的请求,在上述的 etcd 项目代码中对应 etcdserver 包。请求到达 etcd Server 之后,经过 KVServer 拦截,实现诸如日志、Metrics 监控、请求校验等功能。etcd Server 中的 raft 模块,用于与 etcd-raft 库进行通信。applierV3 模块封装了 etcd V3 版本的数据存储;WAL 用于写数据日志,WAL 中保存了任期号、投票信息、已提交索引、提案内容等,etcd 根据 WAL 中的内容在启动时恢复,以此实现集群的数据一致性。
  • etcdraft:etcd 的 raft 库。raftLog 用于管理 raft 协议中单个节点的日志,都处于内存中。raftLog 中还有两种结构体 unstable 和 storage,unsable 中存储不稳定的数据,表示还没有 commit,而 storage 中都是已经被 commit 了的数据。这两种结构体分别用于不同步骤的存储,我们将在下面的交互流程中介绍。除此之外,raft 库更重要的是负责与集群中的其他 etcd Server 进行交互,实现分布式一致性。

在上图中,客户端请求与 etcd 集群交互包括如下两个步骤:

  • 首先是写数据到 etcd 节点中;
  • 其次是当前的 etcd 节点与集群中的其他 etcd 节点之间进行通信,确认存储数据成功之后回复客户端。

请求流程可划分为以下的子步骤:

[外链图片转存中…(img-lPSeXtRX-1657696928624)]

  • 客户端通过负载均衡算法选择一个 etcd 节点,发起 gRPC 调用;
  • etcd Server 收到客户端请求;
  • 经过 gRPC 拦截、Quota 校验,Quota 模块用于校验 etcd db 文件大小是否超过了配额;
  • 接着 KVServer 模块将请求发送给本模块中的 raft,这里负责与 etcd raft 模块进行通信;
  • 发起一个提案,命令为put foo bar,即使用 put 方法将 foo 更新为 bar;
  • 在 raft 中会将数据封装成 raft 日志的形式提交给 raft 模块;
  • raft 模块会首先保存到 raftLog 的 unstable 存储部分;
  • raft 模块通过 raft 协议与集群中其他 etcd 节点进行交互。

需要注意的是,在 raft 协议中写入数据的 etcd 必定是 leader 节点,如果客户端提交数据到非 leader 节点时,该节点需要将请求转发到 etcd leader 节点处理。

我们继续来看相应的应答步骤,流程如下:

[外链图片转存中…(img-PrfIAJ4M-1657696928624)]

  • 提案通过 RaftHTTP 网络模块转发,集群中的其他节点接收到该提案;
  • 在收到提案之后,集群中其他节点向 leader 节点应答 “我已经接收这条日志数据”;
  • Leader 收到应答之后,统计应答的数量,当满足超过集群半数以上节点,应答接收成功;
  • etcd raft 算法模块构造 Ready 结构体,用来通知 etcd Server 模块,该日志数据已经被 commit;
  • etcd Server 中的 raft 模块(交互图中有标识),收到 Ready 消息后,会将这条日志数据写入到 WAL 模块中;
  • 正式通知 etcd Server 该提案已经被 commit;
  • etcd Server 调用 applierV3 模块,将日志写入持久化存储中;
  • etcd Server 应答客户端该数据写入成功;
  • etcd Server 调用 etcd raft 库,将这条日志写入到 raftLog 模块中的 storage。

上述过程中,提案经过网络转发,当多数 etcd 节点持久化日志数据成功并进行应答,提案的状态会变成已提交。

在应答某条日志数据是否已经 commit 时,为什么 etcd raft 模块首先写入到 WAL 模块中?这是因为该过程仅仅添加一条日志,一方面开销小,速度会很快;另一方面,如果在后面 applierV3 写入失败,etcd 服务端在重启的时候也可以根据 WAL 模块中的日志数据进行恢复。etcd Server 从 raft 模块获取已提交的日志条目,由 applierV3 模块通过 MVCC 模块执行提案内容,更新状态机。

整个过程中,etcd raft 模块中的 raftLog 数据在内存中存储,在服务重启后失效;客户端请求的数据则被持久化保存到 WAL 和 applierV3 中,不会在重启之后丢失。

小结

这一讲我们主要介绍了 etcd 整体的架构。首先通过 etcd 项目结构,介绍了各个包的用途,并介绍了其中核心的包;接着基于分层的方式,绘制了 etcd 分层架构图,结合图介绍了各个模块的作用;最后通过客户端写入 etcd 服务端的请求,理解 etcd 各个模块交互的过程。

本讲内容总结如下:

[外链图片转存中…(img-Pzv2zblS-1657696928626)]

本课时通过总览 etcd 的架构,将其重要的模块标识出来,如 etcd raft 模块、WAL、applierV3、Quota 等模块,也为我们下面具体介绍 etcd 的原理做一个铺垫。

学习完本课时,从上述 etcd 各个模块的交互过程,你知道有哪些方式保证了 etcd 写请求保证分布式一致性?欢迎你在留言区和我分享你的学习收获。

下一讲,我们将开始学习 etcd 的 API 接口层,看看 etcd 定义了哪些 API 接口并进行实践。