读书笔记-Docker容器与容器云(第2版)-04

文章目录

上一篇的读书笔记,时间停留在2018年12月22号,一年的时间里,开源界风风雨雨,Docker母公司起起落落,K8s大行其道,而自己一直游走于协议栈和golang,目前认为基于 docker-compose 的容器化部署方式依旧可行,CentOS 7和docker 1.13还不过时。

4. Docker高级实践技巧

4.1 容器化思维

容器是可以说操作系统级别的虚拟化(类似OpenVZ,共享内核),本质是一个进程及其运行所需的各种依赖。

4.1.1 SSH服务器的替代方案

Docker提供了docker exec命令,可以在已运行的容器中执行所需的命令,并得到反馈的结果,提供与宿主机原有功能的结合,同样可以解决诸如定时任务等问题。

4.1.2 Docker内日志管理方案

当前Docker将应用的stdoutstderr两个日志输出通过管道重定向到 /var/log 下,默认以JSON消息记录每一行日志。目前处理Docker日志的主流方案,按日志处理工具的安装位置主要分为三种

  • 在容器内收集。除正在运行的引用程序外,每个容器设置一个日志收集进程。典型代表为baseimage-docker项目,使用runit连同syslog提供日志收集。
  • 在容器外收集。在宿主机上运行一个单独收集日志的代理,收集所有容器的日志。典型代表为Fluentd项目。
  • 在专用容器中收集。这是直接在宿主机上运行代理收集日志的变种方案,该收集代理同样运行在一个容器中,且该容器的volume卷绑定给所有的应用程序容器。实现细节可参考:Docker and Logstash

4.1.3 容器化思维及更多

从技术角度理解容器化思维就是要意识到容器的本质是一个进程及运行该进程所需的各种依赖,若将容器化思维拓展到“如何更好地使用容器”这一层面,那么容器化思维所包含的概念和实践方案就有很多了,其中,大多数人接触比较多的应该是微服务。

所谓微服务模式有如下三大特性

  • 彼此独立:微服务模式下的每一个组成部分,都是一个独立的服务,有一整套的完整运行机制及标准化的对外接口。不依赖于其他部分就能正常运转,同时可探测其他组成部分的存在。
  • 原子化:微服务应该是不可再分的原子化服务。
  • 组合和重构:微服务的最大特点在于它能快速地组合和重构,彼此组合成一个系统。系统里所有实体在逻辑上是等价的,因此它的结构相对简单和松散的,具有极强的可扩展性和鲁棒性。

谈到容器时经常会谈起微服务,因为容器技术轻量级的特性和“构建一次,随处运行”的特性降低了微服务类型应用的额外开销,提升了微服务式的应用开发流程效率,使微服务的开发模式成为可能。与之相关的可以涵盖在容器化思维内的理念还包括DevOps、持续集成和持续交付(CICD)以及不可变基础设施(immutable infrastructure)。

总之,我们在使用Docker时需要关注容器本身,时刻提醒我们是在使用容器,从而享受它带来的各种便利,如快速的应用分发能力、高效的操作及反应能力、弹性灵活的部署能力以及低廉的部署成本;同时我们也需要学习和解决围绕容器的各类实践问题,如网络、存储、监控、资源控制、配置管理、安全等。

4.2 Docker高级网络实践

Docker的libnetwork默认提供类四种驱动,假设需要运营一个数据中心的网络,我们有许多宿主机,每台宿主机上运行了数百个甚至上千个Docker容器,使用四种网络驱动的具体情况如下。

  • host驱动:容器与宿主机共用一个网络栈,未使用network namespace的隔离,缺乏安全性
  • bridge驱动:容器没有对外IP,只能通过NAT来实现对外通信,不能解决跨主机容器间直接通信的问题
  • overlay驱动:可用于支持跨主机的网络通信,但必须配合Swarm进行配置和使用
  • null驱动:实际不进行任何网络设置

