目录

文章目录

  • 目录
  • etcd
  • etcd vs ZooKeeper vs Consul
  • etcd 的应用场景
    • 服务注册与发现
    • 消息发布与订阅
    • 分布式通知与协调
    • 分布式锁
    • 分布式队列
    • 集群监控
    • Leader 竞选
    • 负载均衡
  • etcd 的安装与部署
    • 服务进程
    • 单点部署
      • 编译部署
      • 容器部署
    • 集群部署
      • 静态配置集群
      • 集群健康检测
  • etcd 的架构原理
    • etcd 的核心术语
    • etcd 的 K/V 存储
    • etcd 的软件架构
      • Raft
        • Raft 中一个 Term(任期)是什么意思?
        • Raft 状态机是怎样切换的?
        • 如何保证最短时间内竞选出 Leader,防止竞选冲突?
        • 如何防止别的 Candidate 在遗漏部分数据的情况下发起投票成为 Leader?
        • Raft 某个节点宕机后会如何?
        • 为什么 Raft 算法在确定可用节点数量时不需要考虑拜占庭将军问题?
        • 客户端从集群中的哪个节点读写数据?
        • 如何保证数据一致性?
        • 如何选举 Leader 节点?
        • 如何判断写入是否成功?
        • 如何确定 etcd Cluster 的节点数?
        • etcd 实现的 Raft 算法性能如何?
      • Store
      • WAL
        • 为什么需要 Snapshot(快照)?
        • WAL 和 Snapshot 的命名规则?
    • etcd 的数据模型
      • 逻辑视图
      • 物理视图
    • etcd 的 Proxy 模式
      • Proxy 模式取代 Standby 模式的原因?
  • etcd 的基本操作
    • 与 etcd 交互
    • etcdctl 交互
      • Put(创建/更新)
      • Get(查询)
      • Del(删除)
      • Watch(监控)
      • Lock(分布式锁)
      • Transactions(事务)
      • Compact(压缩)
      • Lease(租约)
    • API 交互
      • Authentication
      • Put and get keys
      • Watch keys
      • Transactions
    • Golang Client SDK 交互

etcd

etcd 是 CoreOS 团队于 2013 年 6 月发起的使用 Golang 开发的开源项目,基于 Raft 强一致性算法,它的目标是构建一个高可用,且数据强一致性的分布式键值(key/value)数据库,专注于配置共享(shared configuration)和服务发现(service discovery)。etcd 目前更新至 v3 版本,已被用在 CoreOS、Kubernetes、Cloud Foundry 等项目中。

A distributed, reliable key-value store for the most critical data of a distributed system

  • 官方网页:https://etcd.io/
  • Github:https://github.com/etcd-io/etcd

etcd 作为一个受到 ZooKeeper 启发而催生的项目,它除了拥有与之类似的功能外,更专注于以下四点:

  1. 简单:易部署,易使用。提供 REST 和 gRPC API。
  2. 安全:支持可选的 SSL 安全认证。
  3. 快速:每个实例每秒支持 10,000 次写操作。
  4. 可靠:使用 Raft 算法保证了分布式的数据强一致性。

etcd 的经典应用场景在于存储控制系统中的元数据,所以 etcd 并不是其他 NoSQL 的替代品,更不作为应用数据的主要存储,etcd 中应尽量只存储系统中服务的配置信息。

etcd 提供了以下能力:

  • 供存储以及获取数据的接口:它通过协议保证 etcd 集群中的多个节点数据的强一致性。用于存储元信息以及共享配置。
  • 提供监听机制:客户端可以监听某个 key 或者某些 key 的变更,用于监听和推送变更。
  • 提供 key 的过期以及续约机制:客户端通过定时刷新来实现续约,用于集群监控以及服务注册发现
  • 提供原子的 CAS(Compare And Swap)和 CAD(Compare And Delete)支持:用于分布式锁以及 Leader 竞选。

etcd vs ZooKeeper vs Consul

Etcd,Zookeeper,Consul 这三个产品是经常被我们拿来进行选型,etcd 和 ZooKeeper 提供的能力非常相似,都是通用的一致性元信息存储,都提供 Watch 机制用于变更通知和分发,也都被分布式系统用来作为共享信息存储,在软件生态中所处的位置也几乎是一样的,可以互相替代。

ZooKeeper 寄托在 Apache 基金会,使用 Java 开发,提供 RPC 接口,最早从 Hadoop 项目中孵化出来,在分布式系统中得到广泛使用,例如:Hadoop、Solr、Kafka 和 Mesos 等。而 etcd 则是后起之秀,主要关注一致性协议、易用性、运维、安全等维度。相较之下,ZooKeeper 有着 “复杂”、“语言绑定”、“发展缓慢” 的缺点。下面列举两者的对比:

  • 一致性协议: etcd 使用 Raft 协议,Zookeeper 使用 ZAB 协议(类 PAXOS 协议),前者更易于理解,方便工程实现;
  • 数据存储:etcd 支持多版本并发控制(MVCC)数据模型,支持查询先前版本的键值对。
  • 运维:etcd 易于运维,Zookeeper 则难以运维;
  • 安全:etcd 支持 HTTPS 协议,Zookeeper 则在这方面有缺失;
  • API:etcd 提供 HTTP+JSON 和 gRPC API,跨平台跨语言,Zookeeper 则需要使用其客户端;

而 Consul 的目标则更为具体一些,etcd 和 ZooKeeper 提供的是分布式一致性存储能力,具体的业务场景需要用户自己实现,比如:服务发现、配置变更。而 Consul 则以服务发现和配置变更为主要目标,同时附带了 key/value 存储。在软件生态中,越抽象的组件适用范围越广,但同时对具体业务场景需求的满足上肯定有不足之处。

etcd 的应用场景

服务注册与发现

服务发现是分布式系统最常见的问题之一,即:“在同一个分布式集群中的进程或服务,要如何才能找到对方并建立连接?” 的问题。简而言之,就是集群中的进程或服务需要让别人发现自己,相对的自己也要发现别人,并且可以通过彼此的名字来进行查找和连接。

要解决服务发现的问题,需要下述三大支柱,缺一不可:

  1. 一个强一致性、高可用的服务存储目录(Service Registry):基于 Raft 算法的 etcd 天生就是这样一个强一致性高可用的服务存储目录。
  2. 一种注册服务和监控服务健康状态的机制:用户可以在 etcd 中注册服务,并且对注册的服务设置 key TTL,定时保持服务的心跳以达到监控健康状态的效果。
  3. 一种查找和连接服务的机制:通过在 etcd 指定的主题(Topic)下注册的服务也能在对应的主题下查找到。为了确保连接,我们可以在每个服务机器上都部署一个 Proxy 模式的 etcd,这样就可以确保能访问 etcd 集群的服务都能互相连接。

  • 微服务架构中的服务动态添加:微服务架构,即:多个微服务共同协作构成一个功能强大的架构。在微服务架构中,如何透明化的动态添加服务是首要解决的问题。通过服务发现机制,在 etcd 中注册某个服务名字的目录(Catalog),在该目录下存储可用的服务节点的 IP 地址。在使用服务的过程中,只要从服务目录下查找可用的服务节点去使用即可。

  • PaaS 平台中的实例故障重启透明化:PaaS 平台中的应用一般是多实例的,客户端通过域名(Domain)就可以透明的对这多个实例进行访问,还可以做到负载均衡。但是应用的某个实例随时可能发生故障重启,这时就需要动态的更新域名解析配置。通过 etcd 的服务发现功能可以轻松解决这个动态配置的问题。

消息发布与订阅

在分布式系统中,消息发布与订阅是一种适用强的组件(服务)间通信方式。即:构建一个配置共享中心,消息生产者(Producer)在这个配置中心发布消息,而消息消费者(Consumer)则订阅他们关心的主题(Topic),一旦主题有消息发布,就会实时通知订阅者(Subscriber)。通过这种方式可以做到分布式系统配置的集中式管理与动态更新。

  • 应用使用到的配置信息存储到 etcd 上进行集中管理(配置共享中心):应用在启动的时候主动从 etcd 获取一次配置信息,同时,在 etcd 上注册一个 Watcher 并等待(完成订阅),以后每次配置有更新的时候,etcd 都会实时通知订阅者,以此达到获取最新配置信息的目的。

  • 分布式搜索服务中,索引的元信息和服务器集群机器的节点状态存放在 etcd 中,供各个客户端订阅使用:使用 etcd 的 key TTL 功能可以确保机器状态是实时更新的。

  • 分布式日志收集系统:这个系统的核心工作是收集分布在不同机器的日志。收集器通常是按照应用或主题来分配收集任务单元,因此可以在 etcd 上创建一个以应用或主题命名的目录,并将这个应用或主题相关的所有机器 IP 地址以子目录的形式存储到目录上,然后设置一个 etcd 递归的 Watcher,递归式的监控应用或主题的目录下所有信息的变动。这样就实现了机器 IP(消息)变动的时候,能够实时通知到收集器调整任务分配。

  • 系统中信息需要动态自动获取与人工干预修改信息请求内容的情况:通常是暴露出接口,例如 JMX 接口,来获取一些运行时的信息。引入 etcd 之后,就不用自己实现一套方案了,只要将这些信息存放到指定的 etcd 目录中即可,etcd 的这些目录就可以通过 HTTP 的接口在外部进行访问。

分布式通知与协调

