Consul文档

由于文章太长,简书放不下,完整文档见Consul文档

一、安装 Consul

Consul 的安装很简单,安装 Consul 有以下两种方式:

  • 使用预编译的二进制文件
  • 使用源代码安装
      下载一个预编译的二进制文件是最简单的,我们通过TLS和SHA256总和提供下载来验证二进制文件。 我们还分发了可以验证的SHA256总和的PGP签名。

1.1 预编译的二进制文件

要安装预编译的二进制文件,请为您的系统下载相应的软件包。 Consul目前打包成一个zip文件。 我们近期没有提供系统软件包的计划。
  一旦zip被下载,解压缩到任何目录。 内部的Consul二进制文件是运行Consul(或者consul.exe for Windows)所必需的。 如果有任何额外的文件,请不需要运行Consul。
  将二进制文件复制到系统中的任何位置。 如果您打算从命令行访问它,请设置PATH环境变量。

1.2 从源代码编译

要从源代码编译,您需要安装并正确配置Go(包括GOPATH环境变量集)以及PATH中的git副本。

1.2.1 从GitHub克隆Consul资源库到你的GOPATH中:
$ mkdir -p $GOPATH/src/github.com/hashicorp && cd $!
$ git clone https://github.com/hashicorp/consul.git
$ cd consul
1.2.2 引导项目。 这将下载和编译Consul所需的库和工具:
$ make bootstrap
1.2.3 为你当前系统编译Consul,并把二进制文件放入./bin/(相对于git checkout)。 make dev目标仅仅是一个快捷方式,只为您的本地构建环境(没有交叉编译的目标)构建Consul。
$ make dev

1.3 安装验证

要确认Consul已正确安装,请在您的系统上运行consul -v。 你应该看到帮助输出。 如果您正在从命令行执行它,请确保已设置好PATH环境变量,否则您可能会收到Consul找不到的错误。

[root@localhost ~]# consul -v
Consul v1.0.1
Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol >2 when speaking to compatible agents)

二、Consul 升级

Consul是在任何参与Consul集群的节点上长期运行的代理。 这些节点一直相互通讯。 因此,在使用Consul时,协议级别的兼容性和易于升级是一个重要的事项。
  下面介绍如何在新版本发布时升级Consul。

2.1 标准升级

为了升级,我们努力确保向后兼容性。 为了支持这个,建立了节点之间通讯协议的版本。 这使得客户端和服务器能够在可用时智能地启用新功能,否则将优雅地退回到向后兼容的操作模式。
  对于大多数升级,过程很简单。 假设Consul的当前版本是A,发布了版本B。

  • 1、在每台服务器上安装Consul版本B。
  • 2、关闭版本A,重新启动版本B。
  • 3、一旦所有服务器都升级完毕,就开始按照相同的流程升级客户端。
  • 4、完成! 你现在正在运行最新的Consul代理。 您可以通过运行consul members来验证这一点,以确保所有成员拥有最新版本和最高协议版本。

2.2 向后不兼容的升级

在某些情况下,可能会发布向后不兼容的更新。 这尚未成为问题,但为了支持升级,我们支持设置显式协议版本。 这会禁用不兼容的功能并启用两阶段升级。
  对于下面的步骤,假设你正在运行Consul版本A,然后发布了版本B。

  • 1、在每个节点上安装Consul的B版本。
  • 2、关闭版本A,并使用-protocol = PREVIOUS选项启动版本B,其中“PREVIOUS”是版本A的协议版本(可通过运行consul -vconsul members来获取)。
  • 3、一旦所有节点都运行版本B,请遍历每个节点并重新启动没有-protocol选项的版本B代理。
  • 4、完成! 你现在正在运行最新的Consul代理,并使用最新的协议。 您可以通过运行consul members来确认所有成员都使用同一个最新的协议版本。
      做这项工作的关键是Consul的协议兼容性。 协议版本系统在下面讨论。

2.3 协议版本

默认情况下,Consul代理会说明最新的协议。 然而,如果有协议的变化,每一个新版本的Consul也 会说明之前的协议。
  你可以通过运行consul -v来查看你的Consul版本所能接受的协议版本。你会看到类似于下面的输出:

[root@localhost ~]# consul -v
Consul v1.0.1
Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol >2 when speaking to compatible agents)

这显示的是Consul的版本,以及这个代理使用和可以接受的协议版本。
  有时Consul会默认使用一个比它接受的更低的协议版本,以便与旧的代理兼容。 例如,接受版本3的Consul代理声称版本2,并且仅向接受版本3的代理发送版本3消息。这允许功能在代理升级时自动升级,并且是可能时使用的策略。 如果这是不可能的,那么您将需要使用上述说明进行不兼容的升级,并且在给定版本的说明中将清楚地列出这样的要求。
  通过在consul代理上指定-protocol选项,你可以告诉Consul代理使用任何可以理解的协议版本。 这只是指定协议版本。 每一个Consul代理都可以随时了解它在consul -v上声称的所有协议版本。
  通过运行以前的协议版本,Consul的某些功能,特别是较新的功能,可能无法使用。 如果是这样的话,Consul通常会提醒你。 通常,您应该始终升级集群,以便运行最新的协议版本。

三、协议兼容性承诺

我们希望Consul能够运行在大批长期运营的代理身上。 因为在这种环境下安全升级代理在很大程度上依赖于向后兼容性,所以我们强烈承诺保持不同Consul版本之间的协议兼容性。
  我们承诺,Consul的每一个后续版本将保持向后兼容至少一个以前的版本。 具体地说:版本0.5可以说是0.4(反之亦然),但可能不能说0.1。
  除非另有说明,向后兼容性是自动的。Consul代理默认会使用最新的协议,但可以理解更早的协议。
  注意:如果使用较早的协议,新功能可能无法使用。
  Consul 使用较早的协议的能力是确保任何代理程序都可以升级而不会造成集群中断。 Consul代理可以一次更新一个,一次一个版本。

