Consul内核
简介
这篇文档将介绍Consul的内核知识,想要在生产环境中正确地使用Consul,必须了解其内部原理,我们会从以下几个方面了解Consul的运行方式:
- 架构
- Consensus 协议
- Gossip 协议
- 网络协作
- 会话
- 反熵(Anti-Entropy)
- 安全模式
- Jepsen 测试
架构
Consul是一个复杂的系统,具有许多不同的活动组件,为了帮助Consul用户及开发人员更好的了解其工作模型,我们在本章节中会描述Consul的系统体系结构。
在讨论体系结构之前,我们建议用户先阅读术语表
,了解Consul的一些概念术语,在生产中部署Consul时,可以结合本文档做一些参考。
俯瞰图
Consul的架构俯瞰图如下:
我们分解一下图像,对每个部分逐一进行描述。首先,我们可以看到有两个数据中心,分别标记为DATACENTER 1
和DATACENETR 2
。 Consul对多个数据中心提供一流的支持,并且在生产环境是常见现象。
在每个数据中心内,都有一些客户端和服务端机器。服务端机器推荐使用3台或5台,在出现故障的情况下,可用性和性能之间容易取得平衡,如果服务端添加更多服务器,那么服务器间达成Consensus
协议会比较慢。然而,客户端服务器数量没有限制,他们可以轻松扩展到成千上万个。
数据中心中的所有代理都会参与Gossip
协议,这会建立一个Gossip Pool
,其中包含给定数据中心的所有代理。这样做有几个目的:首先,不需要为客户端配置服务端地址,地址发现是自动完成的;其次,检测代理故障的工作没有局限在服务端,而是分散处理了,这使得故障检测机制相比心跳
方案更具可伸缩性,它还为节点提供故障检测,如果代理不可访问,则说明该节点可能已发生故障;第三,它用作传递消息,在发生重要事件时发出通知(例如,领导者选举)。
每个数据中心中的服务端都是单个Raft对等集
的一部分。这意味着它们将共同选举一个领导者(该领导者机器具有一些额外职责)。领导者负责处理所有查询
和事务
。作为Consensus
协议的一部分,事务必须复制到所有片区(peer)
,所以当非领导服务器收到RPC请求时,它将先转发给群集领导者。
服务器代理还充当WAN gossip pool
的一部分运行,该池与LAN池
不同,因为它针对Internet
的更高延迟进行了优化,并且将仅包含其他Consul服务端代理。该池的目的是允许数据中心以低接触(low-touch)
的方式发现彼此。在线连接新的数据中心就像加入现有的WAN gossip pool
一样容易。由于服务端都在该池中运行,因此它也支持跨数据中心(cross-datacenter)
请求。服务端收到对其他数据中心的请求时,会将其转发到正确数据中心中的随机服务器,该服务器然后可以转发给本地领导者。
这样,数据中心之间的耦合会非常低,但是由于故障检测
,连接缓存
和多路复用
等机制,跨数据中心的请求相对较快且可靠。
通常情况下,不同的Consul数据中心之间不会复制数据。当请求另一个数据中心中的资源时,本地Consul服务器会将RPC请求转发到该资源的远程Consul服务器并返回结果。如果远程数据中心不可用,那么那些资源也将不可用,但是不会以其他方式影响本地数据中心。在某些特殊情况下,可以复制有限的数据子集,例如使用Consul的内置ACL复制功能,或使用外部工具(如consul-replicate
)。
在某些地方,客户端代理可能会缓存
来自服务端的数据,以提高本地的性能和可靠性。示例包括连接证书
和意图(intention)
,这些证书和意图使客户端代理可以在无需请求服务器的情况下就做出相应。一些API端点还支持缓存结果可选,这也有助于提高可靠性,因为即使与服务器连接中断,或服务器暂时不可用时,本地代理也可以使用缓存继续响应某些查询,例如服务发现或者链接授权。
Consensus 协议
Consul使用Consensus协议
来提供一致性(由CAP定义)。 Consensus协议
基于Raft协议
,有关Raft协议
的直观说明,可以参考http://thesecretlivesofdata.com/raft/
Raft 协议简介
Raft协议
是基于Paxos
算法的,与Paxos相比,Raft具有更少状态,更简单,也更易理解,在讨论Raft时,首先需要了解一些专业术语:
-
日志(Log)
:Raft系统中的主要工作单元是日志记录,一致性问题可以分解为日志副本。日志是记录的有序序列,记录包括任意群集更改:添加节点
,添加服务
,新建键值对
等。如果所有成员都认可记录及其顺序,则我们认为日志是一致的。 -
有限状态机(FSM)
:FSM是有限状态集合,启用新日志时,FSM状态会跟随着切换,应用相同的日志序列最后会得到相同的状态,这意味着行为有必然性。 -
副本集(Peer Set)
:副本集是指所有参与日志复制的成员集合,在Consul中,所有服务端节点都在本地数据中心的副本集中。 -
仲裁人数(Quorum)
:仲裁人数对应副本集中的大多数成员:对于大小为n
的集合,至少需要(n + 1/ 2
个节点。例如,如果在副本集中有5个成员,则需要3个节点来形成仲裁。如果由于某些原因无法达到法定数量的节点,则群集将变得不可用,并且无法提交新日志。 -
提交的记录(Committed Entry)
:当提交记录存储在一定数量的节点上时,该记录被视为已提交,记录提交后即可应用。 -
领导者(Leader)
:在任何时间,副本集都会选择一个节点作为领导者,领导者会负责拉取新的日志记录,并将其复制到跟随者(follower)
,最后提交日志。
Raft
是一个复杂的协议,此处不做详细介绍,我们仅从更高层次对其构建的框架稍加描述。
Raft节点
有三种状态:跟随者(follower)
、候选者(candidate)
或领导者(leader)
。所有节点的最初状态为跟随者,在这种状态下,节点可以接受来自领导者的日志记录并进行投票。如果一段时间未收到任何记录,则节点会自动升级为候选状态。在候选状态下,节点会请求其他节点等待投票。如果候选人获得法定人数的选票,则将晋升为领导人。领导者必须接受新的日志记录并复制到所有其他关注者。此外,如果读取过时数据(stale reads)
是不可接受的,那么所有查询也必须在领导者上执行。
当集群拥有领导者后,就可以接受新的日志记录。客户端可以请求领导者添加新日志记录(从Raft的角度来看,日志记录是二进制blob格式
)。然后,领导者会将记录写入持久性存储,并尝试复制到一定数量的跟随者。一旦日志记录被认为已提交,就可以将其应用到有限状态机。有限状态机是专用的,在Consul中,我们使用MemDB
维护集群状态。Consul在提交并使用日志记录后再执行写操作,查询使用强一致性模型时,可以实现写后再读。
显然,我们不希望备份的日志无限制地增长。 Raft提供了一种机制,通过该机制可以对当前状态捕捉快照并压缩日志。由于FSM抽象,恢复FSM状态需要重播老的日志记录,这使Raft可以在某个时间点捕获FSM状态,然后删除用于达到该状态的所有日志。这些是自动执行的,无需用户干预,这样可以防止磁盘无限制使用,同时还可以最大程度地减少重播日志所花费的时间。使用MemDB
的优点之一是,即使对旧状态打快照时,它也允许Consul继续接受新事务,从而避免了任何可用性问题。
Consensus 协议
是可以容错的,前提是需要达到法定人数下限。如果没有达到法定节点数,则无法处理日志记录。例如,假设只有2个节点:A和B,那么仲裁人数也为2,这意味着两个节点必须都同意提交日志记录。如果A或B失败,则现在无法达到法定人数,这意味着群集无法添加或删除节点,也无法提交任何其他日志记录,这将导致集群不可用。此时,将需要手动干预才能删除A或B节点,并以引导程序模式重新启动其余节点。
3个节点的Raft群集可以容忍单个节点故障,而5个节点的Raft群集可以容忍2个节点故障。推荐的配置是每个数据中心运行3或5个Consul服务器。这样可以在不严重降低性能的情况下最大化可用性。在部署参考一节中总结了可用的群集大小选项以及每个选项的容错能力。
在性能方面,Raft
可以媲美Paxos
。假设领导者稳定,提交日志记录需要单次往返集群一半机器。因此,性能受磁盘I/O
和网络延迟
的约束。尽管Consul并非旨在成为高吞吐量写入系统,但它应根据网络和硬件配置每秒处理数百至数千个事务。
Consul中的Raft协议
只有Consul服务端节点参与了Raft协议,并作为复制集的一部分。所有客户端节点都将请求转发到服务端。这样设计的部分原因是,随着将更多成员添加到复制集,仲裁的数量也会增加,这会引入性能问题,因为用户可能在等待数百台机器就一项输入达成共识,而不是一小部分。
开始时,单个Consul服务器将进入引导(bootstrap)
模式。此模式可以选举自己为领导者。选出领导者后,可以将其他服务器添加到复制集,以保持一致性和安全性,一旦添加了头几台服务器,就可以禁用引导模式了。
由于所有服务端机器都作为复制集的一部分,因此它们都知道当前的领导者是谁。当RPC请求到达非领导者服务器时,它们会将请求转发给领导。如果RPC是查询(query)
类型,则表示它是只读的,领导程序将根据FSM的当前状态生成结果。如果RPC是事务(transaction)
类型,这意味着它会修改状态,则领导者将生成一个新的日志记录,并使用Raft应用它。一旦提交了日志记录并将其应用于FSM,事务就完成了。
由于Raft复制的性质,性能对网络延迟很敏感。因此,每个数据中心选举一个独立的领导者,并维护一个不相交的复制集。数据按数据中心进行分区,因此每个领导者仅负责其数据中心中的数据。收到对远程数据中心的请求后,该请求将转发给正确的领导者。这种设计可在不牺牲一致性的情况下实现较低的事务延迟和较高的可用性。
一致性模式
尽管所有复制日志的写入都通过Raft协议
,但读取操作比较灵活,为了支持开发人员可能需要的各种折衷方案,Consul支持3种不同的读取一致性模式,分别是:
-
默认(default)
:Raft
利用领导者的租赁(leasing)
,提供了一个时间窗口,领导者在该时间窗口内假设为稳定状态。但是,如果将领导者与其余服务器分开,则可以在旧领导者持有租约的同时选出新领导者。这意味着有2个领导节点,由于老领导者将无法提交新日志,因此不会出现脑裂的风险。但是,如果旧的领导者提供任何读取服务,则这些值可能会过时。默认的一致性模式仅依赖于领导者租赁,使客户可能读取到过时的数据。此模式下读取速度快,通常具有很强的一致性,仅在非常意外的情况下才会发生数据过时。 -
强一致(consistent)
:该模式可以提供强一致性,它要求领导者及其仲裁者核实它依然是领导者,这为所有服务器节点都引入了额外的往返。此模式下,读取的数据始终是一致的,但会因为额外的交互增加延迟。 -
弱一致(stale)
:该模式下,任何服务器都可以提供查询服务,不管其是否为引导服务器。这意味着读取的数据可能是非常陈旧的,但通常在领导者50ms范围内。此模式下,查询非常迅速并支持横向扩展,但是可能会查询到过时的值。此模式允许无领导下的读取,这意味着不可用的群集仍将能够响应请求。
部署参考
下面的表格显示了不同服务器个数的集群的仲裁数大小(quorum)
和容错能力(failure tolerance)
,服务器推荐使用3台或者5台,不建议使用单台服务器部署,这样在发生故障时,不可避免会丢失数据。
服务器数 | 仲裁数 | 容错能力 |
---|---|---|
1 | 1 | 0 |
2 | 2 | 0 |
3 | 2 | 1 |
4 | 3 | 1 |
5 | 3 | 2 |
6 | 4 | 2 |
7 | 4 | 3 |
Gossip 协议
Consul使用Gossip协议
来管理成员资格并向集群广播消息,所有这些都是通过使用Serf库
来实现的。 Serf使用的Gossip协议基于SWIM
:可扩展的弱一致性感染型过程组成员身份协议(Scalable Weakly-consistent Infection-style Process Group Membership Protocol)
,并进行了一些小改动,有关Serf的更多细节可以参考其他文档。
Consul中的Gossip协议
Consul中维护两个不同的Gossip Pool
,分别局域网池(LAN pool)
和广域网池(WAN pool)
。每个Consul数据中心内部都有一个局域网池(LAN pool)
,其中包含了数据中心的所有成员(客户端
和服务端
)。局域网池(LAN pool)
有多种用途,成员资格信息(Membership Information)
使客户端可以自动发现服务端,从而减少了所需的配置量。分布式故障检测允许故障检测工作在整个群集中共享,而不是集中在几台服务器上。最后,Gossip Pool
支持进行可靠且快速的事件广播。
广域网池(WAN pool)
在全局范围内是唯一的,所有服务端机器都参与WAN pool
,与数据中心无关。 广域网池提供的成员资格信息允许服务器执行跨数据中心请求,集成的故障检测机制使Consul可以优雅地处理整个数据中心失去连接的情况,或仅处理远程数据中心中的单个服务器异常。
Lifeguard 增强
所有这些功能都是通过使用Serf
第三方库提供的。从用户的角度来看,这些并不重要,因为这些都被Consul覆盖了。但是,对于开发人员而言,了解如何利用该库可能会比较有用。
SWIM
假定本地节点是健康的,从某种意义上来说,可以对数据包进行软实时处理(real-time processing)
。但是,在本地节点遇到CPU或网络耗尽的情况下,可能会违反此假设。 结果是,serfHealth
检查状态可能会发生抖动,从而会导致错误的监控报警,给遥测增加噪音,并导致整个群集浪费了CPU和网络资源。
通过对SWIM进行增强,Lifeguard
可以完全解决此问题,有关Lifeguard
的更多细节可以参考其他文档。
网络坐标
Consul使用网络层析成像(network tomography)
系统为群集中的节点计算网络坐标。这些坐标允许使用非常简单的算法来估计任意两个节点之间的网络往返时间。这个功能可以构建很多实用的功能,例如查找离请求节点最近的服务节点,或将故障转移到离当前节点最近的数据中心。
所有这些都是通过使用Serf库
实现的。Serf的网络断层扫描基于Vivaldi(分散式网络坐标系)
,并在其基础上进行了一些改进。
Consul中的网络坐标
网络坐标在Consul内部表现为以下几种方式:
-
consul rtt
命令可以查询任何两个节点之间的网络往返时间; -
Catalog Endpoint
和Health Endpoint
可以拼接?near=
参数,根据给定节点的网络往返时间对查询结果进行排序; -
Prepared Queries
可以根据网络往返时间自动将服务故障转移到其他Consul数据中心; -
Coordinate Endpoint
公开原始网络坐标以供其他应用程序使用。
Consul使用Serf
管理两个不同的Gossip Pool
,一个是用于管理本地数据中心的LAN
,另一个是用于管理所有数据中心的WAN
。重要的是要注意,这两个池之间的网络坐标不兼容。LAN坐标
仅在与其他LAN坐标
一起计算时才有意义,而WAN坐标
也仅在与其他WAN坐标
一起使用才有意义。
使用网络坐标
一旦有了它们的坐标,就可以很容易计算出任意两个节点之间的估计网络往返时间,这是从Coordinate Endpoint
返回的示例坐标:
"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)
}
会话
Consul提供了一种会话机制,可用于构建分布式锁。会话充当节点(node)
,健康检查(health check)
和键/值数据集
之间的绑定层。它们旨在提供细粒度的锁策略,这个概念大部分来源自http://research.google.com/archive/chubby.html
。
会话设计
Consul中的会话包含许多概念信息,当创建完会话后,会有对应的节点名称(node name)
、健康检查列表(health check)
、行为(behavior)
、TTL
和锁定延迟(lock-delay)
。新建会话可以得到一个唯一ID,该ID可以与KV集合一起使用可以获取锁(用于互斥机制)。
下图显示了这些组件之间的关系:
当发生以下任何一种情况时,会话将失效:
- 节点被注销
- 所有健康检查都被注销
- 所有健康检查都进入预警状态
- 会话被显式销毁
- TTL过期
当会话失效后,该会话会被销毁,并无法再使用。与会话关联的锁可能会发生多种情况,这取决于创建锁时指定的行为,Consul支持释放
和删除
两种行为,默认是选择释放
。
如果选择释放
行为,那么与会话关联的所有锁都将被释放,并且密钥的ModifyIndex
会累加;如果选择删除
行为,则会删除所有与之对应的密钥,这可以用于创建由Consul自动删除的临时项(ephemeral entry)
。
尽管这是一个简单的设计,但它可以实现多种使用模式。默认情况下,基于Gossip 协议
的故障检测器用于健康检查,该故障检测器使Consul可以检测到持有锁的节点何时发生故障,并自动释放锁。此功能为Consul锁机制提供了生命力,也就是说,在发生错误的情况下,系统可以继续维持运转。但是,也因为没有更完善的故障检测机制,有时即使锁拥有者仍然存活着,也可能会产生误报(检测到故障),这会导致锁被意外释放,这样我们机会牺牲一些安全性
。
相反,可以创建不关联任何健康检查的会话,这样就消除了误报的可能性,以可用性
换取安全性
。用户可以确定的是,即使现在持有锁的节点发生故障,Consul也不会释放锁。由于Consul API允许显式销毁会话,因此构建系统时,可以允许运维人员在发生故障时可以进行人工干预,同时避免出现裂脑的可能性。
第三种健康检查机制是会话TTL
。创建会话时,可以指定TTL值。如果TTL间隔过期而没有更新,则说明会话已过期,将触发失效机制。这种类型的故障检测器也称为心跳故障检测器。它比基于Gossip
的故障检测器可扩展性差,因为它给服务器增加了额外负担,但在某些情况下可能适用。TTL约定了失效的下限时间,也就是说,Consul不会在达到TTL之前使会话过期,允许将过期时间延迟到TTL之后。在创建会话,更新会话和领导者故障转移时,会更新TTL。使用TTL时,客户端应注意时钟偏斜问题(clock skew issue)
:即客户端上的时间进度可能与Consul服务器上的速率不相同。
最后的一个细节是会话可以提供锁定延迟(lock-delay)
,锁定延迟是一个持续时间,介于0到60秒之间。当会话失效时,Consul会在锁定延迟间隔内阻止其重新获取任何先前持有的锁,这受到了Google Chubby的启发。此延迟的目的是允许潜在的仍然活跃的领导者检测到无效节点,并停止处理可能导致状态不一致的请求。尽管这不是解决问题的绝对方法,但它确实避免了将系统引入休眠状态,并且可以帮助缓解许多问题。该值默认值为15秒,客户端可以设置为0来禁用此机制。
KV集合
KV集合和会话之间的集成是会话的主要使用场景,会话必须在使用前就创建,然后引用其ID。
KV API
扩展支持获取(acquire)
和释放(release)
操作。获取操作的作用类似于检查并设置(Check-And-Set)
操作,不同之处在于,只有在当前锁没有持有人的情况下它才能成功(当前的锁持有人可以使用重新获取(re-acquire)
操作)。成功后,将进行正常的密钥更新,LockIndex
也会累加,并且Session值也会更新以显示该Session持有了锁。
如果在获取过程中给定会话已持有该锁,则LockIndex
不会递增,但密钥内容会更新。这样,当前的锁持有者就可以更新密钥内容,而不必解锁并重新获取它。
持有锁之后,就可以使用提供相同会话的对应释放操作来释放锁。同样,这也类似于检查并设置(Check-And-Set)
操作,因为如果提供无效会话,则请求将失败。一个需要注意的地方是,可以在不创建会话的情况下释放该锁,这是设计使然,因为它允许运维人员在必要时进行干预并强制终止会话。如上所述,会话失效也将导致所有持有的锁被释放或删除。释放锁后,LockIndex
不会更改;但是,会话被清除后ModifyIndex
递增。
这些概念(大多数来自于Chubby
)允许Key
、LockIndex
、Session
组成的元组充当序列器(sequencer)
,并用于验证请求是否属于当前的锁持有者。 因为LockIndex
在每次获取时都会递增,所以即使同一会话重新获取锁,序列器(sequencer)
也将能够检测到过期请求。 同样,如果会话失效,则与LockIndex对应的会话将变为空白。
需要明确的是,该锁定系统仅是建议性的。没有强制要求客户端必须获得锁才能执行任何操作。任何客户端都可以在不拥有相应锁的情况下读取,写入和删除密钥。Consul的目的不是防止客户端的不当行为。
反熵算法(Anti-Entropy)
Consul使用高级的方法维护服务和健康信息。本章节详细描述了如何注册
服务和健康检查,如何填充(populate)
目录以及健康状态信息在更新时如何变化。
组件
我们首先需要了解服务和健康检查的两个组件:代理(Agent)
和目录(Catalog)
,下面对这些概念进行了详细描述,可以帮助我们更好地理解反熵算法。
代理
每个Consul代理各自维护自己的服务、健康检查注册信息和健康检查信息,代理负责执行自己的健康检查并更新其本地状态。
代理上下文中服务和健康检查有很多可选的配置项,这是因为代理负责通过健康检查功能,生成服务和运行情况信息。
目录
Consul中的服务发现底层由服务目录支持,目录是通过代理提交的信息汇总形成的。目录抽象展现了当前集群的高层视图,包括哪些服务可用,哪些节点运行这些服务,健康检查信息等等。用户可以通过Consul提供的接口(包括DNS
和HTTP
)访问到这些信息。
与代理相比,目录上下文中服务和健康检查包含的字段优先,这是因为目录仅负责记录和返回有关服务,节点和健康状态的信息。
目录仅由服务器节点维护,这是因为Consul底层使用了Raft
协议,集群内部目录信息是统一的。
反熵算法
我们将系统会变混乱的趋势称为熵(Entropy)
,Consul中的反熵(Anti-Entropy)
机制就是用来应对这种情况,即使在其组件发生故障时也能保持群集状态有序。熵
是物理学上的一个概念,代表杂乱无章,而反熵
就是在杂乱无章中寻求一致。
如上所述,Consul在全局服务目录和代理的本地状态之间有明确的区分。反熵机制
打通了这两个世界的视角:反熵
可以将本地代理状态和目录保持同步。例如,当用户在代理中新注册了一个服务或健康检查,代理会通知给目录;同样,当从代理中删除健康检查时,也会将其从目录中删除。
反熵
也用于更新服务可用性(Availability)
信息,当代理运行健康检查时,其状态可能会更改,在这种情况下,新状态将同步到目录。目录使用此信息,可以根据其节点和服务的可用性智能地响应查询请求。
在同步期间,还将检查目录的正确性。如果目录中存在代理不知道的任何服务或健康检查,它们将被自动删除,以使目录可以真实反映代理中运行的服务集和健康检查集合。Consul将代理的状态视为权威(authoritative)
;如果代理视图和目录视图之间有任何差异,将始终使用代理本地视图。
定期同步
除了在代理发生更改时运行之外,反熵
也会作为常驻进程长期运行,该进程会周期性唤醒,将服务和健康检查状态同步给目录,这样可以确保目录和代理之间的状态是匹配的。即使在发生数据丢失的情况下,Consul也可以重新填充服务目录。
为了避免同步过于频繁,反熵
周期性运行时间会根据进群大小变化,下表定义了群集大小和同步间隔时间之间的关系:
集合数 | 定期同步周期 |
---|---|
1-128 | 1分钟 |
129-256 | 2分钟 |
257-512 | 3分钟 |
513-1024 | 4分钟 |
... | ... |
上面的间隔时间是近似值,每个Consul代理会在周期窗口内选择一个随机交错的开始时间,避免集中在一起。
尽力同步
在许多情况下,反熵
可能会失败,原因有很多,包括代理或其操作环境配置错误,I/O问题(磁盘存满,文件系统权限等),网络问题
(代理无法与服务端通信)等。 因此,代理会尽可能保持同步。
如果在反熵
运行过程中遇到错误,代理会记录下来并继续运行。定期运行反熵机制,可以从这些瞬态故障中恢复过来。
启用标签覆盖
服务注册的信息同步可以部分修改,这样允许外部代理更改服务的标签,这在某些情况下很有用,比如外部监视服务作为标签信息的来源,Redis数据库及其监控服务Redis Sentinel
之间就具有这种关系,Redis实例负责其大部分配置,但Sentinels用来确定Redis实例是主实例(primary)
还是副实例(secondary)
。 使用Consul服务配置项enable_tag_override
,可以指示正在运行Redis数据库的Consul代理在反熵同步期间不更新标签。
安全模式
Consul同时依靠轻量级的Gossip
机制和RPC系统来提供各种功能,这两个系统因为设计原理不同,需要提供不同的安全机制。 但总的来说,Consul的安全机制有一个统一的目标:提供机密性(confidentiality)
、完整性(integrity)
和身份校验(authentication)
。
Gossip协议
底层由Serf
提供支持,Serf使用对称密钥
或共享密钥
,有关Serf的相关细节和Consul中使用Serf的细节,可以参考其他文档。
RPC系统支持将端到端的TLS协议
,并且可选用身份验证。TLS是一种广泛运用的非对称密码系统,并且是Web安全的基础。
这些机制意味着Consul通信受到保护,不会被窃听,篡改和欺骗。 这样就可以在不受信任的网络(例如EC2和其他云托管平台)上运行Consul。
安全配置
Consul防范模型(threat model)
是建立在Consul以安全配置运行的前提下,Consul默认情况下没有启用安全配置,如果未启用下文罗列的一些设置,则此防范模型的某些部分将失效。如以下章节所述,还必须对Consul防范模型之外的项目采取其他安全预防措施。
Consul运行方式和其他二进制文件一样
,它作为单个进程运行,并遵循与操作系统上任何其他应用程序相同的安全性要求。Consul不会与主机系统进行交互,不会以任何方式更改或操作安全配置。用户应根据自己的操作系统对程序进行一些防护措施,以下是一些常用的建议:
- 添加适当配置,以非root用户身份运行应用程序(包括Consul);
- 使用内核安全模块(例如
SELinux
)实施强制性访问控制; - 防止非特权用户成为root;
ACL默认启用拒绝策略
。必须将Consul配置为通过允许列表(allowlist)
使用ACL,默认为拒绝。这就强制所有请求具有明确的匿名访问权限或提供ACL令牌。
启用加密
:必须启用和配置TCP
和UDP
加密,防止Consul代理之间进行明文通信
。至少应启用verify_outgoing
来验证每个服务端有唯一的TLS
认证。还需要配置verify_server_hostname
,以防止受感染的代理作为服务端重新启动并获得对所有机密的访问权限。
verify_incoming
通过相互身份验证提供了额外的代理验证,但对于实施防范模型
而言并非绝对必要,因为请求还必须包含有效的ACL令牌。它们之间的细微差别在于当verify_incoming=false
,将允许服务端仍然接受来自客户端的未加密连接,仅此一项并不会违反防范模型
,但是任何选择不使用TLS的配置错误的客户端都会违反该模型。我们建议将该配置项设置为true;如果保留为false,则必须确保所有Consul客户端均如上所述使用verify_outgoing = true
,但所有外部API/UI
访问就必须全部通过HTTPS
,HTTP监听将禁用。
已知的不安全配置
除了配置上述非默认设置外,Consul还具有多个非默认选项,这些选项可能会带来其他安全风险。
- 使用网络公开的API进行脚本检查。如果Consul代理(客户端或服务端)向本地主机以外的网络公开其HTTP API,则
enable_script_checks
必须设置为false
,否则,即使配置了ACL,脚本检查也会带来远程执行代码的威胁。如果必须公开HTTP API,则enable_local_script_checks
提供了一种安全的替代方法,这个方法从1.3.0版本
开始可用,该功能还被反向移植到补丁版本0.9.4
、1.1.1
和1.2.4
。 -
启用远程执行程序
: Consul包含consul exec
功能,允许跨集群执行任意命令。从0.8.0版本
开始默认禁用,我们建议禁用它。如果启用,则必须格外小心,以确保正确的ACL限制访问。 -
验证服务器主机名单独使用
。从0.5.1版本
到1.4.0版本
,我们记录了verify_server_hostname
为true
表示verify_outgoing
,但是由于Bug原因,情况并非如此,因此仅设置verify_server_hostname
会导致客户端和服务端之间进行纯文本通信,这在1.4.1版本
中已修复。
威胁模型
以下是Consul防范模型(threat model)
的一部分:
-
Consul代理间通信
:应确保Consul代理间的通信不被窃听,这需要在集群上启用传输加密,并涵盖TCP和UDP通信; -
Consul代理到CA间通信
:Consul服务端与CA服务器之间的通信始终被加密; -
篡改传输的数据
:应检测到任何篡改,这样Consul可以避免处理该请求; -
无需身份验证或授权即可访问数据
:所有请求都必须经过身份验证和授权,这要求群集上启用ACL,使用默认拒绝模式; -
由于恶意消息导致状态修改或损坏
:格式错误的消息将被直接丢弃,格式正确的消息需要身份验证和授权; -
非服务端成员访问原始数据
:所有服务端必须加入集群(具有正确的身份验证和授权)才能开始参与Raft,Raft数据通过TLS传输; -
针对节点的服务拒绝
:针对节点的DoS攻击不应影响软件的安全性; -
基于连接的服务端通信
:应该保护两个启用连接的服务之间的通信(本机或通过代理)不被窃听并提供身份验证,这是通过双向TLS实现的。
以下内容不是Consul服务端代理防范模型(threat model)
的一部分:
-
访问Consul数据目录(读写操作)
:所有Consul服务器,包括非领导者,都将完整的Consul状态保存到此目录中。数据
包括所有KV数据集
、服务注册
、ACL令牌
、Connect CA配置
等。对该目录的任何读写操作都使攻击者可以访问和篡改该数据; -
访问Consul配置目录(读写操作)
:Consul配置可以启用或禁用ACL系统,修改数据目录路径等。对该目录的任何读写操作都使攻击者可以重新配置Consul的许多方面。通过禁用ACL系统,攻击者可以访问所有Consul数据; -
访问Consul服务端机器内存
:如果攻击者能够访问正在运行的Consul服务端机器的内存状态,则几乎所有Consul数据的机密性都可能受到损害。如果用户使用的是外部Connect CA
,则根私有密钥(root private material)
永远不会用于Consul流程,因此可以认为是安全的。Service Connect TLS
证书应被视为已泄露,它们永远不会被服务端机器持久化,至少在签名请求期间确实存在于内存中。
以下内容不是Consul客户端代理防范模型(threat model)
的一部分:
-
访问Consul数据目录(读写操作)
:Consul客户端将使用数据目录缓存本地状态。这包括本地服务
、关联的ACL令牌
、Connect TLS证书
等。对该目录的读写操作使攻击者可以访问此数据,此数据通常是群集完整数据的较小子集; -
访问Consul配置目录(读写操作)
:Consul客户端配置文件包含服务的地址
和端口信息
、代理的默认ACL令牌
等。访问Consul配置可以使攻击者将服务端口更改为恶意端口,注册新服务等。此外,某些服务定义附加了ACL令牌,可在群集范围内使用ACL令牌来模拟该服务。攻击者无法更改群集范围的配置,例如禁用ACL系统; -
访问Consul客户端机器内存
:其危险半径比服务端代理要小得多,但是仍然会损害数据子集的机密性。特别是,针对代理的API请求的任何数据(包括服务
,KV数据集
和连接信息
)可能会受到威胁。如果客户端从未请求服务端上的特定数据集,则它绝不会进入代理的内存,因为复制仅存在于服务端之间。攻击者还可能提取此代理上用于服务注册的ACL令牌,因为令牌必须与注册的服务一起存储在内存中; -
通过网络访问本地Connect代理或服务
:服务和可识别连接的代理之间的通信通常是未加密的,并且必须通过受信任的网络进行。这通常是回送设备(loopback device)
。这要求信任同一台计算机上的其他进程,或者使用更复杂的隔离机制,例如网络名称空间(network namespaces)
。这还要求外部进程不能与Connect服务
或代理进行通信,入站端口(inbound port)
除外。因此,非本机Connect应用程序应仅绑定到非公共地址; -
实施不完善的Connect代理或服务
:Connect代理或本机集成服务必须正确提供有效的叶子证书(leaf certificate)
,验证入站TLS客户端证书并调用Consul代理本地授权端点。如果其中任何一项未正确执行,则代理或服务可能允许未经身份验证或未经授权的连接。
外部威胁概述
有四个影响Consul防范模型(threat model)
的组件:服务器代理
、客户端代理
、Connect CA
和Consul API客户端
(包括Connect代理)。
服务端代理通过Raft参与领导者选举和数据复制,与其他代理的所有通信都是加密的。数据以明文方式存储在配置的数据目录中。存储的数据包括ACL令牌
和TLS证书
。如果内置CA与Connect一起使用,则根证书私钥(root private key)
也存储在磁盘上,外部CA程序不在此目录中存储数据。必须仔细保护此数据目录,以防止攻击者冒充服务端或特定的ACL用户。我们计划随着时间的推移在数据目录中引入进一步的防范措施(包括至少部分数据加密),但应始终将数据目录视为机密。
要使客户端代理加入集群,它必须提供具有node:write
功能的有效ACL令牌。加入集群的请求和客户端与服务端代理之间所有其他API请求均通过TLS进行通信。客户端提供Consul API,并通过共享的TLS连接将所有请求转发到服务端。每个请求均包含用于身份验证和授权的ACL令牌。不提供ACL令牌的请求将继承可配置代理的默认ACL令牌。
Connect CA提供者负责存储用于签署和验证通过Connect建立的连接的根(或中间)证书的私钥。Consul服务端代理通过加密方法与CA程序进行通信。此方法取决于使用的CA程序,Consul提供一个内置的CA,可以在服务端代理上本地执行所有操作。Consul本身不存储任何私钥信息,内置CA除外。
Consul API客户端(代理本身,内置UI,外部软件)必须通过TLS与Consul代理进行通信,并且必须为身份验证和授权请求提供ACL令牌。
Jepsen 测试
Jepsen
是由Kyle Kingsbury
编写的工具,旨在测试分布式系统的分区容错性(partition tolerance)
。它创建网络分区,同时对系统进行随机操作,并分析结果查看系统是否违反了一致性原则。
作为Consul测试的一部分,我们运行了Jepsen测试,以确定是否可以发现任何一致性问题。 在我们的测试中,Consul从分区中正常恢复,没有引入任何一致性问题。
运行测试
目前,使用Jepsen进行测试非常复杂,因为它需要设置多个虚拟机
,SSH密钥
,DNS配置
和有效的Clojure环境
。 我们希望贡献我们的Consul测试代码,并为Jepsen测试提供一个运行环境。
输出
以下是从Jepsen捕获的输出。我们多次运行Jepsen,每次都测试通过,由于输出内容过于占用篇幅(总共3900+行),我们仅粘贴前100行数据:
$ lein test :only jepsen.system.consul-test
lein test jepsen.system.consul-test
INFO jepsen.os.debian - :n5 setting up debian
INFO jepsen.os.debian - :n3 setting up debian
INFO jepsen.os.debian - :n4 setting up debian
INFO jepsen.os.debian - :n1 setting up debian
INFO jepsen.os.debian - :n2 setting up debian
INFO jepsen.os.debian - :n4 debian set up
INFO jepsen.os.debian - :n5 debian set up
INFO jepsen.os.debian - :n3 debian set up
INFO jepsen.os.debian - :n1 debian set up
INFO jepsen.os.debian - :n2 debian set up
INFO jepsen.system.consul - :n1 consul nuked
INFO jepsen.system.consul - :n4 consul nuked
INFO jepsen.system.consul - :n5 consul nuked
INFO jepsen.system.consul - :n3 consul nuked
INFO jepsen.system.consul - :n2 consul nuked
INFO jepsen.system.consul - Running nodes: {:n1 false, :n2 false, :n3 false, :n4 false, :n5 false}
INFO jepsen.system.consul - :n2 consul nuked
INFO jepsen.system.consul - :n3 consul nuked
INFO jepsen.system.consul - :n4 consul nuked
INFO jepsen.system.consul - :n5 consul nuked
INFO jepsen.system.consul - :n1 consul nuked
INFO jepsen.system.consul - :n1 starting consul
INFO jepsen.system.consul - :n2 starting consul
INFO jepsen.system.consul - :n4 starting consul
INFO jepsen.system.consul - :n5 starting consul
INFO jepsen.system.consul - :n3 starting consul
INFO jepsen.system.consul - :n3 consul ready
INFO jepsen.system.consul - :n2 consul ready
INFO jepsen.system.consul - Running nodes: {:n1 true, :n2 true, :n3 true, :n4 true, :n5 true}
INFO jepsen.system.consul - :n5 consul ready
INFO jepsen.system.consul - :n1 consul ready
INFO jepsen.system.consul - :n4 consul ready
INFO jepsen.core - Worker 0 starting
INFO jepsen.core - Worker 2 starting
INFO jepsen.core - Worker 1 starting
INFO jepsen.core - Worker 3 starting
INFO jepsen.core - Worker 4 starting
INFO jepsen.util - 2 :invoke :read nil
INFO jepsen.util - 3 :invoke :cas [4 4]
INFO jepsen.util - 0 :invoke :write 4
INFO jepsen.util - 1 :invoke :write 1
INFO jepsen.util - 4 :invoke :cas [4 0]
INFO jepsen.util - 2 :ok :read nil
INFO jepsen.util - 4 :fail :cas [4 0]
INFO jepsen.util - 1 :ok :write 1
INFO jepsen.util - 0 :ok :write 4
INFO jepsen.util - 3 :fail :cas [4 4]
INFO jepsen.util - 2 :invoke :cas [0 3]
INFO jepsen.util - 2 :fail :cas [0 3]
INFO jepsen.util - 4 :invoke :cas [4 4]
INFO jepsen.util - 1 :invoke :write 3
INFO jepsen.util - 0 :invoke :cas [3 1]
INFO jepsen.util - 3 :invoke :write 2
INFO jepsen.util - 4 :fail :cas [4 4]
INFO jepsen.util - 0 :fail :cas [3 1]
INFO jepsen.util - 1 :ok :write 3
INFO jepsen.util - 3 :ok :write 2
INFO jepsen.util - 2 :invoke :read nil
INFO jepsen.util - 2 :ok :read 2
INFO jepsen.util - 4 :invoke :read nil
INFO jepsen.util - 0 :invoke :write 4
INFO jepsen.util - 1 :invoke :write 0
INFO jepsen.util - 4 :ok :read 2
INFO jepsen.util - 3 :invoke :read nil
INFO jepsen.util - 0 :ok :write 4
INFO jepsen.util - 3 :ok :read 2
INFO jepsen.util - 1 :ok :write 0
INFO jepsen.util - 2 :invoke :write 3
INFO jepsen.util - 2 :ok :write 3
INFO jepsen.util - 4 :invoke :write 4
INFO jepsen.util - 4 :ok :write 4
INFO jepsen.util - 0 :invoke :write 1
INFO jepsen.util - 3 :invoke :read nil
INFO jepsen.util - 1 :invoke :cas [1 0]
INFO jepsen.util - 3 :ok :read 4
INFO jepsen.util - 0 :ok :write 1
INFO jepsen.util - 1 :fail :cas [1 0]
INFO jepsen.util - 2 :invoke :cas [0 2]
INFO jepsen.util - 2 :fail :cas [0 2]
INFO jepsen.util - 4 :invoke :cas [1 2]
INFO jepsen.util - 4 :fail :cas [1 2]
INFO jepsen.util - 3 :invoke :write 1
INFO jepsen.util - 0 :invoke :write 1
INFO jepsen.util - 1 :invoke :read nil
INFO jepsen.util - 0 :ok :write 1
INFO jepsen.util - 1 :ok :read 1
INFO jepsen.util - 3 :ok :write 1
INFO jepsen.util - 2 :invoke :write 4
INFO jepsen.util - 2 :ok :write 4
INFO jepsen.util - 4 :invoke :cas [2 4]
INFO jepsen.util - 4 :fail :cas [2 4]
INFO jepsen.util - 0 :invoke :read nil
INFO jepsen.util - 1 :invoke :write 3
INFO jepsen.util - 3 :invoke :read nil
INFO jepsen.util - 0 :ok :read 4
INFO jepsen.util - 3 :ok :read 4
INFO jepsen.util - 1 :ok :write 3
INFO jepsen.util - 2 :invoke :cas [4 2]
INFO jepsen.util - 2 :fail :cas [4 2]
INFO jepsen.util - 4 :invoke :read nil
INFO jepsen.util - 4 :ok :read 3
INFO jepsen.util - 0 :invoke :cas [2 4]
INFO jepsen.util - 3 :invoke :write 2
INFO jepsen.util - 1 :invoke :write 0
INFO jepsen.util - 0 :fail :cas [2 4]
INFO jepsen.util - 3 :ok :write 2
INFO jepsen.util - 1 :ok :write 0
INFO jepsen.util - 2 :invoke :cas [0 3]
INFO jepsen.util - 2 :fail :cas [0 3]
INFO jepsen.util - 4 :invoke :write 0
INFO jepsen.util - 4 :ok :write 0
INFO jepsen.util - 0 :invoke :write 1
INFO jepsen.util - 3 :invoke :cas [0 2]
INFO jepsen.util - 1 :invoke :cas [0 0]
INFO jepsen.util - 0 :ok :write 1
Consul 术语表
本章节收集了Consul
和Consul Enterprise
文档中使用的一些技术术语,以及整个Consul社区中经常出现的一些术语。
代理
代理是Consul群集中,每个成员上运行时间较长的守护程序,它通过运行consul agent
启动。代理可以以客户端
或服务端
模式运行。由于所有节点都必须运行代理,所以将节点称为客户端或服务器更为简单,但是还存在代理的其他实例。所有代理都可以运行DNS
或HTTP
接口,并负责运行检查,和保持服务同步。
客户端
客户端是将所有RPC
请求转发到服务端的代理。客户端相对是无状态(stateless)
的,客户端执行的唯一后台活动是参与LAN gossip pool
,这个资源开销很小,并且仅消耗少量的网络带宽。
服务端
服务端在职责上有所增强,它参与Raft仲裁
、维护群集状态
、响应RPC查询
,与其他数据中心交换WAN gossip
以及将查询转发给领导者或远程数据中心。
数据中心
我们将数据中心
定义为私有的
、低延迟
和高带宽
的网络环境。它不包括穿越公共互联网(public internet)
的通信,在我们的定义中,单个EC2区域内的多个可用区视为单个数据中心的一部分。
Consensus
在文档中,我们使用共识(Consensus)
来表示对领导者的选举达成共识,以及对事务顺序达成一致。由于这些事务适用于有限状态机(finite-state machine)
,因此我们对一致性的定义意味着复制状态机(replicated state machine)
的一致。
Gossip
Consul建立在Serf
之上,Serf
提供了完整的Gossip
协议,可用于多种目的。Serf提供成员资格,故障检测和事件广播机制。我们在Gossip
章节中对这些用法有详细说明。简单理解,Gossip协议涉及随机的节点间通信,主要是通过UDP
协议。
LAN Gossip
局域网 Gossip Pool:其中包含的节点都位于同一局域网或数据中心上。
WAN Gossip
广域网 Gossip Pool:其中仅包含服务端机器,这些服务器位于不同数据中心,通常通过Internet或广域网进行通信。
RPC
远程调用,这是一种请求/响应机制,允许客户端向服务器发出请求。