分布式系统的通知与协调,与消息发布和订阅有些相似。都用到了 etcd 中的 Watcher 机制,通过注册与异步通知机制,实现分布式环境下不同系统之间的通知与协调,从而对数据变更做到实时处理。区别在于,前者服务于分布式系统本身,后者服务于上层应用级别。

实现方式通常是这样:不同系统都在 etcd 上对同一个目录进行注册,同时设置 Watcher 观测该目录的变化(如果对子目录的变化也有需要,可以设置递归模式),当某个系统更新了 etcd 的目录,那么设置了 Watcher 的系统就会收到通知,并作出相应处理。

  • 通过 etcd 进行低耦合的心跳检测:检测系统和被检测系统通过 etcd 上某个目录关联而非直接关联起来,这样可以大大减少系统的耦合性。

  • 通过 etcd 完成系统调度:某系统由控制台和推送系统两部分组成,控制台的职责是控制推送系统进行相应的推送工作。管理人员在控制台作的一些操作,实际上是修改了 etcd 上某些目录节点的状态,而 etcd 就把这些变化通知给注册了 Watcher 的推送系统客户端,推送系统再作出相应的推送任务。

  • 通过 etcd 完成工作汇报:大部分类似的任务分发系统,子任务启动后,到 etcd 来注册一个临时工作目录,并且定时将自己的进度进行汇报(将进度写入到这个临时目录),这样任务管理者就能够实时知道任务进度。

分布式锁

因为 etcd 使用 Raft 算法保证了数据的强一致性,某次操作存储到集群中的值必然是全局一致的,所以很容易用于实现分布式锁。锁服务有两种使用方式,一是保持独占,二是控制时序。

  1. 保持独占:即独占锁,所有获取锁的用户最终只有一个可以得到。etcd 为此提供了一套实现分布式锁原子操作 CAS(Compare And Swap)的 API。通过设置 prevExist 值,可以保证在多个节点同时去创建某个目录时,只有一个成功。而创建成功的用户就可以认为是获得了锁。

  2. 控制时序:即时序锁,所有想要获得锁的用户都会被安排执行,但是获得锁的顺序也是全局唯一的,同时决定了执行顺序。etcd 为此也提供了一套 API(自动创建有序键),对一个目录建值时指定为 POST 动作,这样 etcd 会自动在目录下生成一个当前最大的值为键,存储这个新的值(客户端编号)。同时还可以使用 API 按顺序列出所有当前目录下的键值。此时这些键的值就是客户端的时序,而这些键中存储的值可以是代表客户端的编号。

分布式队列

分布式队列与上述提到的分布式时序锁的用法类似,即:创建一个先进先出的队列,并保证顺序。另一种比较有意思的实现是在保证队列达到某个条件时再统一按顺序执行。这种方法的实现可以在 /queue 这个目录中另外建立一个 /queue/condition 节点。

  • condition 可以表示队列大小:比如一个大的任务需要很多小任务就绪的情况下才能执行,每次有一个小任务就绪,就给这个 condition 数字加 1,直到达到大任务规定的数字,再开始执行队列里的一系列小任务,最终执行大任务。
  • condition 可以表示某个任务在不在队列:这个任务可以是所有排序任务的首个执行程序,也可以是拓扑结构中没有依赖的点。通常,必须执行这些任务后才能执行队列中的其他任务。
  • condition 可以表示其它的一类开始执行任务的通知:可以由控制程序指定,当 condition 出现变化时,开始执行队列任务。

集群监控

通过 etcd 来实现集群监控非常简单且实时性强。应用了 etcd 的两大功能:

  1. Watcher 机制:当某个节点消失或有变动时,Watcher 会第一时间发现并告知用户。
  2. key TTL 机制:比如每隔 30s 发送一次心跳使代表该机器存活的节点继续存在,否则节点消失。

这样就可以第一时间检测到各节点的健康状态,以完成集群监控的要求。

Leader 竞选

另外,利用分布式锁,可以简单的实现集群多节点的 Leader 竞选。这种场景通常是一些长时间的 CPU 计算或者使用 I/O 操作的机器,只需要竞选出的 Leader 计算或处理一次,就可以把结果复制给其他的 Follower。从而避免重复劳动,节省计算资源。

经典场景就是在搜索系统中建立全量索引:如果每个机器都进行一遍索引的建立,不但耗时而且建立索引的一致性不能保证。通过在 etcd 的 CAS 机制同时创建一个节点,创建成功的机器作为 Leader,进行索引计算,然后把计算结果分发到其它节点。

负载均衡

通过 etcd 可以实现一个软负载均衡器,体现在两个方面:

  1. etcd 本身分布式架构存储的信息访问支持负载均衡:etcd 集群化以后,每个 etcd 的核心节点都可以处理用户的请求。所以,虽然 etcd 更多的存储控制系统的元数据,但把数据量小且访问频繁的消息数据直接存储到 etcd 中也是个不错的选择,如:业务系统中常用的二级代码表(在表中存储代码,在 etcd 中存储代码所代表的具体含义,业务系统调用查表的过程,就需要查找表中代码的含义)。
  2. 利用 etcd 维护一个负载均衡节点表:etcd 可以监控一个集群中多个节点的状态,当有一个请求发过来后,可以轮询式的把请求转发给存活着的多个状态。类似 KafkaMQ 通过 ZooKeeper 来维护生产者和消费者的负载均衡。同样也可以用 etcd 来做 ZooKeeper 的工作。

etcd 的安装与部署

服务进程

etcd 目前默认使用 2379 端口提供 HTTP API 服务,2380 端口提供 Peer 通信(这两个端口已经被 IANA 官方预留给 etcd),在之前的版本中,可能会分别使用 4001 和 7001,在使用的过程中需要注意这个区别。

虽然 etcd 也支持单点部署,但是在生产环境中推荐集群方式部署,一般 etcd 节点数会选择 3、5、7。etcd 会保证所有的节点都会保存数据,并保证数据的一致性和正确性。

单点部署

编译部署

因为 etcd 是 Golang 编写的,安装只需要下载对应的二进制文件,并放到合适的路径就行。如果在测试环境,启动一个单点的 etcd 服务,只需要运行 etcd 执行即可。

git clone https://github.com/etcd-io/etcd.git
cd etcd
./build

使用 build 脚本构建会在当前项目的 bin 目录生产 etcd 和 etcdctl 可执行程序。etcd 就是 etcd Server,而 etcdctl 主要为 etcd Server 提供指令行操作。

查看版本:

$ ./bin/etcd --version
etcd Version: 3.5.0-pre
Git SHA: ab4cc3cae
Go Version: go1.14.4
Go OS/Arch: darwin/amd64$ ./bin/etcdctl version
etcdctl version: 3.5.0-pre
API version: 3.5

启动 etcd Server:

$ ./bin/etcd
{"level":"info","ts":"2020-10-04T07:39:14.751+0800","caller":"etcdmain/etcd.go:69","msg":"Running: ","args":["./bin/etcd"]}
{"level":"info","ts":"2020-10-04T07:39:14.751+0800","caller":"etcdmain/etcd.go:94","msg":"failed to detect default host","error":"default host not supported on darwin_amd64"}
{"level":"warn","ts":"2020-10-04T07:39:14.751+0800","caller":"etcdmain/etcd.go:99","msg":"'data-dir' was empty; using default","data-dir":"default.etcd"}
{"level":"info","ts":"2020-10-04T07:39:14.751+0800","caller":"embed/etcd.go:113","msg":"configuring peer listeners","listen-peer-urls":["http://localhost:2380"]}
{"level":"info","ts":"2020-10-04T07:39:14.752+0800","caller":"embed/etcd.go:121","msg":"configuring client listeners","listen-client-urls":["http://localhost:2379"]}
{"level":"info","ts":"2020-10-04T07:39:14.753+0800","caller":"embed/etcd.go:266","msg":"starting an etcd server","etcd-version":"3.5.0-pre","git-sha":"ab4cc3cae","go-version":"go1.14.4","go-os":"darwin","go-arch":"amd64","max-cpu-set":4,"max-cpu-available":4,"member-initialized":false,"name":"default","data-dir":"default.etcd","wal-dir":"","wal-dir-dedicated":"","member-dir":"default.etcd/member","force-new-cluster":false,"heartbeat-interval":"100ms","election-timeout":"1s","initial-election-tick-advance":true,"snapshot-count":100000,"snapshot-catchup-entries":5000,"initial-advertise-peer-urls":["http://localhost:2380"],"listen-peer-urls":["http://localhost:2380"],"advertise-client-urls":["http://localhost:2379"],"listen-client-urls":["http://localhost:2379"],"listen-metrics-urls":[],"cors":["*"],"host-whitelist":["*"],"initial-cluster":"default=http://localhost:2380","initial-cluster-state":"new","initial-cluster-token":"etcd-cluster","quota-size-bytes":2147483648,"pre-vote":false,"initial-corrupt-check":false,"corrupt-check-time-interval":"0s","auto-compaction-mode":"periodic","auto-compaction-retention":"0s","auto-compaction-interval":"0s","discovery-url":"","discovery-proxy":""}
{"level":"info","ts":"2020-10-04T07:39:14.764+0800","caller":"etcdserver/backend.go:78","msg":"opened backend db","path":"default.etcd/member/snap/db","took":"9.908726ms"}
{"level":"info","ts":"2020-10-04T07:39:14.862+0800","caller":"etcdserver/raft.go:444","msg":"starting local member","local-member-id":"8e9e05c52164694d","cluster-id":"cdf818194e3a8c32"}
{"level":"info","ts":"2020-10-04T07:39:14.862+0800","caller":"raft/raft.go:1528","msg":"8e9e05c52164694d switched to configuration voters=()"}
{"level":"info","ts":"2020-10-04T07:39:14.862+0800","caller":"raft/raft.go:701","msg":"8e9e05c52164694d became follower at term 0"}
{"level":"info","ts":"2020-10-04T07:39:14.862+0800","caller":"raft/raft.go:383","msg":"newRaft 8e9e05c52164694d [peers: [], term: 0, commit: 0, applied: 0, lastindex: 0, lastterm: 0]"}
{"level":"info","ts":"2020-10-04T07:39:14.863+0800","caller":"raft/raft.go:701","msg":"8e9e05c52164694d became follower at term 1"}
{"level":"info","ts":"2020-10-04T07:39:14.863+0800","caller":"raft/raft.go:1528","msg":"8e9e05c52164694d switched to configuration voters=(10276657743932975437)"}
{"level":"warn","ts":"2020-10-04T07:39:14.888+0800","caller":"auth/store.go:1231","msg":"simple token is not cryptographically signed"}
{"level":"info","ts":"2020-10-04T07:39:14.912+0800","caller":"etcdserver/quota.go:94","msg":"enabled backend quota with default value","quota-name":"v3-applier","quota-size-bytes":2147483648,"quota-size":"2.1 GB"}
{"level":"info","ts":"2020-10-04T07:39:14.924+0800","caller":"etcdserver/server.go:752","msg":"starting etcd server","local-member-id":"8e9e05c52164694d","local-server-version":"3.5.0-pre","cluster-version":"to_be_decided"}
{"level":"info","ts":"2020-10-04T07:39:14.925+0800","caller":"etcdserver/server.go:640","msg":"started as single-node; fast-forwarding election ticks","local-member-id":"8e9e05c52164694d","forward-ticks":9,"forward-duration":"900ms","election-ticks":10,"election-timeout":"1s"}
{"level":"warn","ts":"2020-10-04T07:39:14.925+0800","caller":"etcdserver/metrics.go:212","msg":"failed to get file descriptor usage","error":"cannot get FDUsage on darwin"}
{"level":"info","ts":"2020-10-04T07:39:14.925+0800","caller":"raft/raft.go:1528","msg":"8e9e05c52164694d switched to configuration voters=(10276657743932975437)"}
{"level":"info","ts":"2020-10-04T07:39:14.925+0800","caller":"membership/cluster.go:385","msg":"added member","cluster-id":"cdf818194e3a8c32","local-member-id":"8e9e05c52164694d","added-peer-id":"8e9e05c52164694d","added-peer-peer-urls":["http://localhost:2380"]}
{"level":"info","ts":"2020-10-04T07:39:14.927+0800","caller":"embed/etcd.go:513","msg":"serving peer traffic","address":"127.0.0.1:2380"}
{"level":"info","ts":"2020-10-04T07:39:14.927+0800","caller":"embed/etcd.go:235","msg":"now serving peer/client/metrics","local-member-id":"8e9e05c52164694d","initial-advertise-peer-urls":["http://localhost:2380"],"listen-peer-urls":["http://localhost:2380"],"advertise-client-urls":["http://localhost:2379"],"listen-client-urls":["http://localhost:2379"],"listen-metrics-urls":[]}
{"level":"info","ts":"2020-10-04T07:39:15.866+0800","caller":"raft/raft.go:788","msg":"8e9e05c52164694d is starting a new election at term 1"}
{"level":"info","ts":"2020-10-04T07:39:15.866+0800","caller":"raft/raft.go:714","msg":"8e9e05c52164694d became candidate at term 2"}
{"level":"info","ts":"2020-10-04T07:39:15.866+0800","caller":"raft/raft.go:848","msg":"8e9e05c52164694d received MsgVoteResp from 8e9e05c52164694d at term 2"}
{"level":"info","ts":"2020-10-04T07:39:15.867+0800","caller":"raft/raft.go:766","msg":"8e9e05c52164694d became leader at term 2"}
{"level":"info","ts":"2020-10-04T07:39:15.867+0800","caller":"raft/node.go:327","msg":"raft.node: 8e9e05c52164694d elected leader 8e9e05c52164694d at term 2"}
{"level":"info","ts":"2020-10-04T07:39:15.868+0800","caller":"etcdserver/server.go:2285","msg":"setting up initial cluster version","cluster-version":"3.5"}
{"level":"info","ts":"2020-10-04T07:39:15.876+0800","caller":"membership/cluster.go:523","msg":"set initial cluster version","cluster-id":"cdf818194e3a8c32","local-member-id":"8e9e05c52164694d","cluster-version":"3.5"}
{"level":"info","ts":"2020-10-04T07:39:15.876+0800","caller":"embed/serve.go:97","msg":"ready to serve client requests"}
{"level":"info","ts":"2020-10-04T07:39:15.876+0800","caller":"api/capability.go:75","msg":"enabled capabilities for version","cluster-version":"3.5"}
{"level":"info","ts":"2020-10-04T07:39:15.876+0800","caller":"etcdserver/server.go:2305","msg":"cluster version is updated","cluster-version":"3.5"}
{"level":"info","ts":"2020-10-04T07:39:15.876+0800","caller":"etcdserver/server.go:1863","msg":"published local member to cluster through raft","local-member-id":"8e9e05c52164694d","local-member-attributes":"{Name:default ClientURLs:[http://localhost:2379]}","request-path":"/0/members/8e9e05c52164694d/attributes","cluster-id":"cdf818194e3a8c32","publish-timeout":"7s"}
{"level":"info","ts":"2020-10-04T07:39:15.876+0800","caller":"etcdmain/main.go:47","msg":"notifying init daemon"}
{"level":"info","ts":"2020-10-04T07:39:15.876+0800","caller":"etcdmain/main.go:53","msg":"successfully notified init daemon"}
{"level":"info","ts":"2020-10-04T07:39:15.877+0800","caller":"embed/serve.go:139","msg":"serving client traffic insecurely; this is strongly discouraged!","address":"127.0.0.1:2379"}
  • name 表示节点名称,默认为 default。
  • data-dir 表示 WAL 日志和 Snapshot 数据储存目录,默认为 ./default.etcd/ 目录。
  • 使用 http://localhost:2380 和 etcd Cluster 中其他节点通信。
  • 使用 http://localhost:2379 提供 HTTP API 服务,与客户端通信。
  • heartbeat 为 100ms,表示 Leader 多久发送一次心跳到所有 Followers。
  • election-timeout 为 1s,该参数的作用是重新投票的超时时间,如果 Follow 在该时间间隔内没有收到 Leader 发出的心跳包,就会触发重新投票。
  • snapshot-count 为 100000,该参数的作用是指定有多少次事务被提交后触发快照截取动作并持久化到磁盘。
  • cluster-id 为 cdf818194e3a8c32。
  • raft.node 为 8e9e05c52164694d。
  • 启动的时候,会运行 Raft,选举出 Leader:elected leader 8e9e05c52164694d at term 2

上述方法只是简单的启动了一个 etcd Server。当然,在生产环境中,通常使用 Systemd 来进行管理。

  • 建立相关目录:
$ mkdir -p /var/lib/etcd/
$ mkdir -p /etc/etcd/config/
  • 设定 etcd 配置文件:
$ cat <<EOF| sudo tee /etc/etcd/config/etcd.conf
# 节点名称
ETCD_NAME=$(hostname -s)
# 数据存放路径
ETCD_DATA_DIR=/var/lib/etcd
EOF
  • 创建 systemd 配置文件:
$ cat <<EOF| sudo tee /etc/systemd/system/etcd.service[Unit]
Description=Etcd Server
Documentation=https://github.com/coreos/etcd
After=network.target[Service]
User=root
Type=notify
EnvironmentFile=-/opt/etcd/config/etcd.conf
ExecStart=~/workspace/etcd/bin
Restart=on-failure
RestartSec=10s
LimitNOFILE=40000[Install]
WantedBy=multi-user.target
EOF
  • 启动 etcd Server:
$ systemctl daemon-reload && systemctl enable etcd && systemctl start etcd

关键启动选型清单:

  • –name:指定 etcd Node 名称,可以使用 hostname。
  • –data-dir:指定 etcd Server 持久化数据存储目录路径。
  • –snapshot-count:指定有多少事务(transaction)被提交后,触发截取快照并持久化到磁盘。
  • –heartbeat-interval:指定 Leader 多久发送一次心跳到 Followers。
  • –eletion-timeout:指定重新投票的超时时间,如果 Follow 在该时间间隔没有收到 Leader 发出的心跳包,则会触发重新投票。
  • –listen-peer-urls:指定和 Cluster 其他 Node 通信的地址,比如:http://IP:2380,如果有多个,则使用逗号分隔。需要所有节点都能够访问,所以不要使用 localhost。
  • –listen-client-urls:指定对外提供服务的地址,比如:http://IP:2379,http://127.0.0.1:2379。
  • –advertise-client-urls:对外通告的该节点的客户端监听地址,会告诉集群中其他节点。
  • –initial-advertise-peer-urls:对外通告该节点的同伴(Peer)监听地址,这个值会告诉集群中其他节点。
  • –initial-cluster:指定集群中所有节点的信息,通常为 IP:Port 信息,格式为:node1=http://ip1:2380,node2=http://ip2:2380,…。注意,这里的 node1 就是 --name 指定的名字,ip1:2380 就是 --initial-advertise-peer-urls 指定的值。
  • –initial-cluster-state:新建集群时,这个值为 new;假如已经存在了集群,这个值为 existing。
  • –initial-cluster-token:创建集群的 token,这个值每个集群保持唯一。这样的话,如果你要重新创建集群,即使配置和之前一样,也会再次生成新的集群和节点 UUID;否则会导致多个集群之间的冲突,造成未知的错误。