协议兼容性表

Consul版本 兼容协议
0.1 - 0.3 1
0.4 1, 2
0.5 1, 2。 0.5.X服务器不能与旧服务器混合使用。
0.6 1, 2, 3
>= 0.7 2, 3。向兼容代理通讯时会自动使用协议> 2

注意:Raft协议是单独版本,但与至少一个以前的版本保持兼容。

四、升级特定的版本

升级部分涵盖了进行标准升级的细节。 但是,由于新功能或行为改变,Consul的特定版本可能会提供更多的升级细节。 此处用于将这些详细信息与标准升级流程分开记录。

4.1 Consul 1.0.1

在升级过程中仔细检查并删除老的服务器。
  在使用Raft协议3运行时,Consul 1.0(和Consul的早期版本)有一个问题,即执行Consul服务器的滚动更新可能会导致集群中剩余的旧服务器中断,自动控制系统通常会在新服务器联机时删除旧服务器,但还等着将服务器推向成对的投票人,以维持一个奇数的法定人数,这个成对的推广功能被取消,服务器一旦稳定就成为选民,使得自动控制系统以更安全的方式移除旧服务器。
  从Consul 1.0升级时,您可能需要手动force-leave
将旧服务器作为Consul 1.0.1滚动更新的一部分。

4.2 Consul 1.0

Consul 1.0这里记录了几个重要的重大改变。升级前请务必阅读所有的细节。

Raft协议现在默认为3

-raft-protocol
默认值已从2更改为3,默认启用所有自动控制功能。
  Raft协议版本3要求Consul在所有服务器上运行0.8.0或更高版本才能工作,所以如果您正在使用集群中较旧的服务器进行升级,则需要将其设置为2来升级。 有关更多详细信息,请参阅Raft协议版本兼容 。用最新的Raft协议运行时,用于停机恢复的peers.json的格式也是不同的。 请参阅手动恢复使用peers.json获取所需格式的说明
  请注意,Raft协议与协议兼容性承诺页面上介绍的Consul内部协议不同,正如在consul membersconsul version中所示。要查看每台服务器上使用的Raft协议的版本,请使用consul operator raft list-peers命令。
  升级服务器最简单的方法是让每台服务器离开集群,升级其Consul版本,然后将其添加回来。 确保新服务器成功加入,并且在将升级前滚到下一个服务器之前,集群已经稳定。 也可以启动一套新的服务器,然后以相似的方式慢慢地恢复每个旧的服务器上。
  当使用Raft协议版本3时,当Consul对其内部Raft仲裁配置进行更改时,服务器由-node-id标识,而不是其IP地址。 这意味着,一旦集群升级了所有运行Raft协议版本3的服务器,它将不再允许运行任何旧的Raft协议版本的服务器被添加。 如果运行一个单独的Consul服务器,重新启动它将导致该服务器无法将自己选为领导者。 为避免这种情况,可以将Raft协议设置回2,或者使用手动恢复使用peers.json将服务器映射到Raft仲裁配置中的节点ID。

配置文件需要扩展名

作为支持Consul配置文件的HCL格式的一部分,Consul加载的所有配置文件都需要.hcl或.json扩展名,即使使用-config-file参数直接指定文件也是如此。

已弃用的选项已被删除

移除选项 替换选项
-atlas 没有,Atlas不再支持。
-atlas-token 没有,Atlas不再支持。
-atlas-join 没有,Atlas不再支持。
-atlas-endpoint 没有,Atlas不再支持。
-dc -datacenter
-retry-join-azure-tag-name -retry-join
-retry-join-azure-tag-value -retry-join
-retry-join-ec2-region -retry-join
-retry-join-ec2-tag-key -retry-join
-retry-join-ec2-tag-value -retry-join
-retry-join-gce-credentials-file -retry-join
-retry-join-gce-project-name -retry-join
-retry-join-gce-tag-name -retry-join
-retry-join-gce-zone-pattern -retry-join
addresses.rpc 无,不再支持CLI命令的RPC服务器。
advertise_addrs ports with advertise_addr and/or advertise_addr_wan
atlas_infrastructure 没有,Atlas不再支持。
atlas_token 没有,Atlas不再支持。
atlas_acl_token 没有,Atlas不再支持。
atlas_join 没有,Atlas不再支持。
atlas_endpoint 没有,Atlas不再支持。
dogstatsd_addr telemetry.dogstatsd_addr
dogstatsd_tags telemetry.dogstatsd_tags
http_api_response_headers http_config.response_headers
ports.rpc 无,不再支持CLI命令的RPC服务器。
recursor recursors
retry_join_azure -retry-join
retry_join_ec2 -retry-join
retry_join_gce -retry-join
statsd_addr telemetry.statsd_address
statsite_addr telemetry.statsite_address
statsite_prefix telemetry.metrics_prefix
telemetry.statsite_prefix telemetry.metrics_prefix
(service definitions) serviceid service_id
(service definitions) dockercontainerid docker_container_id
(service definitions) tlsskipverify tls_skip_verify
(service definitions) deregistercriticalserviceafter deregister_critical_service_after

Consul先前不推荐使用的命令行标志和配置选项已被删除,因此在升级之前需要将这些选项映射到它们的等价选项。 以下是已删除选项及其等价选项的完整列表:

移除选项 替换选项
-atlas 没有,Atlas不再支持。
-atlas-token 没有,Atlas不再支持。
-atlas-join 没有,Atlas不再支持。
-atlas-endpoint 没有,Atlas不再支持。
-dc -datacenter
-retry-join-azure-tag-name -retry-join
-retry-join-azure-tag-value -retry-join
-retry-join-ec2-region -retry-join
-retry-join-ec2-tag-key -retry-join
-retry-join-ec2-tag-value -retry-join
-retry-join-gce-credentials-file -retry-join
-retry-join-gce-project-name -retry-join
-retry-join-gce-tag-name -retry-join
-retry-join-gce-zone-pattern -retry-join
addresses.rpc 无,不再支持CLI命令的RPC服务器。
advertise_addrs ports with advertise_addr and/or advertise_addr_wan
atlas_infrastructure 没有,Atlas不再支持。
atlas_token 没有,Atlas不再支持。
atlas_acl_token 没有,Atlas不再支持。
atlas_join 没有,Atlas不再支持。
atlas_endpoint 没有,Atlas不再支持。
dogstatsd_addr telemetry.dogstatsd_addr
dogstatsd_tags telemetry.dogstatsd_tags
http_api_response_headers http_config.response_headers
ports.rpc 无,不再支持CLI命令的RPC服务器。
recursor recursors
retry_join_azure -retry-join
retry_join_ec2 -retry-join
retry_join_gce -retry-join
statsd_addr telemetry.statsd_address
statsite_addr telemetry.statsite_address
statsite_prefix telemetry.metrics_prefix
telemetry.statsite_prefix telemetry.metrics_prefix
(service definitions) serviceid service_id
(service definitions) dockercontainerid docker_container_id
(service definitions) tlsskipverify tls_skip_verify
(service definitions) deregistercriticalserviceafter deregister_critical_service_after

statsite_prefix 重命名为 metrics_prefix

由于应用于所有统计提供者的statsite_prefix配置选项,statsite_prefix已重命名为metrics_prefix。 升级到此版本的Consul时需要更新配置文件。

advertise_addrs已被删除

此配置选项已被删除,因为它与advertise_addr和advertise_addr_wan结合使用端口冗余,并错误地指出您可以配置主机和端口。

下线行为改变为go-discover Configs

使用go-discover云自动加入的-retry-join-retry-join-wan值的格式已更改。 key = val序列中的值不能再进行URL编码,只要它们不包含空格,反斜杠或双引号就可以作为文字提供。如果值包含这些字符,那么在"some key"= "some value"双引号字符串中的特殊字符可以用反斜杠\转义。

HTTP请求类型在许多HTTP API中被强制执行

以前使用任何HTTP请求类型的HTTP API中的许多端点现在检查特定的HTTP请求类型并强制执行它们。 这可能会破坏客户依靠旧的行为。 以下是更新的端点和所需的HTTP请求类型的完整列表:

Endpoint HTTP 请求类型
/v1/acl/info GET
/v1/acl/list GET
/v1/acl/replication GET
/v1/agent/check/deregister PUT
/v1/agent/check/fail PUT
/v1/agent/check/pass PUT
/v1/agent/check/register PUT
/v1/agent/check/warn PUT
/v1/agent/checks GET
/v1/agent/force-leave PUT
/v1/agent/join PUT
/v1/agent/members GET
/v1/agent/metrics GET
/v1/agent/self GET
/v1/agent/service/register PUT
/v1/agent/service/deregister PUT
/v1/agent/services GET
/v1/catalog/datacenters GET
/v1/catalog/deregister PUT
/v1/catalog/node GET
/v1/catalog/nodes GET
/v1/catalog/register PUT
/v1/catalog/service GET
/v1/catalog/services GET
/v1/coordinate/datacenters GET
/v1/coordinate/nodes GET
/v1/health/checks GET
/v1/health/node GET
/v1/health/service GET
/v1/health/state GET
/v1/internal/ui/node GET
/v1/internal/ui/nodes GET
/v1/internal/ui/services GET
/v1/session/info GET
/v1/session/list GET
/v1/session/node GET
/v1/status/leader GET
/v1/status/peers GET
/v1/operator/area/:uuid/members GET
/v1/operator/area/:uuid/join PUT

未经授权的KV请求返回403

启用ACL时,使用未经授权的令牌读取密钥将返回403.之前返回了404响应。

Agent自身端点的配置部分已更改

/v1/agent/self端点的Config部分经常被直接返回Consul的内部数据结构之一。 这个配置结构已经在DebugConfig下移动了,并且是为了调试使用而随时更改的文档,一小部分Config元素已经被维护和记录。 有关详细信息,请参阅配置端点文档

已弃用的configtest命令已删除

configtest命令已被弃用,并已由validate命令取代。

验证命令中的未记录标志已删除

validate命令支持-config-file和-config-dir命令行标志,但没有记录它们。 由于不需要标志,所以这个支持已被删除。

Metric名称已更新

Metric名称不再以consul.consul开头。 为了帮助转换仪表板和其他Metric消费者,enable_deprecated_names字段已经添加到配置的遥测部分,这将使旧命名方案的Metric与新的一起发送。 以下前缀受到影响:

Prefix
consul.consul.acl
consul.consul.autopilot
consul.consul.catalog
consul.consul.fsm
consul.consul.health
consul.consul.http
consul.consul.kvs
consul.consul.leader
consul.consul.prepared-query
consul.consul.rpc
consul.consul.session
consul.consul.session_ttl
consul.consul.txn

代理程序启动时检查已验证

Consul代理现在验证其配置中的运行状况检查定义,如果任何检查无效,将在启动时失败。 在之前的Consul版本中,无效健康检查会被跳过。

五、Consul 内部实现

这部分内容涵盖Consul内部的一些内容,如架构,一致性和 gossip协议,以及安全模式。
注意:了解Consul的内部是没有必要成功地使用它。 我们在这里记录,对于Consul如何工作是完全透明的。

5.1 Consul 架构

Consul是一个复杂的系统,有许多不同的灵活部件。 为了帮助Consul的用户和开发人员了解 Consul 是如何运行的,该页面记录了系统架构。
  高级话题! 这个页面涵盖了Consul内部的技术细节。 有效地运作和使用Consul不需要知道这些细节。 这些细节记录在这里为那些谁想要了解他们,而不需要通过源代码进行探讨。

一些专用术语