可见为了实现数据中心大量容器间的跨主机网络通信,为了更灵活地实现容器间的共享和隔离,也为了在管理成千上万个容器时可以更加自动化地进行网络配置,我们需要学习更高级的网络实践方案。

4.2.1 玩转Linux network namespace

1. 使用ip netns命令操作network namespace

ip netns命令可添加、删除、列出network namespace,可在指定network namespace中执行命令,或打开一个shell。

2. 使用ip命令为network namespace配置网卡

当使用ip netns add命令创建了一个network namespace后,就拥有了一个独立的网络空间,可以根据需求来配置该网络空间,如添加网卡、配置IP、设置路由规则等。

3. 将两个network namespace连接起来

可以在一台普通的机器上,以非常简单的方式创建很多相互隔离的network namespace,然后通过网卡、网桥等虚拟设备将它们连接起来,组成想要的拓扑网络。

如果有更多network namespace需要连接,那就有必要引入虚拟网桥了,就如同Docker的网络一样。

4. 使用ip命令配置Docker容器网络

Docker正是使用Linux namespaces技术进行资源隔离的,网络也是如此。当用默认网络模式(bridge模式)启动一个Docker容器时,一定是在主机上新创建了一个Linux network namespace。用户可以按照在network namespace中配置网络的方法来配置Docker容器的网络。

除了ip netns命令外,还有一些其他工具可以进入Linux namespace,比如nsenter。

4.2.2 pipework 原理解析

Docker现有的网络模式比较简单,扩展性和灵活性都不能满足很多复杂应用场景的需求。很多时候用户都需要自定义Docker容器的网络,而非使用Docker默认创建的IP和NAT规则。一个简单的做法就是将Docker容器网络配置到本地主机网络的网段中。

1. 将Docker容器配置到本地网络环境下

如果想要使Docker容器和容器主机处于同一个网络,那么容器和主机应该处在一个二层网络中。能想到的场景就是把两台机器连在同一个交换机上,或者连在不同的级联交换机上。在虚拟场景下,虚拟网桥可以将容器连在一个二层网络中,只要将主机的网卡桥接到虚拟网桥中,就能将容器和主机的网络连接起来。构建完拓扑结构后,只需再给Docker容器分配一个本地局域网IP就大功告成了。

2. pipework解析

  • 支持Linux网桥连接容器并配置容器IP地址
  • 支持使用macvlan设备将容器连接到本地网络
  • 支持DHCP获取容器的IP
  • 支持Open vSwitch
  • 支持设置网卡MAC地址以及配置容器VLAN

4.2.3 pipework 跨主机通信

如果将Docker容器应用在大规模集群环境中,不可避免地会遭遇Docker容器跨主机通信的问题。在目前Docker默认网络环境下,单台主机上的Docker容器可以通过docker0网桥直接通信,而不同主机上的Docker容器之间只能通过在主机上做端口映射的方法进行通信。这种端口映射方式对很多集群应用来说极不方便。如果能使Docker容器之间直接使用本身IP地址进行通信,很多问题便会自然化解。

1. 桥接

bridge_network_topo

2. 直接路由

route_network_topo

4.2.4 OVS划分VLAN

在计算机网络中,传统的交换机虽然能隔离冲突域,提高每一个端口的性能,但并不能隔离广播域,当网络中的机器足够多时会引发广播风暴。同时,不同部门、不同组织的机器连在同一个二层网络中也会造成安全问题。因此,在交换机中划分子网、隔离广播域的思路便形成了VLAN的概念。VLAN(Virtual Local Area Network)即虚拟局域网,按照功能、部门等因素将网络中的机器进行划分,使之分属于不同的部分,每一个部分形成一个虚拟的局域网络,共享一个单独的广播域。这样就可以把一个大型交换网络划分为许多个独立的广播域,即VLAN。

vlan_01

1. 单主机Docker容器的VLAN划分

vlan_02

2. 多主机Docker容器的VLAN划分

vlan_03

4.2.5 OVS隧道模式