容器部署

  • docker run on console
$ docker run --rm --name etcd -p 2379:2379 -e ETCDCTL_API=3 quay.io/coreos/etcd /usr/local/bin/etcd -advertise-client-urls http://0.0.0.0:2379 -listen-client-urls http://0.0.0.0:2379
$ docker ps -f name=etcd
  • etcdctl
$ docker exec -it etcd etcdctl version
etcdctl version: 3.3.8
API version: 3.3$ docker exec -it etcd etcdctl endpoint health
127.0.0.1:2379 is healthy: successfully committed proposal: took = 571.723µs

集群部署

etcd Cluster 的部署,实际上就是多个主机上都部署 etcd Server,然后将它们加入到一个 Cluster 中。

注意:etcd Cluster 必须具有时间同步服务器,否则会导致 Cluster 频繁进行 Leader Change。在 OpenShift 的 etcd Cluster 中,会每隔 100ms 进行心跳检测。

在安装和启动 etcd 服务进程的时候,各个 Node 都需要知道 Cluster 中其他 Nodes 的信息,一般是 IP:Port 信息。根据用户是否提前知晓(规划)了每个 Node 的 IP 地址,有以下几种不同的集群部署方案:

  1. 静态配置:在启动 etcd Server 的时候,通过 --initial-cluster 参数配置好所有的节点信息。
  2. 注册到已有的 etcd Cluster:比如官方提供的 discovery.etcd.io。
  3. 使用 DNS 启动

静态配置集群

如果 etcd Cluster 中的成员是已知的,且具有固定的 IP 地址,就可以静态的初始化一个集群。

每个 Node 都使用如下环境变量:

ETCD_INITIAL_CLUSTER="radon=http://10.0.2.1:2380,neon=http://10.0.3.1:2380"
ETCD_INITIAL_CLUSTER_STATE=new

或者使用如下指令行参数来指定集群成员:

--initial-cluster radon=http://10.0.2.1:2380,neon=http://10.0.3.1:2380
--initial-cluster-state new

初始化集群:

etcd --name radon --initial-advertise-peer-urls http://10.0.2.1:2380--listen-peer-urls http://10.0.2.1:2380--listen-client-urls http://10.0.2.1:2379,http://127.0.0.1:2379--advertise-client-urls http://10.0.2.1:2380--initial-cluster-token etcd.gmem.cc--initial-cluster radon=http://10.0.2.1:2380,neon=http://10.0.3.1:2380--initial-cluster-state new

注:所有以 --initial-cluster* 开头的选项,在第一次运行(Bootstrap)后都被忽略。

使用 TLS 加密,etcd 支持基于 TLS 加密的集群内部、集群外部(客户端与集群之间)的安全通信,每个集群节点都应该拥有被共享 CA 签名的证书:

# 密钥对、证书签名请求
openssl genrsa -out radon.key 2048
export SAN_CFG=$(printf "\n[SAN]\nsubjectAltName=IP:127.0.0.1,IP:10.0.2.1,DNS:radon.gmem.cc")
openssl req -new -sha256 -key radon.key -out radon.csr \-subj "/C=CN/ST=BeiJing/O=Gmem Studio/CN=Server Radon" \-reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(echo $SAN_CFG))# 执行签名
openssl x509 -req -sha256 -in radon.csr  -out radon.crt -CA ../ca.crt -CAkey ../ca.key -CAcreateserial -days 3650 \-extensions SAN -extfile <(echo "${SAN_CFG}")

初始化集群命令需要修改为:

etcd --name radon --initial-advertise-peer-urls https://10.0.2.1:2380--listen-peer-urls https://10.0.2.1:2380--listen-client-urls https://10.0.2.1:2379,https://127.0.0.1:2379--advertise-client-urls https://10.0.2.1:2380--initial-cluster-token etcd.gmem.cc--initial-cluster radon=https://10.0.2.1:2380,neon=https://10.0.3.1:2380      # 指定集群成员列表--initial-cluster-state new                                                                              # 初始化新集群时使用--initial-cluster-state existing                                                                        # 加入已有集群时使用# 客户端 TLS 相关参数--client-cert-auth --trusted-ca-file=/usr/share/ca-certificates/GmemCA.crt--cert-file=/opt/etcd/cert/radon.crt--key-file=/opt/etcd/cert/radon.key# 集群内部 TLS 相关参数--peer-client-cert-auth--peer-trusted-ca-file=/usr/share/ca-certificates/GmemCA.crt--peer-cert-file=/opt/etcd/cert/radon.crt--peer-key-file=/opt/etcd/cert/radon.key

集群健康检测

ETCDCTL_API=3 /k8s/etcd/bin/etcdctl  endpoint health --write-out=table \--cacert=/k8s/kubernetes/ssl/ca.pem \--cert=/k8s/kubernetes/ssl/server.pem \--key=/k8s/kubernetes/ssl/server-key.pem \--endpoints=https://192.168.0.108:2379,https://192.168.0.109:2379,https://192.168.0.110:2379

etcd 的架构原理

etcd 的核心术语

  • Raft:etcd 所采用的保证分布式系统数据强一致性的算法。
  • Node:一个 Raft 状态机实例。
  • Member:一个 etcd 实例,它管理着一个 Node,并且可以为客户端请求提供服务。
  • Cluster:由多个 Member 构成可以协同工作的 etcd 集群。
  • Peer:对同一个 etcd 集群中另外一个 Member 的称呼。
  • Client:向 etcd 集群发送 HTTP 请求的客户端。
  • WAL:预写式日志,etcd 用于持久化存储的日志格式。
  • Snapshot:etcd 防止 WAL 文件过多而设置的快照,存储 etcd 数据状态。
  • Entry:Raft 算法中的日志的一个条目。
  • Proxy:etcd 的一种模式,为 etcd 集群提供反向代理服务。
  • Leader:Raft 算法中通过竞选而产生的处理所有数据提交的节点。
  • Follower:Raft 算法中竞选失败的节点作为从属节点,为算法提供强一致性保证。
  • Candidate:当 Follower 超过一定时间接收不到 Leader 的心跳时(认为 Leader 发生了故障)转变为 Candidate 开始竞选。
  • Term:某个节点成为 Leader 到下一次竞选时间,称为一个 Term。
  • Vote:选举时的一张投票。
  • Index:数据项编号,Raft 中通过 Term 和 Index 来定位数据。
  • Commit:一个提交,持久化数据写入到日志中。
  • Propose:一个提议,请求大部分 Node 同意数据写入。

etcd 的 K/V 存储

etcd Server 采用树形的结构来组织储存数据,类似 Linux 的文件系统,也有目录和文件的分层结构,不过一般被称为 nodes。

例如:用户指定的 key 可以为单独的名字,如:testkey,此时 key testkey 实际上存放在根目录 “/” 下面。也可以为指定目录结构,如:/testdir/testkey,则将创建相应的目录结构。

etcdctl set /testdir/testkey "Hello world"

etcd 的软件架构

  • HTTP Server:接受客户端发出的 API 请求以及其它 etcd 节点的同步与心跳信息请求。

  • Store:用于处理 etcd 支持的各类功能的事务,包括数据索引、节点状态变更、监控与反馈、事件处理与执行等等,是 etcd 对用户提供的大多数 API 功能的具体实现。

  • Raft:强一致性算法的具体实现,是 etcd 的核心算法。

  • WAL(Write Ahead Log,预写式日志):是 etcd 的数据存储方式,etcd 会在内存中储存所有数据的状态以及节点的索引,此外,etcd 还会通过 WAL 进行持久化存储。WAL 中,所有的数据提交前都会事先记录日志。

    • Snapshot 是为了防止数据过多而进行的状态快照;
    • Entry 表示存储的具体日志内容。

通常,一个用户的请求发送过来,会经由 HTTP Server 转发给 Store 进行具体的事务处理,如果涉及到节点数据的修改,则交给 Raft 模块进行状态的变更、日志的记录,然后再同步给别的 etcd 节点以确认数据提交,最后进行数据的提交,再次同步。

Raft

新版本的 etcd 实现,Raft 包就是 Raft 一致性算法的具体实现。

Raft 中一个 Term(任期)是什么意思?

Raft 算法中,从时间上,一个 Term(任期)即从一次竞选开始到下一次竞选开始之间。从功能上讲,如果 Follower 接收不到 Leader 的心跳信息,就会结束当前 Term,变为 Candidate 继而发起竞选,继而帮助 Leader 故障时集群的恢复。发起竞选投票时,Term Value 小的 Node 不会竞选成功。如果 Cluster 不出现故障,那么一个 Term 将无限延续下去。另外,投票出现冲突也有可能直接进入下一任再次竞选。

Raft 状态机是怎样切换的?

Raft 刚开始运行时,Node 默认进入 Follower 状态,等待 Leader 发来心跳信息。若等待超时,则状态由 Follower 切换到 Candidate 进入下一轮 Term 发起竞选,等到收到 Cluster 的 “多数节点” 的投票时,该 Node 转变为 Leader。Leader 有可能出现网络等故障,导致别的 Nodes 发起投票成为新 Term 的 Leader,此时原先的 Old Leader 会切换为 Follower。Candidate 在等待其它 Nodes 投票的过程中如果发现已经竞选成功了一个 Leader,那么也会切换为 Follower。