在描述架构之前,我们提供术语表来帮助澄清正在讨论的内容:

  • Agent——Agent是Consul集群中每个成员长时间运行的守护进程。它是通过运行consul agent启动的。Agent 可以运行在clientserver模式。由于所有节点都必须运行一个agent,因此将节点称为客户端或服务器更简单,但agent还有其他实例。所有agent都可以运行DNS或HTTP接口,并负责运行检查和保持服务同步。
  • Client——Client是将所有RPC转发给服务器的agent。client是相对无状态的。client执行的唯一后台活动是参与局域网gossip池。 这具有最小的资源开销并且仅消耗少量的网络带宽。
  • Server——Server是具有扩展职责的 agent,包括参与Raft仲裁,维护集群状态,响应RPC查询,通过广域网的 gossip与其他数据中心通讯,以及将查询转发给leader或远程数据中心。
  • Datacenter——虽然数据中心的定义似乎是显而易见的,但必须考虑一些细微的细节。例如,在EC2中,多个可用区域被认为是由一个数据中心组成的? 我们将数据中心定义为私有、低延迟和高带宽的网络环境。 这不包括通过公共互联网的通信,但为了我们的目的,单个EC2区域内的多个可用区域将被视为单个数据中心的一部分。
  • Consensus——在我们的文档中使用Consensus来表示对当选领导人的同意以及对交易顺序的协议。由于这些事务被应用于有限状态机,我们对Consensus的定义意味着复制状态机的一致性。 Consensus在Wikipedia上有更详细的描述,我们的实现在这里描述。
  • Gossip——Consul建立在Serf之上,它提供了一个完整的gossip协议用于多种目的。 Serf提供会员资格、失败检测和事件广播。 在Gossip文档中更多地描述了这些用法。 只要知道gossip涉及随机的节点到节点的通信就足够了,主要是通过UDP。
  • LAN Gossip——指包含全部位于同一局域网或数据中心的节点的局域网gossip池。
  • WAN Gossip——指仅包含服务器的WAN gossip池。 这些服务器主要位于不同的数据中心,通常通过互联网或广域网进行通信。
  • RPC——远程过程调用。 这是一个请求/响应机制,允许客户端发出服务器请求。
内部架构

从一万英尺的高度来看,Consul的架构是这样的:


Consul 架构

我们来分解这个图像并且描述每一个部分。 首先,我们可以看到有两个数据中心,分别是“一”和“二”。 Consul对多个数据中心有一流的支持,并期望这是常见的情况。
  在每个数据中心内,我们都有客户端和服务器的混合。 预计有三到五台服务器。 这在故障情况下的可用性和性能之间取得平衡,因为随着更多机器的添加,一致性逐渐变慢。 但是,客户数量没有限制,可以轻松扩展到数千或数万。
  数据中心内的所有节点都参与到gossip协议中。 这意味着有一个gossip池包含给定数据中心的所有节点。 这有几个目的:首先,不需要为客户端配置服务器的地址; 发现是自动完成的。 其次,检测节点故障的工作不是放在服务器上,而是分布式的。 这使得失败检测比原来的心跳计划更具可扩展性。 第三,它被用作消息层来通知重要事件,例如发生 leader选举。
  每个数据中心中的服务器都是单个Raft对等设备的一部分。 这意味着他们一起工作,选出一个单独的leader。 leader负责处理所有查询和交易。 交易也必须复制到所有同行作为一致性协议的一部分。 由于这个要求,当一个非leader服务器接收到一个RPC请求时,它将其转发给集群leader。
  服务器节点也作为WAN gossip池的一部分运行。 此池与局域网池不同,因为它针对较高的互联网延迟进行了优化,预计只包含其他Consul服务器节点。 这个池的目的是让数据中心以低调的方式发现彼此。 在线新建一个数据中心就像加入现有的广域网gossip一样简单。 因为服务器都在这个池中运行,所以它也支持跨数据中心的请求。 当服务器接收到对其他数据中心的请求时,会将其转发到正确的数据中心中的随机服务器。 该服务器可能会转发给自己的 leader。
  这导致数据中心之间的耦合度非常低,但由于故障检测,连接缓存和多路复用,跨数据中心的请求相对较快且可靠。
  一般来说,数据不会在不同的Consul数据中心之间复制。 当请求另一个数据中心中的资源时,本地Consul服务器将RPC请求转发给该资源的远程Consul服务器,并返回结果。 如果远程数据中心不可用,那么这些资源也将不可用,但这不会影响本地数据中心。 有一些特殊情况,可以复制有限的数据子集,例如Consul的内置ACL复制功能,或者外联工具(如consul-replicate)。

深入研究

在这一点上,我们已经介绍了Consul的高层架构,但是每个子系统都有更多的细节。 与gossip协议一样, 一致性协议也有详细文档。 所使用的安全模型和协议的文档也是可用的。
  有关其他详细信息,请查阅代码,在IRC中询问,或者联系邮件列表。

5.2 一致性协议

Consul使用一致性协议来提供一致性(由CAP定义)。一致性协议是基于“Raft:寻找一个可理解的一致性算法”。 有关Raft的视觉解释,请参阅数据的秘密生活。

Raft 协议概述