在4.2.3节中,讲述了跨主机通信的两种方法,并提到了这两种方法有一个局限——要求主机在同一个子网中。当基础设施的规模足够大时,这种局限性就会暴露出来,比如两个数据中心的Docker容器需要通信时,这两种方法就会失效。目前比较普遍的解决方法是使用Overlay的虚拟化网络技术。

1. Overlay技术模型

Overlay网络其实就是隧道技术,即将一种网络协议包装在另一种协议中传输的技术。

overlay_01

当前主要的Overlay技术有:

  • VXLAN(Virtual Extensible LAN),将以太网报文封装在UDP传输层上的一种隧道转发模式,它采用24位比特标识二层网络分段,称为VNI(VXLAN Network Identifier),类似于VLAN ID的作用
  • NVGRE(Network Virtualization using GenericRouting Encapsulation),同VXLAN类似,它使用GRE的方法来打通二层与三层之间的通路,采用24位比特的GRE key来作为网络标识(TNI)。

2. GRE简介

通用路由封装 (GRE:Generic Routing Encapsulation)定义了在任意一种网络层协议上封装任意一个其它网络层协议的协议。

3. GRE实现Docker容器跨网络通信(容器同在一子网中)

gre_01

4. GRE实现Docker容器跨网络通信(容器在不同子网中)

gre_02

k8s_ovs

5. 多租户环境下的GRE网络

gre_openstack

4.3 Dockerfile 最佳实践

4.3.1 Dockerfile 的使用

1. docker build命令和镜像构建过程

  • docker build命令参数有3种类型(PATH、-、URL),表示构建上下文(context)的3种来源。
  • Dockerfile描述了组装镜像的步骤,其中每条指令都是单独执行的。
  • 除了FROM指令,其他每一条指令都会在上一条指令所生成镜像的基础上执行,执行完后会生成一个新的镜像层。
  • Docker daemon会缓存构建过程中的中间镜像,如果有一个子镜像是由相同的指令生成的,则命中缓存,直接使用该镜像。
  • ADD和COPY指令与其他指令不同,除了对比指令字符串,还要对比容器中的文件内容和ADD、COPY所添加的文件内容是否相同。

2. Dockerfile 指令

  • 指令不区分大小写,为了与参数区分,推荐大写
  • 第一条指令必须是FROM指令,它用于指定构建镜像的基础镜像
  • 在Dockerfile中以#开头的行是注释,而在其他位置出现的#会被当成参数
  • ENV:ENV key value 或ENV key=value,为镜像创建出来的容器声明环境变量,可被后续指令解析使用。
  • FROM:FROM image 或FROM image:tag,后面的指令提供基础镜像。
  • COPY:COPY src dest,复制src所指向的文件或目录,将它添加到新镜像中,复制的文件或目录在镜像中的路径是dest
  • ADD:ADD src dest,ADD与COPY指令在功能上很相似,但src可以是一个指向一个网络文件的URL或一个本地压缩归档文件。
  • RUN:
    • RUN command(shell格式,命令通过/bin/sh-c运行)
    • RUN ["executable", "param1", "param2"](exec格式,推荐格式,命令是直接运行的,容器不调用shell程序)
    • RUN指令会在前一条命令创建出的镜像的基础上创建一个容器,并在容器中运行命令,在命令结束运行后提交容器为新镜像,新镜像被Dockerfile中的下一条指令使用。
  • CMD:
    • CMD command(shell格式)
    • CMD ["executable", "param1", "param2"](exec格式,推荐格式)
    • CMD ["param1", "param2"](为ENTRYPOINT指令提供参数)
    • CMD指令提供容器运行时的默认值,这些默认值可以是一条指令,也可以是一些参数。
    • CMD指令在构建镜像时并不执行任何命令,而是在容器启动时默认将CMD指令作为第一条执行的命令。
    • 如果用户在命令行界面运行docker run命令时指定了命令参数,则会覆盖CMD指令中的命令。
  • ENTRYPOINT:
    • ENTRYPOINT command(shell格式)
    • ENTRYPOINT ["executable", "param1", "param2"](exec格式,推荐格式)
    • ENTRYPOINT指令和CMD指令类似,都可以让容器在每次启动时执行相同的命令。
    • 一个Dockerfile中可以有多条ENTRYPOINT指令,但只有最后一条ENTRYPOINT指令有效。
    • CMD可以是参数,也可以是指令,而ENTRYPOINT只能是命令。
    • docker run命令提供的运行命令参数可以覆盖CMD,但不能覆盖ENTRYPOINT
  • ONBUILD:ONBUILD INSTRUCTION,添加一个将来执行的触发器指令到镜像中。例如使用ONBUILD指令注册触发器指令,构建一个语言栈镜像,该镜像可以构建任何用该语言编写的用户软件的镜像。