如何保证最短时间内竞选出 Leader,防止竞选冲突?

在 Raft 状态机一图中可以看到,在 Candidate 状态下, 有一个 times out,这里的 times out 时间是个随机值,也就是说,每个 Node 成为 Candidate 以后,times out 发起新一轮竞选的时间是各不相同的,这就会出现一个时间差。在时间差内,如果 Candidate1 收到的竞选信息比自己发起的竞选信息 Term Value 大(即对方为新一轮 Term),并且在新一轮想要成为 Leader 的 Candidate2 包含了所有提交的数据,那么 Candidate1 就会投票给 Candidate2。这样就保证了只有很小的概率会出现竞选冲突。

如何防止别的 Candidate 在遗漏部分数据的情况下发起投票成为 Leader?

Raft 竞选的机制中,使用随机值决定 times out,第一个超时的 Node 就会提升 Term 编号发起新一轮投票,一般情况下别的 Node 收到竞选通知就会投票。但是,如果发起竞选的 Node 在上一个 Term 中保存的已提交数据不完整,Node 就会拒绝投票给它。通过这种机制就可以防止遗漏数据的 Node 成为 Leader。

Raft 某个节点宕机后会如何?

通常情况下,如果是 Follower 宕机,如果剩余可用节点数量超过半数,Cluster 可以几乎没有影响的正常工作。如果宕机的是 Leader,那么 Follower 就收不到心跳而超时,发起竞选获得投票,成为新一轮 Term 的 Leader,继续为 Cluster 提供服务。

需要注意的是:etcd 目前没有任何机制会自动去变化整个 Cluster 的 Instances(总节点数量),即:如果没有人为的调用 API,etcd 宕机后的 Node 仍然被计算为总节点数中,任何请求被确认需要获得的投票数都是这个总数的半数以上。

为什么 Raft 算法在确定可用节点数量时不需要考虑拜占庭将军问题?

拜占庭问题中提出:允许 n 个节点宕机还能提供正常服务的分布式架构,所需要的总节点数量为 3n+1。而 Raft 只需要 2n+1 就可以了,其主要原因在于:拜占庭将军问题中存在数据欺骗的现象,而 etcd 中假设所有的 Node 都是诚实的。etcd 在竞选前需要告诉别的 Node 自身的 Term 编号以及前一轮 Term 最终结束时的 Index 值,这些数据都是准确的,其他 Node 可以根据这些值决定是否投票。另外,etcd 严格限制 Leader 到 Follower 这样的数据流向保证数据一致不会出错。

客户端从集群中的哪个节点读写数据?

为了保证数据的强一致性,etcd Cluster 中的数据流向都是从 Leader 流向 Follower,也就是所有 Follower 的数据必须与 Leader 保持一致,如果不一致则会被覆盖。

即所有用户更新数据的请求都最先由 Leader 获得,然后通知其他节点也进行更新,等到 “大多数节点” 反馈时,再把数据一次性提交。一个已提交的数据项才是 Raft 真正稳定存储下来的数据项,不再被修改,最后再把提交的数据同步给其他 Follower。因为每个 Node 都有 Raft 已提交数据准确的备份(最坏的情况也只是已提交数据还未完全同步),所以读的请求任意一个节点都可以处理。

实际上,用户可以对 etcd Cluster 中的任意 Node 进行读写:

  • 读取:可以从任意 Node 进行读取,因为每个节点保存的数据是强一致的。
  • 写入:etcd Cluster 首先会选举出 Leader,如果写入请求来自 Leader 即可直接写入,然后 Leader 会把写入分发给所有 Follower;如果写入请求来自其他 Follower 节点那么写入请求会给转发给 Leader 节点,由 Leader 节点写入之后再分发给集群上的所有其他节点。

如何保证数据一致性?

etcd 使用 Raft 协议来维护 Cluster 内各个 Nodes 状态的一致性。简单的说,etcd Cluster 是一个分布式系统,由多个 Nodes 相互通信构成整体对外服务,每个 Node 都存储了完整的数据,并且通过 Raft 协议保证每个 Node 维护的数据是一致的。

etcd Cluster 中的每个 Node 都维护了一个状态机,并且任意时刻,Cluster 中至多存在一个有效的主节点,即:Leader Node。由 Leader 处理所有来自客户端写操作,通过 Raft 协议保证写操作对状态机的改动会可靠的同步到其他 Follower Nodes。

如何选举 Leader 节点?

假设 etcd Cluster 中有 3 个 Node,Cluster 启动之初并没有被选举出的 Leader。此时,Raft 算法使用随机 Timer 来初始化 Leader 选举流程。比如说上面 3 个 Node 上都运行了 Timer(每个 Timer 的持续时间是随机的),而 Node1 率先完成了 Timer,随后它就会向其他两个 Node 发送成为 Leader 的请求,其他 Node 接收到请求后会以投票回应然后第一个节点被选举为 Leader。

成为 Leader 后,该 Node 会以固定时间间隔向其他 Node 发送通知,确保自己仍是 Leader。有些情况下当 Follower 们收不到 Leader 的通知后,比如说 Leader 节点宕机或者失去了连接,其他 Node 就会重复之前的选举流程,重新选举出新的 Leader。

如何判断写入是否成功?

etcd 认为写入请求被 Leader 处理并分发给了其他的 “多数节点” 后,就是一个成功的写入。“多数节点” 的数量的计算公式是 Quorum=N/2+1,N 为总结点数。也就是说,etcd 并发要将数据写入所有节点才算一次写,而是写入 “多数节点” 即可。

如何确定 etcd Cluster 的节点数?

上图左侧给出了集群中 Instances(节点总数)对应的 Quorum(仲裁数)的关系,Instances - Quorom 得到的就是集群中容错节点(允许出故障的节点)的数量。

所以在 etcd Cluster 推荐最少节点数为 3 个,因为 1 和 2 个 Instance 的容错节点数都是 0,一旦有一个节点宕掉整个集群就不能正常工作了。

进一步的说,当我们需要决定 etcd Cluster 中 Instances 的数量时,强烈推荐奇数数量的节点,比如:3、5、7、…,因为 6 个节点的集群的容错能力并没有比 5 个节点的好,他们的容错节点数是一样的,一旦容错节点超过 2 后,由于 Quorum 节点数小于 4,整个集群也就变为不可用的状态了。

etcd 实现的 Raft 算法性能如何?

单实例节点支持每秒 2000 次数据写入。Node 数量越多,由于数据同步涉及到网络延迟,会根据实际情况越来越慢,而读性能会随之变强,因为每个节点都能处理用户请求。

Store

Store,顾名思义,是 etcd 实现的各项底层逻辑,并提供了相应的 API 支持。要理解 Store,只需要从 etcd 的 API 入手。下面列举最常见的 CURD API 调用。

  • 为 etcd 存储的键赋值:
curl http://127.0.0.1:2379/v2/keys/message -X PUT -d value="Hello world"{"action":"set",                     # 执行的操作"node":{"createdIndex":2,           # etcd Node 每次有变化时都会自增的一个值"key":"/message",          # 请求路径"modifiedIndex":2,          # 类似 node.createdIndex,能引起 modifiedIndex 变化的操作包括 set, delete, update, create, compareAndSwap and compareAndDelete"value":"Hello world"      # 存储的内容}
}
  • 查询 etcd 某个键存储的值:
curl http://127.0.0.1:2379/v2/keys/message -X GET
  • 修改键值:
curl http://127.0.0.1:2379/v2/keys/message -XPUT -d value="Hello etcd"
  • 删除键值:
curl http://127.0.0.1:2379/v2/keys/message -XDELETE

WAL

etcd 的数据存储分为两个部分:

  • 内存存储:内存中的存储除了顺序化的记录下所有用户对节点数据变更的记录外,还会对用户数据进行索引、建堆等方便查询的操作。
  • 持久化(硬盘)存储:持久化则使用 WAL(Write Ahead Log,预写式日志)进行记录存储。

WAL 日志是二进制的,解析出来后是以上数据结构 LogEntry。其中:

第一个字段 type,只有两种:

  1. 0 表示 Normal
  2. 1 表示 ConfChange,ConfChange 表示 etcd 本身的配置变更同步,比如有新的节点加入等。

第二个字段是 term,每个 term 代表一个 Leader 的任期,每次 Leader 变更 term 就会变化。

第三个字段是 index,这个序号是严格有序递增的,代表变更序号。

第四个字段是二进制的 data,将 Raft Request 对象的 pb 结构整个保存下。

etcd 源码下有个 tools/etcd-dump-logs 脚本工具,可以将 WAL 日志 dump 成文本查看,可以协助分析 Raft 协议。

Raft 协议本身不关心应用数据,也就是 data 中的部分,一致性都通过同步 WAL 日志来实现,每个 Node 将从 Leader 收到的 data apply 到本地的存储,Raft 只关心日志的同步状态,如果本地存储实现的有 Bug,比如没有正确的将 data apply 到本地,也可能会导致数据不一致。

在 WAL 的体系中,所有的数据在提交之前都会进行日志记录。在 etcd 的持久化存储目录中,有两个子目录:

  1. 一个是 WAL:存储着所有事务的变化记录;
  2. 另一个是 Snapshot:存储着某一个时刻 etcd 所有目录的数据。

通过 WAL 和 Snapshot 相结合的方式,etcd 可以有效的进行数据存储和节点故障恢复等操作。

为什么需要 Snapshot(快照)?