Raft是基于Paxos的一致性算法。与Paxos相比,Raft被设计为具有更少的状态和更简单,更易于理解的算法。
  在讨论Raft时有几个关键的术语:

  • Log——Raft系统的主要工作单位是日志条目。 一致性问题可以分解为复制日志。 日志是条目的有序序列。 如果所有成员对条目和顺序达成一致,我们认为日志是一致的。
  • FSM——有限状态机。 有限状态机是有限状态的集合,它们之间有转换。 随着新的日志被应用,FSM被允许在状态之间转换。 应用相同的日志序列必须导致相同的状态,这意味着行为必须是确定性的。
  • Peer set——对等集是参与日志复制的所有成员的集合。 为了Consul的目的,所有服务器节点都在本地数据中心的对等集中。
  • Quorum——法定人数是同行中的大多数成员:对于一组规模n,法定人数至少需要(n/2)+1个成员。 例如,如果对等集中有5个成员,则需要3个节点来形成法定人数。 如果由于某种原因导致法定数量的节点不可用,则集群变得不可用,并且不能提交新的日志。
  • Committed Entry——当一个条目持久地存储在一个法定的节点上时,这个条目被认为是提交的。 一旦条目被提交,就可以应用。
  • Leader——在任何给定的时间,对等组选择单个节点作为领导者。 领导者负责接收新的日志条目,复制到关注者,以及管理条目何时被提交。
      Raft是一个复杂的协议,在这里不会详细介绍(对于那些希望得到更全面了解的人,这篇文章有更详细介绍)。但是,我们会试图提供一个高层次的描述,这对于建立一个总体的认识可能是有用的。
      Raft 节点处于三种状态之一:follower, candidate, leader。所有节点最初都是作为follower开始的。在这个状态下,节点可以接受 leader 的日志条目和投票。如果一段时间内没有收到条目,则节点自我提升到candidate状态。在candidate状态,节点向同伴请求投票。如果candidate获得法定人数,则提升为 leader。leader 必须接受新的日志条目并且复制到其它的 follower 上。另外,如果陈旧的数据不可接受,则所有的查询也必须在 leader 上进行。
      一旦一个集群有一个 leader,它就能接受新的日志条目。客户端可以请求 leader 增加一个新的日志条目(从 Raft 角度来看,日志条目是一个不透明的二进制 blob)。然后,leader将条目写入持久存储,并尝试复制到其它的 follower 上面。一旦日志条目被认为是提交的,它可以应用到有限状态机。有限状态机是应用程序特定的;在 Consul 里,我们使用MemDB去维护集群的状态。Consul 在 committed 和 applied 时会进行写阻塞。在与查询的一致模式一起使用时,这实现了在写入语义之后的读取。
      显然,允许日志复制无限制的增长是不可取的。Raft 提供了一种机制,通过该机制,当前状态被快照并且日志被压缩。由于FSM抽象,恢复FSM的状态必须导致与旧日志重播相同的状态。这允许Raft在某个时间点捕获FSM状态,然后删除所有用来达到该状态的日志。这是在没有用户干预的情况下自动执行的,并且可以防止无限制的磁盘使用,同时最大限度地减少重新使用日志的时间。使用BoltDB的好处之一是它允许Consul继续接受新的事务,即使在旧状态被快照时,也防止了任何可用性问题。
      一致性直到到达法定人数都是容错的。如果法定人数的节点不可用,则是无法处理日志条目和同伴成员的理由。例如:假设只有两个对等体 A 和 B。则法定人数大小就是2,这意味着两个节点必须同意提交日志条目。如果 A 或 B 失败,现在不可能达到法定人数。这意味着集群无法添加或删除节点或提交任何其他日志条目。这导致不可用。此时,需要手动干预来移除 A 或 B,并且在引导模式下重新启动剩余的节点。
      3个节点的 Raft 集群可以容忍单个节点故障,而5个节点的集群可以容忍2个节点故障。推荐的配置是每个数据中心配置3或5个 Consul 服务器。这最大限度地提高可用性,而不会大大牺牲性能。下面的部署表总结了潜在的集群大小和每个集群的容错。
      在性能方面,Raft 与Paxos相当。假设稳定的领导,提交日志条目需要一次往返一半的群集。 因此,性能受到磁盘I / O和网络延迟的限制。尽管Consul并不是一个高吞吐量的写入系统,但它依赖于网络和硬件配置,每秒处理数百到数千个事务。
Consul 里的 Raft

只有Consul服务器节点参与Raft并且是对等集合的一部分。所有客户端节点都向服务器转发请求。这种设计的部分原因是,随着更多的成员被添加到对等设置,法定数量的大小也增加。 这会引起性能问题,因为您可能正在等待数百台机器同意进入而不是少数几台机器。
  开始时,一个Consul服务器被置于“bootstrap”模式。这种模式可以让自己当选领导者。 一旦领导者被选举出来,其他服务器可以被添加到对等组中,以保持一致性和安全性。 最后,一旦添加了前几个服务器,引导模式可以被禁用。更详细的介绍参考这个文档

  由于所有服务器都作为对等设备的一部分参与,他们都知道当前的领导者。 当RPC请求到达非领导服务器时,请求被转发给领导。 如果RPC是查询类型,意味着它是只读的,那么领导者将根据FSM的当前状态生成结果。 如果RPC是一个事务类型,这意味着它修改了状态,那么领导者会生成一个新的日志条目并使用Raft来应用它。 一旦日志条目被提交并应用到FSM,交易就完成了。
  由于Raft复制的性质,性能对网络延迟很敏感。 出于这个原因,每个数据中心选择一个独立的领导者,并维护一个不相交的对等集。 数据由数据中心分区,因此每个领导者只负责数据中心的数据。 当收到远程数据中心的请求时,请求被转发给正确的领导。 这种设计允许更低的延迟交易和更高的可用性而不牺牲一致性。

一致性模式

尽管所有对复制日志的写入都是通过Raft进行的,但读取更灵活。 为了支持开发人员可能需要的各种权衡,Consul支持3种不同的读取一致性模式。
  三种读取模式是:

  • default。参考这篇文章为 Raft 引入 leader lease 机制解决集群脑裂时的 stale read 问题
  • consistent。这种模式是非常一致的,没有告诫。 它要求一个领导者与一个同事的法定人数进行核实,它仍然是领导者。 这引入了所有服务器节点的额外往返。 折衷总是一致的读取,但由于额外的往返导致延迟增加。
  • stale。这种模式允许任何服务器为读取服务,而不管它是否是领导者。 这意味着读取可以任意陈旧,但通常在领导者的50毫秒内。 权衡是非常快速和可扩展的读取,但陈旧的价值。 这种模式允许没有领导者的读取意味着不可用的集群仍然能够响应。
      有关使用这些不同模式的更多文档,参考HTTP API