4.3.2 Dockerfile 实践心得

  • 使用标签:给镜像打上标签,易读的镜像标签可以帮助了解镜像的功能
  • 谨慎选择基础镜像:尽量选择当前官方镜像库中的镜像,同时在构建自己的Docker镜像时,只安装和更新必须使用的包
  • 充分利用缓存:保证指令的连续性,尽量将所有Dockerfile文件中相同的部分都放在前面,而将不同的部分放在后面
  • 正确使用ADD与COPY指令:首选COPY,COPY仅提供本地文件向容器的基本复制功能,ADD则支持如复制本地压缩包(复制到容器中会自动解压)和URL远程资源。尽量使用docker volume共享文件,而不是使用ADD或COPY指令添加文件到镜像中
  • RUN指令:使用比较长的RUN指令时可以使用反斜杠\分隔多行,
    • 不要在一行中单独使用指令RUN apt-get update
    • 避免使用指令RUN apt-get upgrade和RUN apt-get dist-upgrade
  • CMD和ENTRYPOINT指令:使用exec格式的ENTRYPOINT指令设置固定的默认命令和参数,然后使用CMD指令设置可变的参数
  • 不要在Dockerfile中做端口映射:仅暴露端口,不做映射,映射由docker run时确定
  • 使用Dockerfile共享Docker镜像:
    • Dockerfile文件可以加入版本控制,这样可以追踪文件的变化和回滚错误
    • 通过Dockerfile文件,可以清楚镜像构建的过程
    • 使用Dockerfile文件构建的镜像具有确定性

4.4 Docker容器的监控手段

4.4.1 Docker容器监控维度

1. 主机维度

  • 主机CPU情况和使用量
  • 主机内存情况和使用量
  • 主机上的本地镜像情况
  • 主机上的容器运行情况

2. 镜像维度

  • 镜像的基本信息,可以包括镜像总数量、ID、名称、版本、大小等
  • 镜像与容器的对应关系
  • 镜像构建的历史信息,即层级的依赖信息

3. 容器维度

  • 容器基本信息,包括容器总数量、ID、名称、镜像、启动命令、端口等
  • 容器的运行状态
  • 容器的用量信息

4.4.2 容器监控命令

  • docker ps:查看当前主机上的容器信息,包括容器ID、镜像名、容器启动执行命令、创建时间、状态、端口信息和容器名
  • docker images:查看当前主机上的镜像信息,包括镜像所属的库、标签、ID、创建时间和实际大小,默认只会列出所有顶层镜像的信息,但可以通过-a参数来查看所有中间层的镜像的信息
  • docker stats:实时监控启动中的容器的运行情况,包括CPU、内存、块设备I/O和网络I/O,同时配套API(GET /containers/(id)/stats),可供开发人员调用
  • docker inspect:查看镜像或容器的底层详细信息,以此来了解镜像或容器的完整构建信息,包括基础配置、主机配置、网络设置、状态信息等
  • docker top:查看正在运行的容器中的进程的运行情况
  • docker port:查看容器与主机之间的端口映射关系信息

4.4.3 常用的容器监控工具

  • Google的cAdvisor
  • Datadog
  • SoundCloud的Prometheus

4.5 容器化应用构建的基础:高可用配置中心

以微服务方式构建的应用中,服务发现、服务状态发布和订阅等模块发挥了连接各个微服务的重要作用,而实现服务发现、服务发布与订阅模块的关键技术则是:高可用的配置中心