因为随着使用量的增加,WAL 存储的数据会暴增,为了防止磁盘很快就爆满,etcd 默认每 10000 条记录做一次 Snapshot,经过 Snapshot 以后的 WAL 文件就可以删除。所以,通过 API 可以查询的操作历史记录默认为 1000 条。

首次启动时,etcd 会把启动的配置信息存储到 data-dir 配置项指定的目录路径下。配置信息包括 Local Node ID、Cluster ID 和初始时的集群信息。用户需要避免 etcd 从一个过期的数据目录中重新启动,因为使用过期的数据目录启动的 Node 会与 Cluster 中的其他 Nodes 产生不一致性,例如:之前已经记录并同意 Leader Node 存储某个信息,重启后又向 Leader Node 申请这个信息。所以,为了最大化集群的安全性,一旦有任何数据损坏或丢失的可能性,你就应该把这个 Node 从 Cluster 中移除,然后加入一个不带数据目录的 New Node。

WAL(Write Ahead Log)最大的作用是记录了整个数据变化的全部历程。在 etcd 中,所有数据的修改在提交前,都要先写入到 WAL 中。使用 WAL 进行数据的存储使得 etcd 拥有两个重要功能:

  1. 故障快速恢复: 当你的数据遭到破坏时,就可以通过执行所有 WAL 中记录的修改操作,快速从最原始的数据恢复到数据损坏前的状态。
  2. 数据回滚(undo)或重做(redo):因为所有的修改操作都被记录在 WAL 中,需要回滚或重做,只需要方向或正向执行日志中的操作即可。

WAL 和 Snapshot 的命名规则?

在 etcd 的数据目录中,WAL 文件以 $seq-$index.wal 的格式存储。最初始的 WAL 文件是 0000000000000000-0000000000000000.wal,表示这是所有 WAL 文件中的第 0 个,初始的 Raft 状态编号为 0。运行一段时间后可能需要进行日志切分,把新的条目放到一个新的 WAL 文件中。

假设,当集群运行到 Raft 状态为 20 时,需要进行 WAL 文件的切分时,下一份 WAL 文件就会变为 0000000000000001-0000000000000021.wal。如果在 10 次操作后又进行了一次日志切分,那么后一次的 WAL 文件名会变为 0000000000000002-0000000000000031.wal。可以看到 “-” 符号前面的数字是每次切分后自增 1,而 “-” 符号后面的数字则是根据实际存储的 Raft 起始状态来定。

而 Snapshot 的存储命名则比较容易理解,以 $term-$index.wal 格式进行命名存储。term 和 index 就表示存储 Snapshot 时数据所在的 Raft 节点状态,当前的任期编号以及数据项位置信息。

etcd 的数据模型

etcd 的设计目的是用来存放非频繁更新的数据,提供可靠的 Watch 插件,它暴露了键值对的历史版本,以支持低成本的快照、监控历史事件。这些设计目标要求它使用一个持久化的、多版本的、支持并发的数据数据模型。

当 etcd 键值对的新版本保存后,先前的版本依然存在。从效果上来说,键值对是不可变的,etcd 不会对其进行 in-place 的更新操作,而总是生成一个新的数据结构。为了防止历史版本无限增加,etcd 的存储支持压缩(Compact)以及删除老旧版本。

逻辑视图

从逻辑角度看,etcd 的存储是一个扁平的二进制键空间,键空间有一个针对键(字节字符串)的词典序索引,因此范围查询的成本较低。

键空间维护了多个修订版本(Revisions),每一个原子变动操作(一个事务可由多个子操作组成)都会产生一个新的修订版本。在集群的整个生命周期中,修订版都是单调递增的。修订版同样支持索引,因此基于修订版的范围扫描也是高效的。压缩操作需要指定一个修订版本号,小于它的修订版会被移除。

一个键的一次生命周期(从创建到删除)叫做 “代(Generation)”,每个键可以有多个代。创建一个键时会增加键的版本(Version),如果在当前修订版中键不存在则版本设置为 1。删除一个键会创建一个墓碑(Tombstone),将版本设置为 0,结束当前代。每次对键的值进行修改都会增加其版本号,即:在同一代中版本号是单调递增的。

当压缩时,任何在压缩修订版之前结束的代,都会被移除。值在修订版之前的修改记录(仅仅保留最后一个)都会被移除。

物理视图

etcd 将数据存放在一个持久化的 B+ 树中,出于效率的考虑,每个修订版仅仅存储相对前一个修订版的数据状态变化(Delta)。单个修订版中可能包含了 B+ 树中的多个键。

键值对的键,是三元组(Major,Sub,Type):

  • Major:存储键值的修订版。
  • Sub:用于区分相同修订版中的不同键。
  • Type:用于特殊值的可选后缀,例如 t 表示值包含墓碑

键值对的值,包含从上一个修订版的 Delta。B+ 树,即:键的词法字节序排列,基于修订版的范围扫描速度快,可以方便的从一个修改版到另外一个的值变更情况查找。

etcd 同时在内存中维护了一个 B 树索引,用于加速针对键的范围扫描。索引的键是物理存储的键面向用户的映射,索引的值则是指向 B+ 树修该点的指针。

etcd 的 Proxy 模式

Proxy 模式,即:etcd 作为一个反向代理把客户端的请求转发给可用的 etcd Cluster。这样,你就可以在每一台机器上都部署一个 Proxy 模式的 etcd 作为本地服务,如果这些 etcd Proxy 都能正常运行,那么你的服务发现必然是稳定可靠的。

所以 Proxy 模式并不是直接加入到符合强一致性的 etcd Cluster 中,也同样的,Proxy 并没有增加集群的可靠性,当然也没有降低集群的写入性能。

Proxy 模式取代 Standby 模式的原因?

实际上 etcd 每增加一个核心节点(Peer),都会增加 Leader 一定程度的包括网络、CPU 和磁盘的负担,因为每次信息的变化都需要进行同步备份。增加 etcd 的核心节点可以让整个集群具有更高的可靠性,但是当数量达到一定程度以后,增加可靠性带来的好处就变得不那么明显,反倒是降低了集群写入同步的性能。因此,增加一个轻量级的 Proxy 模式 etcd Node 是对直接增加 etcd 核心节点的一个有效代替。

Proxy 模式实际上是取代了原先的 Standby 模式。Standby 模式除了转发代理的功能以外,还会在核心节点因为故障导致数量不足的时候,从 Standby 模式转为正常节点模式。而当那个故障的节点恢复时,发现 etcd 的核心节点数量已经达到的预先设置的值,就会转为 Standby 模式。

但是新版本的 etcd 中,只会在最初启动 etcd Cluster 时,发现核心节点的数量已经满足要求时,自动启用 Proxy 模式,反之则并未实现。主要原因如下:

  • etcd 是用来保证高可用的组件,因此它所需要的系统资源,包括:内存、硬盘和 CPU 等,都应该得到充分保障以保证高可用。任由集群的自动变换随意地改变核心节点,无法让机器保证性能。所以 etcd 官方鼓励大家在大型集群中为运行 etcd 准备专有机器集群。
  • 因为 etcd 集群是支持高可用的,部分机器故障并不会导致功能失效。所以机器发生故障时,管理员有充分的时间对机器进行检查和修复。
  • 自动转换使得 etcd 集群变得复杂,尤其是如今 etcd 支持多种网络环境的监听和交互。在不同网络间进行转换,更容易发生错误,导致集群不稳定。

etcd 的基本操作

与 etcd 交互

etcd v3 API 使用 gRPC 协议。etcd 项目包括了一个基于 gRPC 的 Golang Client SDK 和一个指令行工具 etcdctl,用于通过 gRPC 协议与 etcd Cluster 进行通信。对于不支持 gRPC 的编程语言,etcd 还提供了一个 JSON gRPC 网关。 该网关提供一个 RESTful Proxy,该 Proxy 将 HTTP/JSON 请求转换为 gRPC 消息。

  • Swagger API Docs:https://github.com/etcd-io/etcd/blob/master/Documentation/dev-guide/apispec/swagger/rpc.swagger.json

注意,gRPC 网关不支持使用 TLS 通用名称的身份验证。

etcdctl 交互

Put(创建/更新)

通过 put 将 key 和 value 存储到 etcd 集群中。每个存储的 key 都通过 Raft 协议复制到所有 etcd 集群成员,以实现一致性和可靠性。

$ ./etcdctl put foo bar --user root --password=pass
OK

Get(查询)

通过 get 可以从一个 etcd 集群中读取 key 的值。

假设现在 etcd 集群已经存储了以下数据:

foo = bar
foo1 = bar1
foo2 = bar2
foo3 = bar3
a = 123
b = 456
z = 789
  • 获取所有的 keys:
etcdctl get --prefix "" --keys-only=true
  • 读取键为 foo 的命令:
$ ./etcdctl get foo --user root --password=pass
foo    // key
bar    // value
  • 只读取 key 对应的值呢:
$ ./etcdctl get foo --print-value-only --user root --password=pass
bar
  • 读取一系列 key,例如区间 [foo, foo3):
$ ./etcdctl get foo foo3 --print-value-only --user root --password=pass
bar
bar1
bar2
  • 按前缀读取:
$ ./etcdctl get --prefix foo --print-value-only --user root --password=pass
bar
bar1
bar2
bar3
  • 限制结果数量:
$ ./etcdctl get --prefix foo --print-value-only --limit=2 --user root --password=pass
bar
bar1
  • 读取大于或等于指定键的字节值的键:
$ ./etcdctl get --from-key b --user root --password=pass
b
456
c
789
foo
bar
foo1
bar1
foo2
bar2
foo3
bar3

用户可能希望通过访问早期版本的 key 来回滚到旧版本的配置。由于对 etcd 集群键值存储区的每次修改都会增加一个 etcd 集群的全局修订版本(revision),因此用户可以通过提供旧的 etcd 修订版(revision)来读取被取代的键。

假设一个 etcd 集群已经有以下 key:

foo = bar         # revision = 2
foo1 = bar1       # revision = 3
foo = bar_new     # revision = 4
foo1 = bar1_new   # revision = 5

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

# 访问最新版本的 key
$ etcdctl get --prefix foo
foo
bar_new
foo1
bar1_new# 访问第 4 个版本的 key
$ etcdctl get --prefix foo --rev=4
foo
bar_new
foo1
bar1#  访问第 3 个版本的key
$ etcdctl get --prefix foo --rev=3
foo
bar
foo1
bar1

Del(删除)

通过 del 可以从一个 etcd 集群中删除一个 key 或一系列 key。

假设一个 etcd 集群已经有以下key:

foo = bar
foo1 = bar1
foo3 = bar3
zoo = val
zoo1 = val1
zoo2 = val2
a = 123
b = 456
z = 789
  • 删除指定的 key:
$ etcdctl del foo
1
  • 删除指定的键值对:
$ etcdctl del --prev-kv zoo
1
zoo
val
  • 删除从 foo 到 foo9 的命令:
$ etcdctl del foo foo9
2
  • 删除具有前缀的键的命令:
$ etcdctl del --prefix zoo
2
  • 删除大于或等于键的字节值的键的命令:
$ etcdctl del --from-key b
2

Watch(监控)

Watch 用于监测一个 key-value 的变化,一旦 key-value 发生更新,就会输出最新的值并退出。

  • 打开第一个终端,监听 foo 的变化:
$ etcdctl watch foo
  • 打开另外一个终端来对 foo 进行操作:
$ etcdctl put foo 123
OK
$ etcdctl put foo 456
OK
$ ./etcdctl del foo
1
  • 第一个终端追踪的结果如下:
$ etcdctl watch foo
PUT
foo
123
PUT
foo
456
DELETE
foo

除了以上基本操作,Watch 也可以像 get、del 操作那样使用 prefix、rev、hex 等参数。

Lock(分布式锁)

Distributed locks(分布式锁),即:一个人操作的时候,另外一个人只能看,不能操作。

etcd 的 lock 指令对指定的 key 进行加锁。注意,只有当正常退出且释放锁后,lock 命令的退出码是 0,否则这个锁会一直被占用直到过期(默认 60 秒)。

  • 在第一个终端输入如下命令:
$ etcdctl lock mutex1 --user root --password=pass
mutex1/326963a02758b52d
  • 在第二个终端输入同样的命令:
$ etcdctl lock mutex1 --user root --password=pass

在此可以发现第二个终端发生了阻塞,并未返回类似 mutex1/326963a02758b52d 的输出。此时,如果我们使用 Ctrl+C 结束了第一个终端的 lock,然后第二个终端的显示如下:

mutex1/694d74f33b51c654

可见,这就是一个分布式锁的实现。

Transactions(事务)

txn 支持从标准输入中读取多个请求,并将它们看做一个原子性的事务执行。事务是由条件列表,条件判断成功时的执行列表(条件列表中全部条件为真表示成功)和条件判断失败时的执行列表(条件列表中有一个为假即为失败)组成的。

$ ./etcdctl put user frank --user root --password=pass
OK$ ./etcdctl txn -i --user root --password=pass
compares:
value("user") = "frank"success requests (get, put, del):
put result okfailure requests (get, put, del):
put result failedSUCCESSOK$ ./etcdctl get result --user root --password=pass
result
ok
  1. 先使用 etcdctl put user frank 设置 user 为 frank。然后 etcdctl txn -i 开启事务(-i 表示交互模式)。
  2. 第 2 步输入命令后回车,终端显示出 compares:
  3. 输入 value("user") = "frank",此命令是比较 user 的值与 frank 是否相等。
  4. 第 4 步完成后输入回车,终端会换行显示,此时可以继续输入判断条件(前面说过事务由条件列表组成),再次输入回车表示判断条件输入完毕。
  5. 第 5 步连续输入两个回车后,终端显示出 success requests (get, put, delete):,表示下面输入判断条件为真时要执行的命令。
  6. 与输入判断条件相同,连续两个回车表示成功时的执行列表输入完成。
  7. 终端显示 failure requests (get, put, delete): 后输入条件判断失败时的执行列表。

为了看起来简洁,此实例中条件列表和执行列表只写了一行命令,实际可以输入多行。总结上面的事务,要做的事情就是 user 为 frank 时设置 result 为 ok,否则设置 result 为 failed,事务执行完成后查看 result 值为 ok。

Compact(压缩)

etcd 会保存数据的修订版本,以便用户可以读取旧版本的 key。但是为了避免累积无尽头的版本历史,就需要压缩过去的修订版本。压缩后,etcd 会删除历史版本并释放资源。

$ etcdctl compact 5
compacted revision 5$ etcdctl get --rev=4 foo
Error: etcdserver: mvcc: required revision has been compacted

Lease(租约)

key TTL(生存时间)是 etcd 的重要特性之一,即设置 key 的超时时间。与 Redis 不同,etcd 需要先创建 lease(租约),通过 put --lease= 设置。而 lease 又由 TTL 管理,以此来实现 key 超时设置的功能。

  • 授予 Lease:
$ etcdctl lease grant 30
lease 694d6ee9ac06945d granted with TTL(30s)$ etcdctl put --lease=694d6ee9ac06945d foo bar
OK
  • 撤销指定 Lease:
$ etcdctl lease revoke 694d6ee9ac06945d
lease 694d6ee9ac06945d revoked$ etcdctl get foo
  • 用户可以通过刷新指定的 key TTL 来保持 Lease:
$ etcdctl lease grant 10
lease 32695410dcc0ca06 granted with TTL(10s)

某些时候,用户可能希望了解 Lease 的信息,以便可以续约或检查 Lease 是否仍然存在或已过期。另外,用户也可能希望了解到指定 Lease 所附的 keys。

假设我们完成了以下一系列操作:

$ etcdctl lease grant 200
lease 694d6ee9ac06946a granted with TTL(200s)$ etcdctl put demo1 val1 --lease=694d6ee9ac06946a
OK$ etcdctl put demo2 val2 --lease=694d6ee9ac06946a
OK
  • 获取指定 Lease 的信息:
$ etcdctl lease timetolive 694d6ee9ac06946a
lease 694d6ee9ac06946a granted with TTL(200s), remaining(178s)
  • 获取指定 Lease 所附的 keys:
$ etcdctl lease timetolive --keys 694d6ee9ac06946a
lease 694d6ee9ac06946a granted with TTL(200s), remaining(129s), attached keys([demo1 demo2])

API 交互

Authentication

Set up authentication with the /v3/auth service.

# create root user
$ curl -L http://localhost:2379/v3/auth/user/add \-X POST -d '{"name": "root", "password": "pass"}'{"header": {"cluster_id": "14841639068965178418","member_id": "10276657743932975437","revision": "5","raft_term": "3"}
}# create root role
$ curl -L http://localhost:2379/v3/auth/role/add \-X POST -d '{"name": "root"}'{"header": {"cluster_id": "14841639068965178418","member_id": "10276657743932975437","revision": "5","raft_term": "3"}
}# grant root role
$ curl -L http://localhost:2379/v3/auth/user/grant \-X POST -d '{"user": "root", "role": "root"}'{"header": {"cluster_id": "14841639068965178418","member_id": "10276657743932975437","revision": "5","raft_term": "3"}
}# enable auth
$ curl -L http://localhost:2379/v3/auth/enable -X POST -d '{}'{"header": {"cluster_id": "14841639068965178418","member_id": "10276657743932975437","revision": "5","raft_term": "3"}
}

Authenticate with etcd for an authentication token using /v3/auth/authenticate.

# get the auth token for the root user
$ curl -L http://localhost:2379/v3/auth/authenticate \-X POST -d '{"name": "root", "password": "pass"}'{"header": {"cluster_id": "14841639068965178418","member_id": "10276657743932975437","revision": "5","raft_term": "3"},"token": "CAmBqFhXjZCCFRQV.15"
}

Set the Authorization header to the authentication token to fetch a key using authentication credentials.

$ curl -L http://localhost:2379/v3/kv/put \-H 'Authorization: CAmBqFhXjZCCFRQV.15' \-X POST -d '{"key": "Zm9v", "value": "YmFy"}'{"header": {"cluster_id": "14841639068965178418","member_id": "10276657743932975437","revision": "7","raft_term": "3"}
}

Put and get keys

Use the /v3/kv/range and /v3/kv/put services to read and write keys.

# Write
$ curl -L http://localhost:2379/v3/kv/put \-H 'Authorization: CAmBqFhXjZCCFRQV.15' \-X POST -d '{"key": "Zm9v", "value": "YmFy"}'{"header": {"cluster_id": "14841639068965178418","member_id": "10276657743932975437","revision": "3","raft_term": "3"}
}# Read
$ curl -L http://localhost:2379/v3/kv/range \-H 'Authorization: CAmBqFhXjZCCFRQV.15' \-X POST -d '{"key": "Zm9v"}'{"header": {"cluster_id": "14841639068965178418","member_id": "10276657743932975437","revision": "3","raft_term": "3"},"kvs": [{"key": "Zm9v","create_revision": "2","mod_revision": "3","version": "2","value": "YmFy"}],"count": "1"
}# get all keys prefixed with "foo"
$ curl -L http://localhost:2379/v3/kv/range \-H 'Authorization: CAmBqFhXjZCCFRQV.15' \-X POST -d '{"key": "Zm9v", "range_end": "Zm9w"}'{"header": {"cluster_id": "14841639068965178418","member_id": "10276657743932975437","revision": "3","raft_term": "3"},"kvs": [{"key": "Zm9v","create_revision": "2","mod_revision": "3","version": "2","value": "YmFy"}],"count": "1"
}