部署表

以下是显示各种群集大小的法定大小和容错的表格。 推荐的部署是3或5个服务器。 一个单一的服务器部署是非常不鼓励的,因为在故障情况下数据丢失是不可避免的。

Servers Quorum Size Failure Tolerance
1 1 0
2 2 0
3 2 1
4 3 1
5 3 2
6 4 2
7 4 3

5.3 Gossip Protocol

Consul使用Gossip Protocol来管理成员资格并向集群广播消息。所有这些都是通过使用Serf库提供的。Serf使用的Gossip Protocol是基于"SWIM: Scalable Weakly-consistent Infection-style Process Group Membership Protocol",并进行了一些小修改。Serf's protocol详情请看这里。

Gossip in Consul

领事使用两个不同的gossip池。 我们将每个池分别称为LAN或WAN池。 每个Consul运行的数据中心都有一个包含数据中心所有成员(包括客户端和服务器)的LAN gossip池。 LAN池用于几个目的。 成员信息允许客户端自动发现服务器,减少所需的配置量。 分布式故障检测允许整个集群共享故障检测工作,而不是集中在几台服务器上。 最后,gossip池允许可靠和快速的事件广播,如领导人选举事件。
  WAN池是全球唯一的,因为无论数据中心如何,所有服务器都应该参与WAN池。 WAN池提供的成员资格信息允许服务器执行跨数据中心请求。 集成的故障检测功能使Consul可以正常处理丢失连接的整个数据中心,或者只处理远程数据中心内的单个服务器。
  所有这些功能都是利用Serf提供的。 它被用作嵌入式库来提供这些功能。 从用户的角度来看,这并不重要,因为抽象应该被Consul所掩盖。 作为一名开发人员,了解这个库如何被利用是非常有用的。

Lifeguard Enhancements

SWIM假定本地节点是健康的,因为软实时处理包是可能的。 但是,如果本地节点遇到CPU或网络耗尽,则可能违反此假设。 其结果是,serfHealth检查状态可能会偶尔发生抖动,导致错误的监视警报,增加了遥测的噪声,并导致整个群集浪费CPU和网络资源来诊断可能不存在的故障。
  Lifeguard通过SWIM的新型增强完全解决这个问题。
  有关Lifeguard的更多详情,请阅读Making Gossip More Robust with Lifeguard这篇博客文章,其中提供了HashiCorp研究论文的高级概述Lifeguard : SWIM-ing with Situational AwarenessSerf gossip protocol guide还提供了有关gossip protocol 和 Lifeguard的一些较低层次的细节。

5.4 Network Coordinates

Consul使用网络层析成像系统来计算集群中节点的网络坐标。 这些坐标允许使用非常简单的计算在任何两个节点之间估计网络往返时间。 这允许许多有用的应用程序,例如查找离请求节点最近的服务节点,或者故障转移到下一个最近的数据中心中的服务。
  所有这些都是通过使用Serf库提供的。 Serf的网络层析成像基于"Vivaldi: A Decentralized Network Coordinate System",并基于其他研究进行了一些改进。 这里有关于Serf的网络坐标的更多细节。

Network Coordinates in Consul

Consul内部的网络坐标清单有几种形式:

  • consul rtt命令可用于查询任意两个节点之间的网络往返时间。
  • 目录端点运行状况端点可以使用"?near="参数根据给定节点的网络往返时间对查询结果进行排序。
  • 准备好的查询可以根据网络往返时间自动将服务故障转移到其他Consul数据中心。 有关示例,请参阅地理故障转移。
  • 坐标端点显示原始网络坐标以用于其他应用程序。
      Consul使用Serf来管理两个不同的gossip池,一个是给定数据中心成员的局域网,另一个是由所有数据中心的Consul服务器组成的WAN。 请注意,这两个池之间的网络坐标不兼容。 局域网坐标只有在与其他局域网坐标进行计算时才有意义,而广域网坐标只对其他广域网坐标有意义。
使用坐标

一旦有了它们的坐标,计算任何两个节点之间估计的网络往返时间就很简单。 以下是从坐标端点返回的示例坐标。

    "Coord": {
        "Adjustment": 0.1,
        "Error": 1.5,
        "Height": 0.02,
        "Vec": [0.34,0.68,0.003,0.01,0.05,0.1,0.34,0.06]
    }

所有值都是以秒为单位的浮点数,除了不用于距离计算的误差项。
  Go中有一个完整的例子,展示了如何计算两个坐标之间的距离:

import (
    "math"
    "time"

    "github.com/hashicorp/serf/coordinate"
)

func dist(a *coordinate.Coordinate, b *coordinate.Coordinate) time.Duration {
    // Coordinates will always have the same dimensionality, so this is
    // just a sanity check.
    if len(a.Vec) != len(b.Vec) {
        panic("dimensions aren't compatible")
    }

    // Calculate the Euclidean distance plus the heights.
    sumsq := 0.0
    for i := 0; i < len(a.Vec); i++ {
        diff := a.Vec[i] - b.Vec[i]
        sumsq += diff * diff
    }
    rtt := math.Sqrt(sumsq) + a.Height + b.Height

    // Apply the adjustment components, guarding against negatives.
    adjusted := rtt + a.Adjustment + b.Adjustment
    if adjusted > 0.0 {
        rtt = adjusted
    }

    // Go's times are natively nanoseconds, so we convert from seconds.
    const secondsToNanoseconds = 1.0e9
    return time.Duration(rtt * secondsToNanoseconds)
}

5.5 Sessions

Consul提供了一个可用于构建分布式锁的会话机制。 会话充当节点之间的绑定层,运行状况检查和键/值数据。 它们旨在提供细粒度锁定,并深受The Chubby Lock Service for Loosely-Coupled Distributed Systems的启发。

Session Design