4.5.1 etcd 经典应用场景

etcd:

  • 键值存储仓库
  • 用于配置共享和服务发现
  • 解决分布式系统中数据一致性的问题
  • 分布式系统中的数据分为控制数据和应用数据,etcd处理的数据默认为控制数据,对于应用数据只推荐处理数据量很小但更新访问频繁的情况

4个特点:

  • 简单:基于HTTP+JSON的API,用curl命令就可以轻松使用
  • 安全:可选SSL客户认证机制
  • 快速:每个实例每秒支持一千次写操作
  • 可信:使用Raft算法充分实现了分布式

1. 场景一:服务发现

什么是服务发现:在同一个分布式集群中的进程或服务,互相感知并建立连接,这就是服务发现

要解决服务发现的问题的三大支柱:

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

service_discovery

服务发现对应的具体应用场景:

  • 微服务协同工作架构中,服务动态添加:通过服务发现机制,在etcd中注册某个服务名字的目录,在该目录下存储可用的服务节点的IP。在使用服务的过程中,只要从服务目录下查找可用的服务节点使用即可。
  • PaaS平台中应用多实例与实例故障重启透明化:服务发现机制提供动态配置域名解析(路由),实现负载均衡和故障恢复。

microservice-01

microservice-02

2.场景二:消息发布与订阅

通过发布订阅的方式实现分布式系统配置的集中式管理与实时动态更新

3.场景三:负载均衡

这里提及的负载均衡均指软负载均衡,在分布式系统中,为了保证服务的高可用以及数据的一致性,通常都会部署多份数据和服务,以此达到对等服务,即使其中某一个服务失效了,也不影响使用

4.场景四:分布式通知与协调

与消息发布订阅类似,使用了etcd中的Watcher机制,通过注册与异步通知机制,实现分布式环境下不同系统之间的通知与协调。

  • 通过etcd进行低耦合的心跳检测。检测系统和被检测系统通过etcd上某个目录关联而非直接关联起来,这样可以大大减少系统的耦合性。
  • 通过etcd完成系统调度。不同系统都在etcd上对同一个目录进行注册,同时设置Watcher监控该目录的变化,当某个系统更新了etcd的目录,那么设置了Watcher的系统就会收到通知,并作出相应处理。
  • 通过etcd完成工作汇报。大部分类似的任务分发系统会在子任务启动后,到etcd来注册一个临时工作目录,并且定时将自己的进度进行汇报(即将进度写入到这个临时目录),这样任务管理者就能够实时知道任务进度。

5.场景五:分布式锁与竞选

etcd使用Raft算法保持了数据的强一致性,某次操作存储到集群中的值必然是全局一致的。此很容易实现分布式锁。锁服务有两种使用方式:

  • 保持独占。即所有试图获取锁的用户最终只有一个可以得到。
  • 控制时序。即所有试图获取锁的用户都会进入等待队列,获得锁的顺序是全局唯一的,同时决定了队列的执行顺序。

使用分布式锁可以完成Leader竞选。对于一些长时间的CPU计算或者使用I/O操作,只需要由竞选出的Leader计算或处理一次,再把结果复制给其他Follower即可,从而避免重复劳动,节省计算资源。

6.场景六:分布式队列

分布式队列的常规用法与场景五中所描述的分布式锁的控制时序用法类似,即创建一个先进先出的队列,保证顺序。

另一种比较有意思的实现是在保证队列达到某个条件时再统一按顺序执行。这种方法的实现可以在/queue这个目录中另外建立一个/queue/condition节点。

  • condition可以表示队列大小。
  • condition可以表示某个任务是否在队列。
  • condition还可以表示其他的一类开始执行任务的通知。

7.场景七:集群监控

通过etcd来进行监控,实现起来非常简单并且实时性强,主要用到了以下两点特性:

  • 前面几个场景已经提到了Watcher机制,当某个节点消失或有变动时,Watcher会第一时间发现并告知用户。
  • 节点可以设置TTL key,例如,每隔30秒向etcd发送一次心跳,代表该节点仍然存活;否则说明节点消失。