Watch keys

Use the /v3/watch service to watch keys.

  • Watch 指定的 key:
$ curl -N http://localhost:2379/v3/watch \-H 'Authorization: CAmBqFhXjZCCFRQV.15' \-X POST -d '{"create_request": {"key":"Zm9v"} }' | jq .{"result": {"header": {"cluster_id": "14841639068965178418","member_id": "10276657743932975437","revision": "3","raft_term": "3"},"created": true}
}
  • 更新该 key:
$ curl -L http://localhost:2379/v3/kv/put \-H 'Authorization: CAmBqFhXjZCCFRQV.15' \-X POST -d '{"key": "Zm9v", "value": "YmFy"}' >/dev/null 2>&1
  • Watch key 的变更会被跟踪到:
{"result": {"header": {"cluster_id": "14841639068965178418","member_id": "10276657743932975437","revision": "4","raft_term": "3"},"events": [{"kv": {"key": "Zm9v","create_revision": "2","mod_revision": "4","version": "3","value": "YmFy"}}]}
}

Transactions

Issue a transaction with /v3/kv/txn.

# target CREATE
$ curl -L http://localhost:2379/v3/kv/txn \-H 'Authorization: CAmBqFhXjZCCFRQV.15' \-X POST \-d '{"compare":[{"target":"CREATE","key":"Zm9v","createRevision":"2"}],"success":[{"requestPut":{"key":"Zm9v","value":"YmFy"}}]}'{"header": {"cluster_id": "14841639068965178418","member_id": "10276657743932975437","revision": "5","raft_term": "3"},"succeeded": true,"responses": [{"response_put": {"header": {"revision": "5"}}}]
}# target VERSION$ curl -L http://localhost:2379/v3/kv/txn \-H 'Authorization: CAmBqFhXjZCCFRQV.15' \-X POST \-d '{"compare":[{"version":"4","result":"EQUAL","target":"VERSION","key":"Zm9v"}],"success":[{"requestRange":{"key":"Zm9v"}}]}'{"header": {"cluster_id": "14841639068965178418","member_id": "10276657743932975437","revision": "5","raft_term": "3"},"succeeded": true,"responses": [{"response_range": {"header": {"revision": "5"},"kvs": [{"key": "Zm9v","create_revision": "2","mod_revision": "5","version": "4","value": "YmFy"}],"count": "1"}}]
}

Golang Client SDK 交互

etcd 提供了一个 Golang Client SDK 用于编写 etcd 客户端程序,下面简单列举一些例子,加强体感。

首先实例化一个 client 实例:

    cli,err := clientv3.New(clientv3.Config{Endpoints:[]string{"localhost:2379"},DialTimeout: 5 * time.Second,})
  • Endpoints:etcd 的多个节点服务地址,因为笔者使用的单点部署,所以只传 1 个服务入口。
  • DialTimeout:创建 client 的首次连接超时,这里传了 5 秒,如果 5 秒都没有连接成功就会返回 err;值得注意的是,一旦 client 创建成功,我们就不用再关心后续底层连接的状态了,client 内部会进行重连。

k-v 存取:

kv := clientev3.NewKV(client)// Put
putResp, err := kv.Put(context.TODO(), "/data-dir/example", "hello-world!")// Get
getResp, err := kv.Get(context.TODO(), "/data-dir/example")

Lease:

lease := clientv3.NewLease(client)// 创建一个租约,它有 10 秒的 TTL
grantResp, err := lease.Grant(context.TODO(), 10)// 指定一个租约创建 k-v
kv.Put(context.TODO(), "/example/expireme", "lease-go", clientv3.WithLease(grantResp.ID))

云计算与云原生 — ETCD 数据库完全解析相关推荐

  1. 云计算与云原生技术系列文章

    目录 文章目录 目录 云计算 云原生 云原生思想 容器技术 Docker containerd Kata Container APIGW ETCD 服务治理 - Service Mesh FaaS O ...

  2. 李飞飞:云原生分布式数据库面临哪些机遇与挑战?

    简介:12月3日,由阿里云CIO学院主办的[2020中国企业数字创新峰会]在杭州举行.400位企业CEO.CTO.CIO齐聚一堂,共同探讨企业数字化转型经验.阿里巴巴集团副总裁.达摩院数据库首席科学家 ...

  3. 每秒8.8亿次请求!让数据存得起,看得见 - 云原生多模数据库Lindorm 2020双十一总结

    简介:2020双十一顺利落下帷幕,这也是云原生多模数据库Lindorm参与的第九个双十一,其作为阿里经济体的核心数据库产品之一,全面支撑了淘宝.天猫.蚂蚁.菜鸟.阿里妈妈.高德.优酷.钉钉.大文娱等经 ...

  4. 斩获大奖|阿里云PolarDB-X引领云原生分布式数据库新时代

    简介:阿里云原生分布式数据库PolarDB-X荣获"2021年度最佳分布式数据库". 12月15-16日,以"引领分布式云变革 助力湾区数字经济"为主题的全球分 ...

  5. 什么是云原生分布式数据库?

    这两天朋友圈中刷屏最多的是达梦数据库产品发布会,众多嘉宾,群星璀璨, 此次一口气推出了达梦数据共享集群(DMDSC).达梦启云数据库(DMCDB).梦图数据库(GDM).达梦新一代分布式数据库等四款产 ...

  6. 阿里巴巴云原生 etcd 服务集群管控优化实践

    作者 | 陈星宇(宇慕) 来源 | 阿里巴巴云原生公众号 背景 Kubernetes 采用 etcd 存储其内部核心元数据信息.经过这些年的发展,尤其是伴随着这两年云原生的快速发展,Kubernete ...

  7. 阿里云开源PolarDB数据库,与社区共建云原生分布式数据库生态

    简介:5月29日,阿里云开发者大会上,阿里巴巴宣布开源云原生数据库能力,对外开放关系型数据库PolarDB for PostgreSQL 源代码,服务百万开发者,与社区开发者一起共建云原生分布式数据库 ...

  8. 阿里云原生多模数据库Lindorm联合东软云科技,赋能车联网数字化运营运维创新升级

    简介:阿里云原生多模数据库Lindorm与东软云科技推出联合解决方案,共建面向未来的车联网数字化运营运维云平台.目前,该联合解决方案已在东软睿驰.江铃汽车.长城汽车等客户中得到广泛应用和实践落地. 作 ...

  9. 云原生 SQL 数据库 CockroachDB 2.0 发布:性能极大提升

    2018 年 4 月,CockroachDB 2.0 版本正式发布,带来全新升级.作为 CockroachDB 2.x 系列的第一个版本,CockroachDB 2.0 极大提升了性能,并带来了一系列 ...

  10. 通过云速搭CADT实现云原生分布式数据库PolarDB-X 2.0的部署

    云速搭 CADT 是一款为上云应用提供自助式云架构管理的产品,显著地降低应用云上管理的难度和时间成本.本产品提供丰富的预制应用架构模板,同时也支持自助拖拽方式定义应用云上架构:支持较多阿里云服务的配置 ...

最新文章

  1. 危险进程大集合咯!(吐血推荐)
  2. CentOS 6.5下编译安装新版LNMP
  3. 【勘误】第4章字符串的实现
  4. 风水学是天气预测模型
  5. 摆脱“人肉”审核,从0搭建可视化SQL自动审核平台
  6. Android 设置thumb图片大小
  7. PAT (Basic Level) 1091 N-自守数(模拟+stl)
  8. 2008年初看的书[带简评]
  9. Java使用TCP实现群聊 聊天室(多线程和tcp的使用)
  10. YUV测试序列下载地址
  11. 微信这10个牛X的隐藏功能,你知道几个?
  12. 东大OJ-Prim算法
  13. GitHub CEO 回应源代码泄露:没有黑客!没有被入侵!
  14. javascript跨域、iframe跨域访问
  15. matlab 设计 希尔伯特变换器,基于Matlab的FIR型希尔伯特变换器设计
  16. NS3 之 入坑
  17. 基于Java的超市积分管理系统(附:论文 源码 课件)
  18. 如何编辑修改PDF,PDF怎么删除页眉页脚
  19. 给我写信 wyz831201王玉镇
  20. 使用openfiler配置共享存储(1)——openfiler的安装和配置

热门文章

  1. aardio利用鼠标钩子响应文件拖动过程(一)
  2. 烟雨要饭网最终版源码
  3. win10设置眼睛保护色脚本
  4. 14款微信小游戏源码下载
  5. 二维码生成原理及解析代码
  6. 如何查看android设备是否支持Widevine DRM
  7. 【公司内部资料】顺丰新员工入职考试35道考试题目答案
  8. Python路飞学城老男孩内部书籍,Python全栈开发实战pdf
  9. [lammps安装教程]lammps串行版安装教程
  10. 4800包括了路线坐标正反算、竖曲线、超高加宽、边坡放样及断面计算等程序。