Consul中的一个会话代表一个具有非常特定语义的合约。 当构建会话时,可以提供节点名称,健康检查列表,行为,TTL和锁定延迟。 新构建的会话提供了一个可用于标识的命名标识。 该ID可以与KV存储一起使用以获取锁定:用于相互排斥的咨询机制。
  以下是显示这些组件之间的关系的图表:


image.png

Consul所提供的合约,在下列情况下, 会话将失效:

  • 节点被注销
  • 任何健康检查被取消注册
  • 任何健康检查进入临界状态
  • 会话被明确销毁
  • TTL到期,如果适用
      当会话失效时,会话被破坏,不能再使用。 关联的锁发生什么取决于在创建时指定的行为。 Consul支持发布和删除行为。 如果没有指定,则释放行为是默认行为。
      如果正在使用释放行为,则释放与该会话关联的任何锁,并且该密钥的ModifyIndex递增。 或者,如果使用删除行为,则简单地删除与任何保持锁相对应的密钥。 这可以用来创建Consul自动删除的临时条目。
      虽然这是一个简单的设计,但却可以实现多种使用模式。 默认情况下,基于gossip的故障检测器被用作关联的健康检查。 这个失败检测器允许Consul检测一个正在锁定的节点何时失败并自动释放锁定。 这种能力为Consul锁提供活力。 即在失败的情况下,系统可以继续取得进展。 但是,由于没有完美的故障检测器,所以即使锁拥有者仍然活着,也可能产生误报(检测到故障),从而导致锁被释放。 这意味着我们正在牺牲一些安全。
      相反,可以创建没有关联健康检查的会话。 这消除了假阳性的可能性,并且为了安全而进行交易。 你绝对可以肯定,即使现有业主失败,Consul也不会解锁。 由于Consul API允许会话强制销毁,因此可以构建系统,在发生故障的情况下需要操作员进行干预,同时排除了脑裂的可能性。
      第三种健康检查机制是会话TTL。 创建会话时,可以指定TTL。 如果TTL时间间隔过期而没有更新,则会话已经过期并触发失效。 这种类型的故障检测器也被称为心跳故障检测器。 它比基于gossip的故障检测器的可扩展性低,因为它增加了对服务器的负担,但在某些情况下可能适用。 TTL的合同是代表失效的下限; 也就是说Consul在达到TTL之前不会过期会话,但是允许延迟超过TTL的期限。 TTL在会话创建,会话更新和领导者故障切换时被更新。 使用TTL时,客户端应该了解时钟偏移问题:即客户端上的时间不能像Consul服务器上那样以相同的速率进行。 最好设置保守的TTL值,并在TTL之前更新以解决网络延迟和时间偏差。
      最后的细微差别是会话可能会提供锁定延迟。 这是一个持续时间,介于0到60秒之间。 当会话失效发生时,Consul防止在锁定延迟时间间隔内重新获得之前保存的任何锁定; 这是Google的Chubby所鼓舞的保障。 这种延迟的目的是让潜在的现场领导者检测到失效并停止处理可能导致不一致状态的请求。 虽然不是一个防弹的方法,但它确实避免了将睡眠状态引入到应用程序逻辑中的需要,并且可以帮助减轻许多问题。 默认情况下是使用15秒的延迟,客户端可以通过提供零延迟值来禁用此机制。
K/V集成

KV存储和会话之间的整合是会话使用的主要场所。 会话必须在使用之前创建,然后通过其ID来引用。
  KV API被扩展为支持获取和发布操作。 获取操作的行为类似于“检查和设置”操作,只有在没有现有锁持有者(当前锁持有者可以重新获取,见下文)时才能成功。 成功时,会有一个正常的密钥更新,但LockIndex也有一个增量,Session值会更新以反映持有该锁的会话。
  如果在获取期间锁已经被给定的会话持有,那么LockIndex不会递增,而是关键内容被更新。 这使得当前锁持有者可以更新关键内容,而不必放弃锁并重新获取它。
  一旦举行,锁可以释放使用相应的释放操作,提供相同的会话。 再次,这就像一个检查和设置操作,因为如果给定一个无效的会话,请求将失败。 一个重要的注意事项是锁可以被释放,而不是会话的创建者。 这是设计上的,因为它允许运营商进行干预,并在必要时强制终止会话。 如上所述,会话失效也会导致所有持有的锁被释放或删除。 当一个锁被释放时,LockIndex不会改变; 但是,会话被清除并且ModifyIndex递增。
  这些语义(大量借用Chubby)允许(Key,LockIndex,Session)的元组作为唯一的“音序器”。 该顺控程序可以被传递并用于验证请求是否属于当前锁持有者。 因为每次获取都会增加LockIndex,即使同一个会话重新获取一个锁,排序器也能够检测到一个陈旧的请求。 同样,如果一个会话是无效的,与给定的LockIndex对应的Session将是空白的。
  要清楚的是,这个锁定系统是纯粹的咨询。 没有强制执行,客户必须获得一个锁来执行任何操作。 任何客户端都可以读取,写入和删除一个密钥,而不必拥有相应的锁。 领事的目标不是保护不当客户。

Leader Election

会话提供的原语和KV存储的锁定机制可用于构建客户端领导者选举算法。 这些在leader选举指南中有更详细的介绍。

Prepared Query Integration

准备好的查询可以附加到会话中,以便在会话失效时自动删除准备好的查询。

5.6 Anti-Entropy

Consul使用维护服务和健康信息的先进方法。本页详细介绍了如何注册服务和检查,如何填充目录以及在更改时更新健康状态信息的方式。

组件

首先要了解服务和健康检查中涉及的组件:Agent和catalog。这些在下面的概念描述,使Anti-Entropy更容易理解。

Agent

每个Consul Agent维护自己的一套服务和检查注册以及健康信息。Agent负责执行自己的健康检查并更新其本地状态。
  Agent上下文中的服务和检查具有丰富的可用配置选项。 这是因为Agent负责通过使用健康检查来生成关于其服务及其健康的信息。