8.场景八:etcd vs ZooKeeper

这是因为与etcd相比,ZooKeeper有如下缺点。

  • 复杂。ZooKeeper的部署维护复杂,管理员需要掌握一系列的知识和技能;而Paxos强一致性算法也素来以复杂难懂而闻名于世;另外,ZooKeeper的使用也比较复杂,需要安装客户端,官方只提供了Java和C两种语言的接口。
  • Java编写。Java本身就偏向于重型应用,它会引入大量的依赖。而运维人员则普遍希望机器集群尽可能地简单,维护起来也不易出错。
  • 发展缓慢。Apache基金会项目特有的Apache Way在开源界饱受争议,其中一大原因就是基金会结构庞大,管理松散,导致项目发展缓慢。

etcd作为后起之秀,优点也很明显。

  • 简单。使用Go语言编写,部署简单;使用HTTP作为接口使用简单;使用Raft算法保证强一致性让用户易于理解。
  • 数据持久化。etcd默认数据一更新就进行持久化。
  • 安全。etcd支持SSL客户端安全认证。

4.5.2 etcd 实现原理

1. etcd架构与术语表

etcd_arch

  • HTTP Server:用于处理用户发送的API请求以及其他etcd节点的同步与心跳信息请求
  • Store:用于处理etcd支持的各类功能的事务,包括数据索引、节点状态变更、监控与反馈、事件处理与执行等。它是etcd对用户提供的大多数API功能的具体实现。
  • Raft:Raft强一致性算法的具体实现,是etcd的核心。
  • WAL:即Write Ahead Log(预写式日志),它是etcd的数据存储方式。除了在内存中存有所有数据的状态以及节点的索引以外,etcd还通过WAL进行持久化存储。WAL中,所有的数据在提交前都会事先记录日志。Snapshot是为了防止数据过多而进行的状态快照;Entry则表示存储的具体日志内容。

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

术语解析:

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

2.集群化应用与实现原理

  • 集群启动:etcd有3种集群化启动的配置方案
    • 静态配置:适用于离线环境。在启动整个集群之前已预先清楚所要配置的集群大小,以及集群上各节点的地址和端口信息,那么启动时,可以通过配置initial-cluster参数进行etcd集群的启动。
    • etcd自发现模式:事先准备一个etcd集群,然后要把这个集群URL地址作为参数来启动etcd。这样节点会自动使用进行etcd的注册和发现服务。
    • DNS自发现模式:etcd还支持使用DNS SRV记录进行启动,需开启DNS服务器上的SRV记录查询,分别为各个域名配置相关的A记录,指向etcd核心节点对应的机器IP,最后使用DNS启动etcd集群。
  • 运行时节点变更:只有在集群中多数节点正常的情况下,才可以进行运行时的配置管理。
    • 节点迁移、替换:当节点所在的机器出现硬件故障或者节点出现数据目录损坏等问题,导致节点永久性地不可恢复时执行
    • 节点增加:增加节点可以让etcd的高可用性更强。配置有3个节点,那么最多允许一个节点失效;当配置有5个节点时,就可以允许有两个节点失效。
    • 节点移除:Leader节点在提交一个写记录时,会把这个消息同步到每个节点上,当得到多数节点的同意反馈后,才会真正写入数据。节点越多,写入性能越差。当节点过多时,可能需要移除其中的一个或多个。
    • 强制性重启集群:当集群超过半数的节点都失效时,就需要通过手动的方式,强制性让某个节点以自身为Leader,利用原有数据启动一个新集群。

3.代理模式与实现原理

Proxy模式是etcd的另一种形态,Proxy模式下的etcd作为一个反向代理把客户的请求转发给可用的etcd集群。官方推荐在每一台机器都部署一个Proxy模式的etcd作为本地服务,若Proxy都能正常运行,那么服务发现集群必然是稳定可连接的。

etcd_proxy

Proxy并不是直接加入到符合强一致性的etcd集群中,它没有增加集群的可靠性,也没有降低集群的写入性能。

为什么要有Proxy模式而不是直接增加etcd核心节点呢?etcd每增加一个核心节点(peer),都会给Leader节点增加一定程度的负担; peer数量达到一定程度以后,会降低集群写入同步的性能。因此增加一个轻量级的Proxy模式etcd节点是对直接增加etcd核心节点的一个有效代替。

Proxy模式实际上取代了原先具备转发代理功能的Standby模式,旧版本在核心节点因为故障导致数量不足时,还会从Standby模式转为核心节点,而当故障节点恢复时,若etcd的核心节点数量已达到预设值,则前述节点会再次转为Standby模式。

新版etcd中,只在最初启动etcd集群的过程中,若核心节点的数量已满足要求,则自动启用Proxy模式;反之则并未实现,主要原因如下

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

4. etcd数据存储原理

etcd的存储分为:

  • 内存存储:除了顺序化地记录所有用户对节点数据变更的记录外,还会对用户数据进行、建堆等方便查询的操作。
  • 持久化:
    • WAL,存储着所有事务的变化记录。
    • Snapshot,用于存储某一个时刻etcd所有目录的数据。
    • 为了防止磁盘空间不足,etcd默认每一万条记录做一次Snapshot,经过Snapshot以后的WAL文件就可以删除。

WAL最大的作用是记录了整个数据变化的全部历程。使etcd拥有如下两个重要功能:

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

5. etcd核心算法Raft

Raft中一个任期:在Raft算法中,从时间上讲,一个任期(term)即从某一次竞选开始到下一次竞选开始。如果集群不出现故障,那么一个任期将无限延续下去。而投票出现冲突则有可能直接进入下一任再次竞选。

Raft状态机是怎样切换的

raft

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

如何保证最短时间内竞选出Leader,以防止竞选冲突:在Candidate状态下,有一个“心跳超时”,这是个随机值,也就是说,每个机器成为Candidate以后,超时发起新一轮竞选的时间是各不相同的,这就会出现一个时间差。在时间差内,如果Candidate1收到的竞选信息比自己发起的竞选信息的任期值大(即对方为新一轮任期),并且新一轮想要成为Leader的Candidate2包含了所有提交的数据,那么Candidate1就会投票给Candidate2,这样就保证了出现竞选冲突的概率很小。

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

Raft某个节点宕机后会如何:通常情况下,如果是Follower节点宕机,且剩余可用节点数量超过总节点数的一半,集群可以几乎不受影响地正常工作。如果是Leader节点宕机,那么Follower节点会因为收不到心跳而超时,发起竞选获得投票,成为新一轮任期的Leader,继续为集群提供服务。

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

用户从集群中哪个节点读写数据:Raft为了保证数据的强一致性,所有的数据流向都是一个方向,从Leader流向Follower,即所有Follower的数据必须与Leader保持一致,如果不一致则会被覆盖。因为每个节点都有Raft已提交数据准确的备份,所以任何一个节点都可以处理读请求。

etcd实现的Raft算法的性能:单实例节点支持每秒一千次数据写入。随着节点数目的增加,数据同步会因为网络延迟越来越慢;而读性能则会随之提升,因为每个节点都能处理用户的读请求。

6. etcd的AIP一览

etcd中处理API的包称为Store,顾名思义,Store模块就像一个商店一样把etcd已经准备好的各项底层支持加工起来,为用户提供各式各样的API支持,处理用户的各项请求。要理解Store,就要从etcd的API入手。打开etcd的API列表,我们可以看到API,它们都是对etcd存储的键值进行的操作,亦即Store提供的内容。API中提到的目录(directory)和键(key),上文中有时也称为etcd节点。

etcd并不是一个简单的分布式键值存储系统。它解决了分布式场景中最为常见的数据一致性问题,为服务发现提供了一个稳定、高可用的消息注册仓库,为以微服务协同工作的架构提供了无限的可能。