Catalog

Consul的服务发现由服务目录支持。该目录是通过汇总Agent提交的信息而形成的。该目录维护集群的高级视图,包括哪些服务可用,哪些节点运行这些服务,健康信息等等。该目录用于通过Consul提供的各种接口公开这些信息,包括DNS和HTTP。
  与Agent相比,目录上下文中的服务和检查具有更为有限的一组字段。 这是因为该目录仅负责记录和返回有关服务,节点和运行状况的信息。
  该目录仅由服务器节点维护。这是因为目录是通过Raft日志复制的,以提供对集群的统一和一致的视图。

Anti-Entropy

熵是系统日益混乱的趋势。Consul的反熵机制旨在对付这种趋势,甚至通过其组成部分的失败来保持集群的状态。
  如上所述,Consul在全局目录和Agent 本地之间有明确的分离。反熵机制调和了这两种世界观:反熵是本地Agent与目录的同步。 例如,当用户注册新服务或与Agent核对时,Agent会依次通知目录该新的支票存在。 同样,从代理中删除支票时,也会从目录中删除支票。
  反熵也被用来更新可用性信息。 当代理商运行健康检查时,他们的状态可能会改变,在这种情况下,他们的新状态将被同步到目录中。 使用这些信息,目录可以根据其可用性,智能地响应有关其节点和服务的查询。
  在此同步过程中,还会检查目录的正确性。 如果目录中存在Agent不知道的任何服务或检查,它们将被自动删除,以使目录反映该Agent的适当的一组服务和运行状况信息。 Consul将Agent的状态视为权威;如果Agent和目录视图之间有任何差异,则将始终使用Agent本地视图。

定期同步

除了在对Consul进行更改时运行外,反熵也是一个长期运行的过程,它会定期唤醒以同步服务并检查目录中的状态。 这可以确保目录与Agent的真实状态紧密匹配。 这也使Consul能够重新填充服务目录,即使在数据丢失的情况下也是如此。
  为避免饱和,定期反熵运行之间的时间量将根据集群大小而变化。 下表定义了集群大小和同步间隔之间的关系:

Cluster Size Periodic Sync Interval
1 - 128 1 minute
129 - 256 2 minutes
257 - 512 3 minutes
513 - 1024 4 minutes
... ...

上面的时间间隔是近似的。每个Consul代理将在间隔窗口内选择一个随机错开的开始时间,以避免雷鸣般的牛群。

尽力而为的同步

在一些情况下,反熵可能会失败,包括代理程序或其运行环境配置错误,I / O问题(完整磁盘,文件系统权限等),网络问题(代理程序无法与服务器通信)等等。 因此,代理尝试以尽力而为的方式进行同步。
  如果在反熵运行期间遇到错误,则会记录错误,并且代理继续运行。 定期运行反熵机制以自动从这些类型的瞬态故障中恢复。

EnableTagOverride

服务注册的同步可以被部分修改以允许外部代理改变服务的标签。 这在外部监视服务需要成为标签信息的真实来源的情况下非常有用。 例如,Redis数据库及其监控服务Redis Sentinel就有这种关系。 Redis实例负责其大部分配置,但Sentinel确定Redis实例是主要还是次要实例。 使用Consul服务配置项EnableTagOverride,您可以指示运行Redis数据库的Consul代理在反熵同步期间不更新标签。 有关更多信息,请参阅服务页面。

5.7 安全模型

Consul依靠轻量级gossip机制和RPC系统来提供各种功能。这两个系统都有不同的安全机制。然而,Consul的安全机制有一个共同的目标:提供机密性,完整性和认证
  gossip协议Serf提供支持,Serf使用对称密钥或共享密钥加密系统。 这里有更多关于Serf安全的细节。 有关如何在Consul中启用Serf的gossip加密的详细信息,请参阅此处的加密文档
  RPC系统支持使用端对端的TLS和可选的客户端认证。 TLS是广泛部署的非对称密码系统,是网络安全的基础。
  这意味着Consul沟通是防止窃听,篡改和欺骗。 这使得Consul可以通过不受信任的网络(如EC2和其他共享主机提供商)运行。

威胁模型

以下是我们的威胁模型的各个部分:

  • 非会员可以访问数据
  • 由于恶意消息而导致集群状态操作
  • 由于恶意消息而产生假数据
  • 篡改造成国家腐败
  • 针对节点的拒绝服务
      另外,我们认识到,可以长时间观察网络流量的攻击者可以推断集群成员。 Consul使用的gossip机制依赖于向随机成员发送消息,因此攻击者可以记录所有目标并确定群集的所有成员。
      在将安全性设计为系统时,将其设计为适合威胁模型。 我们的目标不是保护绝密数据,而是提供一个“合理的”安全级别,要求攻击者投入大量的资源来击败。
网络端口

有关配置网络规则以支持Consul,请参阅Consul使用的端口列表以及使用哪些功能的详细信息。

5.8 Jepsen Testing

Jepsen是由Kyle Kingsbury编写的一个工具,旨在测试分布式系统的分区容差。 它创建网络分区,同时随机操作模糊系统。 分析结果,看看系统是否违反了它声称拥有的任何一致性属性。
  作为我们Consul测试的一部分,我们运行了一个Jepsen测试来确定是否可以发现任何一致性问题。 在我们的测试中,Consul从分区中恢复正常,没有引入任何一致性问题。

Running the tests

目前,使用Jepsen进行测试相当复杂,因为它需要设置多个虚拟机,SSH密钥,DNS配置以及一个可用的Clojure环境。 我们希望尽快为我们的Consul测试代码贡献一份测试代码,并为Jepsen测试提供一个Vagrant环境。

Output

以下是Jepsen的输出。 我们多次跑Jepsen,每次都是通过的。 这个输出仅代表一次运行。

六、Consul命令(CLI)

Consul命令(CLI),太长简书放不下。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容