3月技术周 | 面对SSH暴力破解,给你支个招


在最近一次云上线的过程中,频繁遇到绑定公网浮动IP的云主机遭受外界SSH暴力破解攻击及用户设置弱密码的问题,由此引发的安全问题引起了针对防御SSH暴力破解的思考。


SSH暴力破解


hydra 和medusa是世界顶级密码暴力破解工具,支持几乎所有协议的在线密码破解,功能强大,密码能否被破解关键取决于破解字典是否足够强大。在网络安全渗透过程中,hydra 和medusa是必备的测试工具,配合社工库进行社会工程学攻击,有时会获得意想不到的效果。图示两款工具使用密码字典穷举SSH密码的过程。


3月技术周 | 面对SSH暴力破解,给你支个招

 

iptables限制ssh访问频率


面对暴力破解,根据其工作原理可知:降低其试错频率,提高其试错次数,从而将破解时间提高到不可容忍的程度,是一条有效的防范手段。


提高攻击方试错次数,无非是提升密码长度,扩展密码复杂度,定期更换密码这些手段。而降低攻击方的试错频率其实也是一条值得一试的防御手段。


通过调用iptables的state模块与recent模块,实现对SSH访问的频率限制。这里重点解释下不常用的recent扩展模块。


recent模块


Recent,该扩展能够动态的创建IP地址列表,用于后期以多种不同形式做出匹配。该扩展支持以下多种选项:


3月技术周 | 面对SSH暴力破解,给你支个招


iptables规则内容


要实现对SSH访问频率的控制,iptables规则如下两条:


#若是SSH访问,源IP在最近访问列表中,且60秒内访问次数大于等于3次,则丢弃。        

iptables -I INPUT -p tcp –dport 22 -m state –state NEW -m recent –name SSH_RECENT –rcheck –seconds 60 –hitcount 3 -j DROP 

             

#若是SSH访问,则将源IP加入最近访问列表中。                                        

iptables -I INPUT -p tcp –dport 22 -m state –state NEW -m recent –name SSH_RECENT –set                                                           

实现效果


实现效果如下图所示。高频率的密码试错将被终结,直至一分钟超时后才可重新开始。


3月技术周 | 面对SSH暴力破解,给你支个招


在/proc/net/xt_recent目录中,存在名为SSH_RECENT的一个日志文件。文件中记录了上面输入的iptables规则记录的最近访问SSH服务的源IP信息以及访问时间。其中默认记录的oldest_pkt是20个,可以通过modprobe ipt_recent ip_pkt_list_tot=50调大。默认记录的源IP是100个,可以通过modprobe ipt_recent ip_list_tot=1024 扩大记录数量。


3月技术周 | 面对SSH暴力破解,给你支个招


iptables实现远程开启ssh功能


任何一次靠谱的网络攻击都起步于网络侦查。如果攻击者在网络侦查阶段未发现目标开启SSH登录服务,这也将挫败其针对SSH发起攻击的计划。这里常用的操作都是更改SSH的默认22端口至其他端口号上以迷惑端口扫描软件。实际通过nmap等工具还是可以扫描到端口上捆绑的具体服务,如下图所示。这里通过一个取巧的办法,利用指定报文长度的ICMP作为钥匙,开启主机上的SSH服务。通过这种方式隐藏SSH服务端口。


3月技术周 | 面对SSH暴力破解,给你支个招


iptables规则内容


以指定包长的ICMP报文,作为钥匙,开启对端的SSH服务。具体iptables规则如下所示。


#用78字节的icmp数据包作为钥匙(包含IP头部20字节,ICMP头部8字节),将源IP加入SSH白名单          iptables -A INPUT -p icmp –icmp-type 8 -m length –length 78 -m recent –name SSH_ALLOW –set -j ACCEPT    

                                         

#检查访问SSH服务的源IP是否在白名单中,且白名单中的IP有效期为15秒。若在白名单中则放行通讯。 

iptables -A INPUT -p tcp –dport 22 -m state –state NEW -m recent –name SSH_ALLOW  –rcheck –seconds 15 -j ACCEPT  

                         

#对于已建立的SSH连接放行                                                          

iptables -A INPUT -p tcp –dport 22 -m state –state ESTABLISHED -j ACCEPT                                                                            

#其他SSH无关匹配全部拒止                                                           

iptables -A INPUT -p tcp –dport 22 -j DROP


实现效果


最终可以实现下图所示效果。在未使用指定包长ICMP之前,SSH服务无法通行(步骤1)。在使用指定包长ping之后(步骤2),使用SSH可以正常连接(步骤3)。以此实现了指定包长ICMP作为钥匙开启SSH通信服务的效果。其原理与上节限制SSH通信频率的原理一致。


3月技术周 | 面对SSH暴力破解,给你支个招


Fail2ban防止SSH暴力破解


安装:

Centos上可以直接通过yum install fail2ban –y安装。安装完成后,可在/etc/fail2ban路径下找到程序运行的相应文件。在filter.d目录下存放有fail2ban支持的所有过滤器,action.d目录下存放有fail2ban支持的所有动作。通过在jail配置文件中组合多种过滤器与动作,可以实现各种自定义的防御功能(不仅限于SSH防护)。


配置及运行:

对于fail2ban而言,每个.conf配置文件都可以被同名的.local文件重写。程序先读取.conf文件,然后读取.local文件。.local中的配置优先级更高。通过新建jail.local,增加下述配置,运行fail2ban-client start来实现对SSH暴力破解的防御。

[DEFAULT]

#白名单

ignoreip = 127.0.0.1/8

#解封禁时间

bantime  = 600

#试错窗口时间

findtime  = 600

#容许试错次数

maxretry = 3

 

[ssh-iptables]

#使能

enabled = true

#选择过滤器

filter = sshd

#选择防御动作

action = iptables[name=SSH, port=ssh, protocol=tcp]

#邮件通知

sendmail-whois[name=SSH,dest=yang.hongyu@99cloud.net, sender=test@email.com]

#SSH日志路径

logpath = /var/log/secure

#容许试错次数(优先级比default高)

maxretry = 1


运行效果:

通过对目标主机的SSH试错,/var/log/secure日志中记录了SSH登录的错误信息。fail2ban通过对该文件的分析,识别出当前正在遭遇到SSH的暴力破解,继而触发防御功能。fail2ban-client status命令可以查看当前fail2ban的运行状态,遭遇SSH暴力破解后,识别到的攻击IP被添加至Banned IP list中,实际阻断功能则是fail2ban通过在iptables中下发针对攻击IP的阻断规则来实现。


3月技术周 | 面对SSH暴力破解,给你支个招


Denyhost防止SSH暴力破解


Denyhost工作原理与Fail2ban基本一致,同样是分析SSH的日志文件,定位重复的暴力破解IP。与Fail2ban通过写iptables规则阻断攻击IP的访问不同,Denyhost通过将攻击IP记录到hosts.deny文件来实现屏蔽攻击IP对SSH的访问。


Denyhost安装:

wget “downloads.sourceforge.net/project/denyhosts/denyhosts/2.6/DenyHosts-2.6.tar.gz”

tar -xzf DenyHosts-2.6.tar.gz 

cd DenyHosts-2.6

python setup.py install


Denyhost配置及运行:

#生成配置文件副本

cd /usr/share/denyhosts/

#生成配置文件副本

cp denyhosts.cfg-dist denyhosts.cfg

#生成执行文件副本

cp daemon-control-dist daemon-control 

chmod 700 daemon-control 

#自定义配置文件denyhosts.cfg

#SSH log路径

SECURE_LOG = /var/log/secure

#存储SSH拒止host信息的配置文件路径

HOSTS_DENY = /etc/hosts.deny

#拒止时间,此处配置为10分钟

PURGE_DENY = 10m

#无效用户登录重试次数限制

DENY_THRESHOLD_INVALID = 5

#有效用户登录重试次数限制

DENY_THRESHOLD_VALID = 10

#ROOT用户登录重试次数限制

DENY_THRESHOLD_ROOT = 1

#启动运行

./daemon-control start


Denyhost效果:

从Denyhost的运行日志中看出,对目标主机的多次SSH密码试错触发了Denyhost的防御功能。攻击者的IP被添加至hosts.deny文件,该IP下的SSH访问也被拒止。


3月技术周 | 面对SSH暴力破解,给你支个招

3月技术周 | 面对SSH暴力破解,给你支个招

 

网络安全,何来一招鲜


可能有些人要说使用密钥登录就能完美解决SSH暴力破解的问题。这里要说一段历史。2006年Debian Linux发行版中发生了一件有意思的事,软件自动分析工具发现了一行被开发人员注释掉的代码。这行被注释掉的代码用来确保创建SSH秘密钥的信息量足够大。该代码被注释后,密钥空间大小的熵值降低到215。这意味着不论哪种算法和密钥长度,最终生成的密钥一共只有32767个,复杂度比一个纯6位数字的密码的复杂度更差。该错误在两年之后才被发现,无疑相当多的服务器上都利用这这种存在缺陷的弱密钥。(引用自:Violent Python:A Cookbook for Hackers)


网络安全没有一招鲜。前文中列举的四种安全加固方式也无法抵御运维人员设置的弱密码,及攻击者的社工密码库。运维人员,唯有提高自身安全意识,合理利用安全工具,才能保障网络安全。


最后说一句:道路千万条,安全第一条。操作不规范,运维两行泪。


关于九州云99Cloud

九州云成立于2012年,是中国早期从事开放基础架构服务的专业公司。公司成立六年,秉承“开源 · 赋能变革”的理念,不断夯实自身实力,先后为政府、金融、运营商、能源、制造业、商业、交通、物流、教育、医疗等各大行业的企业级客户提供高质量的开放基础架构服务。目前拥有国家电网、南方电网广东公司、中国人民银行、中国银联、中国移动、中国电信、中国联通、中国资源卫星、eBay、国际陆港集团、中国人寿、万达信息、东风汽车、诺基亚等众多重量级客户,被用户认可为最值得信赖的合作伙伴。

3月技术周 | 面对SSH暴力破解,给你支个招

3月技术周 | 我非要捅穿这 Neutron(一):底层网络实现模型篇

前言

有人说 Neutron 难学,不信邪的我非要捅穿这 Neutron。

本系列将从整体上介绍 Neutron 的部署架构、网络实现模型、上层资源模型、底层技术支撑、设计意图以及实践案例。目的是从鸟瞰的视角掌握 Neutron 的全局。本文参考和引用了大量的文献与书籍,于文末附有链接,感谢知识的分享(传播)者们。

传统网络到虚拟化网络的演进

传统网络3月技术周 | 我非要捅穿这 Neutron(一):底层网络实现模型篇虚拟网络3月技术周 | 我非要捅穿这 Neutron(一):底层网络实现模型篇分布式虚拟网络3月技术周 | 我非要捅穿这 Neutron(一):底层网络实现模型篇

单一平面网络到混合平面网络的演进

单一平面租户共享网络:所有租户共享一个网络(IP 地址池),只能存在单一网络类型(VLAN 或 Flat)。

  • 没有私有网络

  • 没有租户隔离

  • 没有三层路由

3月技术周 | 我非要捅穿这 Neutron(一):底层网络实现模型篇

多平面租户共享网络:具有多个共享网络供租户选择。

  • 没有私有网络

  • 没有租户隔离

  • 没有三层路由

3月技术周 | 我非要捅穿这 Neutron(一):底层网络实现模型篇

混合平面(共享/私有)网络:共享网络和租户私有网络混合。

  • 有私有网络

  • 没有租户隔离

  • 没有三层路由

NOTE:因为多租户之间还是依赖共享网络(e.g. 需要访问外部网络),没有做到完全的租户隔离。 3月技术周 | 我非要捅穿这 Neutron(一):底层网络实现模型篇

基于运营商路由功能的多平面租户私有网络:每个租户拥有自己私有的网络,不同的网络之间通过运营商路由器(公共)来实现三层互通。

  • 有私有网络

  • 有租户隔离

  • 共享三层路由


 3月技术周 | 我非要捅穿这 Neutron(一):底层网络实现模型篇

基于私有路由器实现的多平面租户私有多网络:每个租户可以拥有若干个私有网络及路由器。租户可以利用私有路由器实现私有网络间的三层互通。

  • 有私有网络

  • 有租户隔离

  • 有三层路由

3月技术周 | 我非要捅穿这 Neutron(一):底层网络实现模型篇

Neutron 简述

Neutron is an OpenStack project to provide “network connectivity as a service” between interface devices (e.g., vNICs) managed by other OpenStack services (e.g., nova).  The OpenStack Networking service (neutron) provides an API that allows users to set up and define network connectivity and addressing in the cloud. The project code-name for Networking services is neutron. OpenStack Networking handles the creation and management of a virtual networking infrastructure, including networks, switches, subnets, and routers for devices managed by the OpenStack Compute service (nova). Advanced services such as firewalls or virtual private network (VPN) can also be used. — — 官方网站:https://docs.openstack.org/neutron/latest/

Neutron 是为 OpenStack 提供 Network Connectivity as s Service 的项目,当然 Neutron 也可以脱离 Keystone 体系作为一个独立的 SDN 中间件。

  • 作为 OpenStack 组件:为 OpenStack 虚拟机、裸机、容器提供网络服务。

  • 作为 SDN 中间件:北向提供统一抽象资源接口,南向对接异构 SDN 控制器。

之所以有前两个章节,是希望以此来引出 Neutron 所追求的目标 —— 云计算时代的分布式虚拟化网络与多租户隔离私有的多平面网络

从软件实现的角度来看,对 Neutron 熟悉的开发者不难察觉:Neutron 团队肯定首先设计好 Core API Models(核心资源模型)然后再往下实现的。这也是很多 OpenStack 项目的实现风格,值得我们学习。在设计软件架构时,不要急于撸代码,而是首先思考清楚 「XXX as a Service」中 Service 的含义,即 “什么是服务、为谁服务、提供什么服务、如何提供”?Neutron 提供的网络即服务的精髓就在于租户只需关心服务,不必关心实现细节,而服务的内容就是资源类型(API)的定义。只有稳定、兼容、平滑过渡的 API 才能引发 APIs 经济(围绕开放式 API 的生态圈),让软件得以在残酷的开源市场中存活下来。

而且 Neutron 的设计初衷是纯粹的 “软件定义”,无需设计新的硬件,而是对现有硬件的协同,这是一个务实的设计思想,通过 Plugin-Driver 的方式来调用底层物理/虚拟网元功能,为上层服务提供支撑。这一特点使 Neutron 得以在业界收到广泛的关注。

Neutron 的网络实现模型

在了解 Neutron 网络实现模型的过程中,我们主要弄明白三个问题:

  • Neutron 是怎么支持多类型网络的(e.g. VLAN、VxLAN)?

  • 同一个租户网络内的跨计算节点虚拟机之间是怎么通信的?

  • 虚拟机是怎么访问外网的?

Neutron 的网络实现模型概览3月技术周 | 我非要捅穿这 Neutron(一):底层网络实现模型篇

计算节点网络实现模型

3月技术周 | 我非要捅穿这 Neutron(一):底层网络实现模型篇

下面我们直接介绍计算机节点上的虚拟机流量是如果被送出计算节点的,并在过程中插入介绍相关的虚拟网络设备于工作机理。

Step 1. 流量经由虚拟机内核 TCP/IP Stack 交给虚拟网卡 vNIC 处理,vNIC 是一个 Tap 设备,它允许用户态程序(这里指 GuestOS,一个 qemu 进程)向内核 TCP/IP 协议栈注入数据。Tap 设备可以运行在 GuestOS 上,提供与物理 NIC 完全相同的功能。

Step 2. 虚拟机的 vNIC(Tap 设备)没有直连到 OvS Bridge 上,而是通过 Linux Bridge 中继到 OvS br-int(Integrated bridge,综合网桥)。为什么 vNIC 不直连 br-int?这是因为 OvS 在 v2.5 以前只有静态防火墙规则,并不支持有状态的防火墙规则。这些规则是 Neutron Security Group 的底层支撑,为虚拟机提供针对端口(vNIC)级别的安全防护,所以引入了 Linux Bridge 作为安全层,应用 iptables 对有状态的数据包进行过滤。其中 Linux Bridge qbr 是 quantum bridge 的缩写。

Step 3. Linux Bridge 与 OvS Bridge 之间通过 veth pair (虚拟网线)设备连接,“网线” 的一端命名为 qvb(quantum veth bridge)另一端命名为 qvo(quantum veth ovs)。veth pair 设备总是成对存在的,用于连接两个虚拟网络设备,模拟虚拟设备间的数据收发。veth pair 设备的工作原理是反转通讯数据的方向,需要发送的数据会被转换成需要收到的数据重新送入内核 TCP/IP 协议栈进行处理,最终重定向到目标设备(“网线” 的另一端)。

Step 4. OvS br-int 是一个综合网桥,作为计算节点本地(Local)虚拟交换设备,完成虚拟机流量在本地的处理 —— Tenant flows are separated by internally assigned VLAN ID.(Tenant 网络的 Local VLAN ID 由 内部分配

  • 虚拟机发出的流量在 br-int 打上 Local VLAN tag,传输到虚拟机的流量在 br-int 被去掉 Local VLAN tag。

  • 本地虚拟机之间的 2 层流量直接在 br-int 转发,被同 VLAN tag 的端口接收。

Step 5. 从本地虚拟机传输到远端虚拟机(或网关)的流量由 OvS br-int 上的 int-br-eth1(ovs patch peer 设备)端口送到 OvS br-ethX 上。需要注意的是,br-ethX 并未是一成不变的:当网络类型为 Flat、VLAN 时,使用 br-ethX;当网络类型为 Tunnel(e.g. VxLAN、GRE)时,br-ethX 就会被 br-tun 替换。上图示例为 VLAN 网络。我们将 br-ethX、br-tun 统称为租户网络层网桥设备(TNB),以便于叙述。

NOTE:qbr-xxx 与 OvS br-int 之间使用 veth pair 设备相连,br-int 与 br-ethx/br-tun 之间使用 patch peer 设备相连。两种都是类似的虚拟 “网线”,区别在于前者是 Linux 虚拟网络设备,后者是 OvS 实现的虚拟网络设备。

NOTE:如果租户网桥是 br-tun 而不是 br-ethX,那么在 br-tun 上会有 port:patch-int,br-int 上会有 port:patch-tun,通过 patch-int 和 patch-tun 实现租户网桥 br-tun 和本地网桥 br-int 之间的通信。

Step 6. 当虚拟机流量流经 OvS br-int 与 OvS br-ethX 之间时,需要进行一个至关重要且非常复杂的动作 —— 内外 VID 转换。这是一种 “分层兼容” 的设计理念,让我想起了:所有计算机问题都可以通过引入一个中间层来解决。而 Neutron 面对的这个问题就是 —— 支持多种租户网络类型(Flat、VLAN、VxLAN、GRE)。

Step 7. 最后虚拟机流量通过挂载到 TNB(br-ethX/br-tun)上的物理网卡 ethX 发出到物理网络。OvS br-int 与 OvS br-ethX 之间也是通过 veth pair 设备实现。

Step 8. 虚拟机的流量进入到物理网络之后,剩下的就是传统网络的那一套了。网络包被交换机或其他桥接设备转发到其他计算、网络节点(PS:这取决于具体的部署网络拓扑,一般虚拟机流量只会被同处 Internal Network 的计算节点)接收到。

内外 VID 转换

从网络层的角度看,计算节点的网络模型可分为:租户网络层、本地网络层。 

其中本地网络层通过 VLAN ID 来划分 br-int 中处于不同网络的虚拟机(端口),本地网络仅支持 VLAN 类型;而租户网络层为了实现多类型混合平面的需求却要支持非隧道类型网络 Flat、VLAN(br-ethX)及隧道类型网络 VxLAN、GRE(br-tun)。显然,本地网络层和租户网络层之间必须存在一层转换。这就是所谓的 VID 转换。

VID 是一个逻辑概念,针对不同的租户网络类型也有不同类型的 “VID”。

本地网络层 租户网络层
VLAN(ID) VLAN(ID)
VLAN(ID) VxLAN(VNI)
VLAN(ID) GRE(key)

需要注意的是 VID 的范围是由租户定义的 —— Tenant flows are separated by user defined VLAN ID.(租户网络的 VID 由 User Defined,此时示例为 VLAN 类型网络)。所谓 “User Defined” 就是用户通过 Neutron 配置文件 ml2_conf.ini 为各种网络类型配置的 range。e.g.

  1. # /etc/neutron/plugins/ml2/ml2_conf.ini


  2. [ml2_type_vlan]

  3. network_vlan_ranges = public:3001:4000


  4. [ml2_type_vxlan]

  5. vni_ranges = 1:1000


  6. [ml2_type_gre]

  7. tunnel_id_ranges = 1:1000

再次强调,本地网络的 VLAN ID 是由内部代码逻辑算法分配的,而租户网络的 VID 才是由用户自定义配置的

或许你会感到奇怪,为什么租户网络与本地网络同为 VLAN 类型的情况下还需要进行内外 VID 转换?解答这个问题只需要辩证思考一下:加入不进行内外 VID 转换会怎样?答案是会在混合 VLAN 和 VxLAN 类型租户网络的场景中出现内外 VID 冲突的情况。 

假设网络1:租户网络 VLAN 的 VID 为 100,不做内外 VID 转换,那么本地网络 VLAN ID 也是 100。 网络2:租户网络 VxLAN 的 VID 为 1000,进行内外 VID 转换,本地网络 VLAN 100 也可能是 100。

这是因为 VxLAN 网络再进行内外 VID 转换的时候并不知道 VLAN 网络的 VID 是多少,只有当 VLAN 网络进行了内外 VID 转换之后才会知道,因为 VID 转换是有由 OvS Flow Table 记录并执行的 —— VLAN ID is converted with flow table

所以 VLAN 类型网络也要进行内外 VID 转换是为了防止混合 VLAN 和 VxLAN 类型租户网络的场景中出现内外 VID 冲突的情况

至此,我们就可以回到开篇的其中两个问题了。

  • Neutron 是怎么支持多类型网络的?

  • 同一个租户网络内的跨计算节点虚拟机之间是怎么通信的?

网络节点网络实现模型

网络节点所承载的最核心的功能就是解决虚拟机与外部网络(e.g. Internet)通信的问题,围绕这个问题我们设想一下在传统网络中是怎么实现的?

  1. 所有计算节点内的虚拟机,要访问 Internet,必须先经过网络节点,网络节点作为第一层网关。

  2. 网络节点会通过连接 DC 物理网络中的一个设备(e.g. 交换机,或者是路由器)到达 DC 的网关 。 这个设备称为第二层网关。当然,网络节点也可以直接对接 DC 网关(如果 DC 网关可控的话),这时候,就不需要第二层网关了。

  3. DC 网关再连接到 Internet上。

可见,网络节点要处理的事情就是第一层网关处理的事情,西接所有计算节点的流量,东接物理网络(第二层)网关设备。为此,网络节点所需要的元素就是一个 L3 Router。需要注意的是,这里所提到的 L3 Router 并不是一个 vRouter(SDN 中的虚拟路由器),而是网络节点本身(一个可作为路由器的 Linux 服务器)。借助于 Linux 服务器本身的路由转发功能,网络节点得以成为了上文提到的:访问外部网络所需要通过的 第一层网关

依旧从分层的角度来看,网络节点的网络实现模型可以分为 4 层:

  • 租户网络层:对接所有(计算、控制、网络)节点,支持多种网络类型。

  • 本地网络层:基于内外 VID 转换机制,通过 Local VLAN tag 来区分网络包所属的网络。

  • 服务网络层:提供 L3 Router 和 DHCP 服务。

  • 外部网络层:对接外部网络(e.g. Internet)。

网络节点出了提供 L3 Router 服务,同时也为每个有必要(用户手动开启)的租户网络提供 DHCP 服务。为了实现多租户网络资源隔离,应用了 Linux 提供的 Network Namesapce 技术。每创建一个 L3 Router 资源对象,就会添加一个与之对应的 qrouter-XXX namesapce;每为一个租户网络开启 DHCP 服务,就会添加一个与之对应的 qdhcp-XXX namesapce。 3月技术周 | 我非要捅穿这 Neutron(一):底层网络实现模型篇 上图可见,DHCP 服务实际是由 dnsmasq 服务进程提供的,在 qdhcp-XXX namesapce 中会添加一个 DHCP 的端口(Tap 设备),这个 DHCP 端口连接到 br-int 上,具有与对应的租户网络相同的 Local VLAN tag(和租户网络 VID),以此实现为租户网络提供 DHCP 服务。

除此之外,还可以看见在 qrouter-XXX namesapce 中 br-int 和 br-ex 两个网桥设备通过 veth pair 设备 qr-XXX(quantum router) 和 qg-XXX(quantum gateway)连接到了一起。而且 qr-XXX 端口也具有租户网络对应的 Local VLAN tag。不同的租户网络通过不同的 Network Namesapce 实现了 Router 配置(路由表、iptables 规则)的隔离,启用 iptables 在 qrouter-XXX 中主要是提供 NAT 功能,支撑 Neutron 的 Floating IP 功能。

不同的租户具有不同的 qrouter-XXX 与 qdhcp-XXX 实例,所以不同租户间可以实现 IP 地址的复用3月技术周 | 我非要捅穿这 Neutron(一):底层网络实现模型篇在计算节点的网络模型章节中国我们介绍了虚拟机流量如何被送出计算节点,这里继续介绍虚拟机流量是如何被送出外部网络的:

Step 9. 物理网卡 ethX(OvS br-ethX/br-tun)从物理网络接收到从计算节点上虚拟机发出的外部网络访问流量,首先进行内外 VID 转换,然后通过 VETH Pair 设备传输到 OvS br-int。

NOTE:针对每一个租户网络在租户网络层上的 VID 肯定的一致的,而在不同(计算、网络)节点上的 Local VLAN ID 却未必一样,但这并不会导致不同租户网络间的数据包混乱,因为这一切都在 Open vSwitch 流表的掌握之中,我们暂且先对这个问题有个印象。

Step 10. 在网络节点上 OvS br-int 连接了不同的 Network namesapce,qrouter-XXX 通过 qr-XXX 端口接收到租户内跨网段访问流量以及公网访问流量。

  • 跨网段访问流量:qr-XXX 端口接收到流量后,内核 TCP/IP 协议栈根据 qrouter-XXX 中的路由规则对跨网段访问流量进行路由、改写 MAC 地址并通过相应的 qr-YYY 接口向 OvS br-int 传输数据包。实现不同网段之间的路由转发。

  • 公网访问流量:qr-XXX 端口接收到流量后,内核 TCP/IP 协议栈根据 qrouter-XXX 中的 iptables NAT 规则对流量进行网络地址转换(Floating IP 的实现原理),之后再通过 qg-XXX 接口将数据包发送给 OvS br-ex。

Step 11. 最终由连接第二层网关并挂载到 br-ex 上的物理网卡 ethX 发出送至 Internet 路由器。

综上,“虚拟机是怎么访问外网的?” 这个问题解决了。

控制节点的网络实现模型

控制节点的网络实现模型就相对好理解很多了,因为控制节点不负责数据平面的问题,所以也没有实现具体的网络功能。我们需要关心的只是 neutron-server 这个服务进程。Neutron 没有显式(名为)的 neutron-api 服务进程,而是由 neutron-server 提供 Web Server 的服务。北向接收 REST API 请求,南向通过 RPC 协议转发各节点上的 Neutron Agent 服务进程,这就是 Neutron 在控制节点上的网络实现模型。

至此,我们介绍完了 Neutron 在分别计算、网络、控制节点上的网络实现模型,我们不妨回味一下这之间最重要的的设计思想 —— 分层。

让我们先抛开 OpenStack 和 Neutron,单纯的想象如何应用虚拟交换机实现跨主机间的虚拟机通信以及虚拟机访问外网的需求?答案其实很简单,如果应用 OvS,就会简单到只需要在每个节点上创建一个 Bridge(e.g. br-int)设备即可,无需 br-ex、br-ethX/br-tun、Linux Bridge 等等一大堆东西。e.g.

  1. # Host1

  2. ovs-vsctl add-br br-vxlan

  3. # Host2

  4. ovs-vsctl add-br br-vxlan


  5. # Host1 上添加连接到 Host2 的 Tunnel Port

  6. ovs-vsctl add-port br-vxlan tun0 -- set Interface tun0 type=vxlan options:remote_ip=<host2_ip>


  7. # Host2 上添加连接到 Host1 的 Tunnel Port

  8. ovs-vsctl add-port br-vxlan tun0 -- set Interface tun0 type=vxlan options:remote_ip=<host1_ip>

如果应用 Linux Bridge 的话,就会稍微复杂一些,正如我在《KVM + LinuxBridge 的网络虚拟化解决方案实践》所记录的一般,这里不再赘述。e.g. 3月技术周 | 我非要捅穿这 Neutron(一):底层网络实现模型篇

但如果考虑到需要实现的是在「云平台上的多租户多平面网络」的话,那么事情就会变得相当复杂。一个有生命力的灵活的云平台,需要支持的网络类型实在是太多了,Neutron 至今仍在为之付出努力。在这样的条件下就急需设计出一个「上层抽象统一,下层异构兼容」的软件架构,而这种架构设计就是我们常说的 —— 分层架构设计。设计方案总是依赖于底层支撑选型,Neutron 网络实现模型的分层架构得益于 Open vSwitch(OpenFlow 交换机)的 Flow Table 定义功能。通过这个章节,希望大家能够对此有所感受。

看到这里如果你是一位好奇的小伙伴,你或许还会具有以下疑问:

  • br-int 怎么为每个租户网络的端口分配 Local VLAN tag 的?

  • br-ethX/br-tun 是怎么维护 VID 转换隐射表的?

  • Router 是怎么设定网关路由的?

  • 物理网络和租户网络异同?

这些问题,我们将在下中一一解答。

参考文献

https://baijiahao.baidu.com/s?id=1590910154984727315&wfr=spider&for=pc https://cloud.tencent.com/developer/article/1083332


相关阅读:

2月技术周 | 启用 SR-IOV 提升 Neutron 网络 I/O 性能瓶颈

技术分享:OpenStack Neutron L3性能测试

OpenStack实践分享:Neutron中Linux bridge与Open vSwitch两种plugin优劣势对比


关于九州云99Cloud

九州云成立于2012年,是中国早期从事开放基础架构服务的专业公司。公司成立六年,秉承“开源 · 赋能变革”的理念,不断夯实自身实力,先后为政府、金融、运营商、能源、制造业、商业、交通、物流、教育、医疗等各大行业的企业级客户提供高质量的开放基础架构服务。目前拥有国家电网、南方电网广东公司、中国人民银行、中国银联、中国移动、中国电信、中国联通、中国资源卫星、eBay、国际陆港集团、中国人寿、万达信息、东风汽车、诺基亚等众多重量级客户,被用户认可为最值得信赖的合作伙伴。

3月技术周 | 我非要捅穿这 Neutron(一):底层网络实现模型篇

3月技术周 | Cinder 架构分析、高可用部署与核心功能解析

Cinder

操作系统获得外部存储空间一般有以下三种方式:

Block Storage:通过某种协议(e.g.SAS、SCSI、SAN、iSCSI)从后端存储 Assigned、Attached 块设备(Volume),然后分区格式化、创建文件系统并 mount 到操作系统,然后就可以在此文件系统之上存储数据,或者也可以直接使用裸硬盘来存储数据(e.g. 数据库系统)

FileSystem Storage:通过 NFS、CIFS 等协议,mount 远程文件系统到本地操作系统。NAS、NFS 服务器,以及各种分布式文件系统提供的都是这种存储。

Object Storage:对象存储是以对象形式存储数据的存储系统,最大的优势就是可以让用户更加灵活的处理海量数据。操作系统客户端可以通过对象存储提供的存储网关接口(一般是 HTTP/S)来上传或下载存储数据。

而本篇的主角就是 Cinder,OpenStack Block Storage as a Services(块存储即服务)。

Cinder is the OpenStack Block Storage service for providing volumes to Nova virtual machines, Ironic bare metal hosts, containers and more. Some of the goals of Cinder are to be/have:

  • Component based architecture: Quickly add new behaviors

  • Highly available: Scale to very serious workloads

  • Fault-Tolerant: Isolated processes avoid cascading failures

  • Recoverable: Failures should be easy to diagnose, debug, and rectify

  • Open Standards: Be a reference implementation for a community-driven api — — 官方文档:https://docs.openstack.org/cinder/latest/

需要注意的是,正如 Nova 本身不提供 Hypervisor 技术一般,Cinder 自身也不提供存储技术,而是作为一个抽象的中间管理层,北向提供稳定而统一的 Block Storage 资源模型、南向通过 Plug-ins&Drivers 模型对接多样化的后端存储设备(e.g. LVM、CEPH、NetApp、Datastore etc.)。所以 Cinder 的精华从不在于存储技术,而是在于对 Block Storage as a Service 需求(创建、删除、快照、挂载、分离、备份卷)的抽象与理解。

  • 存储资源模型(e.g. Volume, Snapshot, Pool etc.)

  • 兼容多样化存储设备的 “逻辑驱动层”

  • 分布式架构

  • 高可用设计

Cinder 的软件架构

3月技术周 | Cinder 架构分析、高可用部署与核心功能解析

cinder-api

a WSGI app that authenticates and routes requests throughout the Block Storage service. It supports the OpenStack APIs only, although there is a translation that can be done through Compute’s EC2 interface, which calls in to the Block Storage client.

对外提供稳定而统一的北向 RESTful API,cinder-api service 服务进程通常运行在控制节点,支持多 Workers 进程(通过配置项 osapivolumeworkers 设定)。接收到的合法请求会经由 MQ 传递到 cinder-volume 执行。Cinder API 现存 v2(DEPRECATED)、v3(CURRENT) 两个版本,可以通过配置文件来启用。

Cinder API 官方文档:https://developer.openstack.org/api-ref/block-storage/

NOTE:从 API 的官方文档,或从 /opt/stack/cinder/cinder/db/sqlalchemy/models.py 都可以从侧面对 Cinder 的资源模型定义有所了解。

cinder-scheduler

schedules and routes requests to the appropriate volume service. Depending upon your configuration, this may be simple round-robin scheduling to the running volume services, or it can be more sophisticated through the use of the Filter Scheduler. The Filter Scheduler is the default and enables filters on things like Capacity, Availability Zone, Volume Types, and Capabilities as well as custom filters.

如果说 cinder-api 接收的是关于 “创建” 的请求(e.g. Create Volume),那么该请求就会通过 MQ 转发到 cinder-scheduler service 服务进程,cinder-scheduler 与 nova-scheduler 一般,顾名思义是调度的层面。通过 Filters 选择最 “合适” 的 Storage Provider Node 来对请求资源(e.g. Volume)进行创建。不同的 Filters 具有不同的过滤(调度)算法,所谓的 “合适” 就是达到客户预期的结果,用户还可以自定义 Filter Class 来实现符合自身需求的过滤器,让调度更加灵活。与 nova-scheduler 一般,cinder-scheduler 同样需要维护调度对象(存储节点)“实时” 状态,cinder-volume service 会定期的向 cinder-scheduler service 上报存储节点状态(注:这实际上是通过后端存储设备的驱动程序上报了该设备的状态)。

  1. 首先判断存储节点状态,只有状态为 up 的存储节点才会被考虑。

  2. 创建 Volume 时,根据 Filter 和 Weight 算法选出最优存储节点。

  3. 迁移 Volume 时,根据 Filter 和 Weight 算法来判断目的存储节点是否符合要求。

现支持的 Filters 清单如下

  • AffinityFilter

  • AvailabilityZoneFilter:可以在 cinder-volume(存储节点)的 cinder.conf 中设置 storage_availability_zone=az1 来指定该存储节点的 Zone。配合 AvailabilityZoneFilter,用户创建 Volume 时选择期望的 AZ,就可以实现将 Volume 创建到指定的 AZ 中了。 默认 Zone 为 nova。

  • CapabilitiesFilter:不同的 Volume Provider 自然具有不同的特点,用户可以通过设置 Volume Type 的 extra specs 来描述这些特性,该 Filter 就是为了通过这些特性来过滤存储节点。

  • CapacityFilter:根据存储节点上的剩余空间(freecapacitygb)大小来进行过滤,存储节点的 freecapacitygb 正是由 cinder-volume service 上报的。

  • DriverFilter

  • IgnoreAttemptedHostsFilter

  • InstanceLocalityFilter

  • JsonFilter

通过 nova-scheduler service 的配置文件 cinder.conf 指定你需要使用的 Filters 列表,e.g.

  1. [DEFAULT]

  2. ...

  3. scheduler_driver =cinder.scheduler.filter_scheduler.FilterScheduler

  4. scheduler_default_filters =AvailabilityZoneFilter,CapacityFilter,CapabilitiesFilter

现支持的 Weights 算法如下

  • AllocatedCapacityWeigher:有最先可使用空间的权重大,相关配置项有 allocated_capacity_weight_multiplier

  • CapacityWeigher:有最大可使用空间的权重大,相关配置项有 capacity_weight_multiplier

  • ChanceWeigher:随意选取

  • VolumeNumberWeigher

  • GoodnessWeigher

当然了,在实际使用中也存在不需要或者说不希望进行调度的情况,还是正如 Nova 一般,创建虚拟机时可以通过 --availability-zone AZ:Host:Node 来强制指定计算节点,Cinder 也有这般手段。

cinder-volume

manages Block Storage devices, specifically the back-end devices themselves.

cinder-volume service 是 Cinder 的核心服务进程,运行该服务进程的节点都可以被称之为存储节点。cinder-volume 通过抽象出统一的 Back-end Storage Driver 层,让不同存储厂商得以通过提供各自的驱动程序来对接自己的后端存储设备,实现即插即用(通过配置文件指定),多个这样的存储节点共同构成了一个庞大而复杂多样的存储资源池系统。

Driver 框架

OpenStack 作为开放的 Infrastracture as a Service 云操作系统,支持业界各种优秀的技术,这些技术可能是开源免费的,也可能是商业收费的。 这种开放的架构使得 OpenStack 保持技术上的先进性,具有很强的竞争力,同时又不会造成厂商锁定(Lock-in)。

以 Cinder 为例,存储节点支持多种 Volume Provider,包括 LVM、NFS、Ceph、GlusterFS 以及 EMC、IBM 等商业存储系统。 cinder-volume 为这些 Volume Provider 抽象了统一的 Driver 接口,Volume Provider 只需要实现这些接口,就可以以 Driver 的形式即插即(volume_driver 配置项)用到 OpenStack 中。下面是 Cinder Driver 的架构示意图:

3月技术周 | Cinder 架构分析、高可用部署与核心功能解析 

在 cinder-volume 的配置文件 /etc/cinder/cinder.conf 中设置该存储节点使用的 Volume Provider Driver 类型:

  1. # 启用 LVM Provider(默认)

  2. volume_driver=cinder.volume.drivers.lvm.LVMVolumeDriver

Plugin 框架

Driver 和 Plugin 通常不会分家,Driver 是由各存储厂商提供的,那么 Plugins(插槽)就应该有 Cinder 的提供。 根据 FileSystem Storage 和 Block Storage 两个不同类型的外部存储系统,Cinder Plugins 也提供了 FileSystem based 和 Block based 两种不同类型 Plugin。除此之外,Cinder Plugins 还提供了 iSCSC、FC、NFS 等常用的数据传输协议 Plugin 框架,上传逻辑得以根据实际情况来使用(Attached/Dettached)存储资源。

cinder-backup

provides a means to back up a Block Storage volume to OpenStack Object Storage (swift).

提供 Volume 的备份功能,支持将 Volume 备份到对象存储中(e.g. Swift、Ceph、IBM TSM、NFS),也支持从备份 Restore 成为 Volume。

Cinder Backup 架构实现3月技术周 | Cinder 架构分析、高可用部署与核心功能解析

Volume Provider

Volume Provider 不属于 Cinder 架构的组件,其含义是真实的物理数据存储设备,为 Volume 提供物理存储空间,故又称 Backend Storage。Cinder 支持多种 Volume Provider,每种 Volume Provider 都通过自己的 Driver 与 cinder-volume 通信。之所以单独列出 Volume Provider,是为了强调 Cinder 本身并不具备存储技术,真实的存储资源,由后端 Volume Provider 提供。

Cinder 与 Volume Provider 之间的关系3月技术周 | Cinder 架构分析、高可用部署与核心功能解析

中间件

  • Messaging queue:Routes information between the Block Storage processes.

  • DB:sql database for data storage. Used by all components (LINKS NOT SHOWN).

创建 Volume 流程分析

创建 Volume 是 Cinder 的头一号功能,自然花样也就多了。

  • 创建 RAW 格式的卷

  • 从快照创建卷

  • 从已有卷创建卷(克隆)

  • 从 Image 创建卷

  • source_replica 创建卷

  1. # /opt/stack/cinder/cinder/volume/flows/manager/create_volume.py


  2. # 根据不同的类型有不同的创建执行细节

  3. create_type = volume_spec.pop('type', None)

  4. LOG.info("Volume %(volume_id)s: being created as %(create_type)s "

  5. "with specification: %(volume_spec)s",

  6. {'volume_spec': volume_spec, 'volume_id': volume_id,

  7. 'create_type': create_type})

  8. if create_type == 'raw':

  9. model_update = self._create_raw_volume(

  10. context, volume, **volume_spec)

  11. elif create_type == 'snap':

  12. model_update = self._create_from_snapshot(context, volume,

  13. **volume_spec)

  14. elif create_type == 'source_vol':

  15. model_update = self._create_from_source_volume(

  16. context, volume, **volume_spec)

  17. elif create_type == 'image':

  18. model_update = self._create_from_image(context,

  19. volume,

  20. **volume_spec)

  21. elif create_type == 'backup':

  22. model_update, need_update_volume = self._create_from_backup(

  23. context, volume, **volume_spec)

  24. volume_spec.update({'need_update_volume': need_update_volume})

  25. else:

  26. raise exception.VolumeTypeNotFound(volume_type_id=create_type)

这里我们先不考虑这些 “花样” 底层细节的区别,主要关注 create_volume 在 Cinder 所有服务部件之间的流转过程。 3月技术周 | Cinder 架构分析、高可用部署与核心功能解析

cinder-api 阶段

cinder-api service 接收 Post/v3/{project_id}/volumes 请求,经过了一系列的 Body 数据校验、数据类型转换(UUID => Object Model)和操作授权校验(context.authorize)之后,启动 volumecreateapi flow,此 Flow 中包含了下列 Tasks:

  1. ExtractVolumeRequestTask:获取(Extract)、验证(Validates)create volume 在 cinder-api 阶段相关的信息

  2. QuotaReserverTask:预留配额

  3. EntryCreateTask:在数据库中创建 Volume 条目

  4. QuotaCommitTask:确认配额

  5. VolumeCastTask:发出一个 Cast 异步请求,将创建请求丢到 MQ,最终被 cinder-scheduler service 接收

自此 volumecreateapi flow 完成了 pending -> running -> success 的整个流程,cinder-api 阶段结束。

需要注意的是,正如上文提到过的 create volume 也存在不需要进入 cinder-scheduler 的情况:

  1. # /opt/stack/cinder/cinder/volume/api.py


  2. # sched_rpcapi 可能为 None

  3. sched_rpcapi = (self.scheduler_rpcapi if (

  4. not cgsnapshot and not source_cg and

  5. not group_snapshot and not source_group)

  6. else None)

  7. volume_rpcapi = (self.volume_rpcapi if (

  8. not cgsnapshot and not source_cg and

  9. not group_snapshot and not source_group)

  10. else None)

  11. flow_engine = create_volume.get_flow(self.db,

  12. self.image_service,

  13. availability_zones,

  14. create_what,

  15. sched_rpcapi,

  16. volume_rpcapi)

cinder-scheduler 阶段

cinder-scheduler service 接收到了 create volume request 之后也会启动一个 volumecreatescheduler flow,此 Flow 包含了一下 Tasks:

  1. ExtraceSchedulerSpecTask:将 request body 转换成为 Scheduler Filter 中通用的 RequestSpec 数据结构(实例对象)。

  2. SchedulerCreateVolumeTask:完成 Filter 和 Weight 的调度算法。

最终在 cinder-scheduler service 选出一个最佳的存储节点(cinder-volume),并继续讲 create volume request 通过 RPC 丢到选中的 cinder-volume service 接收。

cinder-volume 阶段

同样的,cinder-volume service 还是启用了 TaskFlow:volumecreatemanager,该 Flow 具有以下 Tasks:

  1. ExtractVolumeRefTask:Extracts volume reference for given volume id

  2. OnFailureRescheduleTask:Triggers a rescheduling request to be sent when reverting occurs

  3. ExtractVolumeSpecTask:Extracts a spec of a volume to be created into a common structure

  4. NotifyVolumeActionTask:Performs a notification about the given volume when called.

  5. CreateVolumeFromSpecTask:Creates a volume from a provided specification.

  6. CreateVolumeOnFinishTask:On successful volume creation this will perform final volume actions.

其中最主要的我们关注 CreateVolumeFromSpecTask,该 Task 调用了后端存储设备的 Driver 真正执行 Volume 创建的任务。

  1. # /opt/stack/cinder/cinder/volume/flows/manager/create_volume.py


  2. def _create_raw_volume(self, context, volume, **kwargs):

  3. try:

  4. # 后端存储设备的驱动程序

  5. ret = self.driver.create_volume(volume)

  6. except Exception as ex:

  7. with excutils.save_and_reraise_exception():

  8. self.message.create(

  9. context,

  10. message_field.Action.CREATE_VOLUME_FROM_BACKEND,

  11. resource_uuid=volume.id,

  12. detail=message_field.Detail.DRIVER_FAILED_CREATE,

  13. exception=ex)

  14. finally:

  15. self._cleanup_cg_in_volume(volume)

  16. return ret

这里以 LVM Backend 为例,该 Task 的本质就是调用操作系统指令 lvcreate 从 Backend 对应的 VG 中划分出 LV(Volume)。

  1. # /opt/stack/cinder/cinder/brick/local_dev/lvm.py


  2. def create_volume(self, name, size_str, lv_type='default', mirror_count=0):

  3. """Creates a logical volume on the object's VG.


  4. :param name: Name to use when creating Logical Volume

  5. :param size_str: Size to use when creating Logical Volume

  6. :param lv_type: Type of Volume (default or thin)

  7. :param mirror_count: Use LVM mirroring with specified count


  8. """


  9. if lv_type == 'thin':

  10. pool_path = '%s/%s' % (self.vg_name, self.vg_thin_pool)

  11. cmd = LVM.LVM_CMD_PREFIX + ['lvcreate', '-T', '-V', size_str, '-n',

  12. name, pool_path]

  13. else:

  14. cmd = LVM.LVM_CMD_PREFIX + ['lvcreate', '-n', name, self.vg_name,

  15. '-L', size_str]


  16. if mirror_count > 0:

  17. cmd.extend(['--type=mirror', '-m', mirror_count, '--nosync',

  18. '--mirrorlog', 'mirrored'])

  19. terras = int(size_str[:-1]) / 1024.0

  20. if terras >= 1.5:

  21. rsize = int(2 ** math.ceil(math.log(terras) / math.log(2)))

  22. # NOTE(vish): Next power of two for region size. See:

  23. # http://red.ht/U2BPOD

  24. cmd.extend(['-R', str(rsize)])


  25. try:

  26. # 执行组装好的 CLI

  27. self._execute(*cmd,

  28. root_helper=self._root_helper,

  29. run_as_root=True)

  30. except putils.ProcessExecutionError as err:

  31. LOG.exception('Error creating Volume')

  32. LOG.error('Cmd :%s', err.cmd)

  33. LOG.error('StdOut :%s', err.stdout)

  34. LOG.error('StdErr :%s', err.stderr)

  35. LOG.error('Current state: %s',

  36. self.get_all_volume_groups(self._root_helper))

  37. raise

NOTE: 需要注意的是,虽然 LVM Driver 是通过 CLI 的方式来对 VG 进行操作的,但每种不同的后端存储设备都有自己的实现方式,例如:通过 HTTP/HTTPS、TCP Socket 来进行 Driver 与存储设备的连接。

TaskFlow

通过上述流程的分析,或者你会疑惑什么是 Flow?什么是 Task?这是因为 Cinder 在 create volume 的流程中采用了 TaskFlow 通用技术库,其带来的好处就是能够有效的保证了实际存储资源与代码逻辑记录的一致性,说白了就是避免了程序帐数据的出现。这不能算做一个小问题,因为存储卷管理的流程往往是长线、长时间的操作,自动化流程脆弱,如果没有一个有效的 “流程原子性” 机制,将导致程序的稳定性无法得到保证。

更多的 TaskFlow 资料请浏览:《Openstack 实现技术分解 (4) 通用技术 — TaskFlow》

创建 Volume 失败重试机制

可以在 cinder-volume service 的配置文件 cinder.conf 中 使用 scheduler_max_attempts来配置 Volume 创建失败重试的次数,默认值为 3,值为 1 则表示不启用失败重试机制。

  1. # Maximum number of attempts to schedule an volume (integer value)

  2. scheduler_max_attempts=3

cinder-sheduler 和 cinder-volume 之间会传递当前的失败重试次数。当 Volume 创建失败,cinder-volume 会通过 RPC 通知 cinder-scheduler 重新进行调度。

  1. # /opt/stack/cinder/cinder/volume/manager.py


  2. # 确定需要重新调度

  3. if rescheduled:

  4. # NOTE(geguileo): Volume was rescheduled so we need to update

  5. # volume stats because the volume wasn't created here.

  6. # Volume.host is None now, so we pass the original host value.

  7. self._update_allocated_capacity(volume, decrement=True,

  8. host=original_host)

cinder-scheduler 检查当前重试次数若没有超出最大重试次数,则会重新进入调度环节,选择当前最优存储节点重新创建 Volume。否则,就触发 No valid host was found 异常。Nova 同样具有 RetryFilter 机制,这是为了防止 “实时资源缺失(调度时参考的资源存量与实际资源存量有差距)” 问题的出现,进一步保障了操作的成功率。

删除 Volume 流程分析

还是从 REST API 看起: DELETE/v3/{project_id}/volumes/{volume_id}。删除的请求比较简单,指定要删除的 UUID of Volume。在版本较新的 API 中还支持 cascade 和 force 两个可选的 request query parameter。 3月技术周 | Cinder 架构分析、高可用部署与核心功能解析

  • cascade:删除所有相关的快照和卷。

  • force:无视卷状态,强制删除。

在经过 cinder-api service 一系列的删除 “预准备(主要是看这个 Volume 删了适合不适合)” 操作之后,delete volume request 依旧会进入 cinder-volume service。

  1. # /opt/stack/cinder/cinder/volume/manager.py


  2. """Deletes and unexports volume.


  3. 1. Delete a volume(normal case)

  4. Delete a volume and update quotas.


  5. 2. Delete a migration volume

  6. If deleting the volume in a migration, we want to skip

  7. quotas but we need database updates for the volume.


  8. 3. Delete a temp volume for backup

  9. If deleting the temp volume for backup, we want to skip

  10. quotas but we need database updates for the volume.

  11. """

最后的最后实际上还是交由后端存储设备的驱动程序来完成 “真·卷” 的删除。当然了,在整个删除的过程中还需要处理多种多样的复杂场景的问题,这里就不一一列举了,代码会清晰的告诉你。

  1. @utils.retry(putils.ProcessExecutionError)

  2. def delete(self, name):

  3. """Delete logical volume or snapshot.


  4. :param name: Name of LV to delete


  5. """


  6. def run_udevadm_settle():

  7. cinder.privsep.lvm.udevadm_settle()


  8. # LV removal seems to be a race with other writers or udev in

  9. # some cases (see LP #1270192), so we enable retry deactivation

  10. LVM_CONFIG = 'activation { retry_deactivation = 1} '


  11. try:

  12. # 执行 CLI 完成 LV 的删除

  13. self._execute(

  14. 'lvremove',

  15. '--config', LVM_CONFIG,

  16. '-f',

  17. '%s/%s' % (self.vg_name, name),

  18. root_helper=self._root_helper, run_as_root=True)

  19. except putils.ProcessExecutionError as err:

  20. LOG.debug('Error reported running lvremove: CMD: %(command)s, '

  21. 'RESPONSE: %(response)s',

  22. {'command': err.cmd, 'response': err.stderr})


  23. LOG.debug('Attempting udev settle and retry of lvremove...')

  24. run_udevadm_settle()


  25. # The previous failing lvremove -f might leave behind

  26. # suspended devices; when lvmetad is not available, any

  27. # further lvm command will block forever.

  28. # Therefore we need to skip suspended devices on retry.

  29. LVM_CONFIG += 'devices { ignore_suspended_devices = 1}'


  30. self._execute(

  31. 'lvremove',

  32. '--config', LVM_CONFIG,

  33. '-f',

  34. '%s/%s' % (self.vg_name, name),

  35. root_helper=self._root_helper, run_as_root=True)

  36. LOG.debug('Successfully deleted volume: %s after '

  37. 'udev settle.', name)

Volume 的资源模型

Data Module

  1. # /opt/stack/cinder/cinder/db/sqlalchemy/models.py


  2. class Volume(BASE, CinderBase):

  3. """Represents a block storage device that can be attached to a vm."""

  4. __tablename__ = 'volumes'

  5. __table_args__ = (Index('volumes_service_uuid_idx',

  6. 'deleted', 'service_uuid'),

  7. CinderBase.__table_args__)


  8. id = Column(String(36), primary_key=True)

  9. _name_id = Column(String(36)) # Don't access/modify this directly!


  10. @property

  11. def name_id(self):

  12. return self.id if not self._name_id else self._name_id


  13. @name_id.setter

  14. def name_id(self, value):

  15. self._name_id = value


  16. @property

  17. def name(self):

  18. return CONF.volume_name_template % self.name_id


  19. ec2_id = Column(Integer)

  20. user_id = Column(String(255))

  21. project_id = Column(String(255))


  22. snapshot_id = Column(String(36))


  23. cluster_name = Column(String(255), nullable=True)

  24. host = Column(String(255)) # , ForeignKey('hosts.id'))

  25. size = Column(Integer)

  26. availability_zone = Column(String(255)) # TODO(vish): foreign key?

  27. status = Column(String(255)) # TODO(vish): enum?

  28. attach_status = Column(String(255)) # TODO(vish): enum

  29. migration_status = Column(String(255))


  30. scheduled_at = Column(DateTime)

  31. launched_at = Column(DateTime)

  32. terminated_at = Column(DateTime)


  33. display_name = Column(String(255))

  34. display_description = Column(String(255))


  35. provider_location = Column(String(255))

  36. provider_auth = Column(String(255))

  37. provider_geometry = Column(String(255))

  38. provider_id = Column(String(255))


  39. volume_type_id = Column(String(36))

  40. source_volid = Column(String(36))

  41. encryption_key_id = Column(String(36))


  42. consistencygroup_id = Column(String(36), index=True)

  43. group_id = Column(String(36), index=True)


  44. bootable = Column(Boolean, default=False)

  45. multiattach = Column(Boolean, default=False)


  46. replication_status = Column(String(255))

  47. replication_extended_status = Column(String(255))

  48. replication_driver_data = Column(String(255))


  49. previous_status = Column(String(255))


  50. consistencygroup = relationship(

  51. ConsistencyGroup,

  52. backref="volumes",

  53. foreign_keys=consistencygroup_id,

  54. primaryjoin='Volume.consistencygroup_id == ConsistencyGroup.id')


  55. group = relationship(

  56. Group,

  57. backref="volumes",

  58. foreign_keys=group_id,

  59. primaryjoin='Volume.group_id == Group.id')


  60. service_uuid = Column(String(36), index=True)

  61. service = relationship(Service,

  62. backref="volumes",

  63. foreign_keys=service_uuid,

  64. primaryjoin='Volume.service_uuid == Service.uuid')

  65. shared_targets = Column(Boolean, default=True) # make an FK of service?

数据库属性

  1. MariaDB [cinder]> desc volumes;

  2. +-----------------------------+--------------+------+-----+---------+-------+

  3. | Field | Type | Null | Key | Default | Extra |

  4. +-----------------------------+--------------+------+-----+---------+-------+

  5. | created_at | datetime | YES | | NULL | |

  6. | updated_at | datetime | YES | | NULL | |

  7. | deleted_at | datetime | YES | | NULL | |

  8. | deleted | tinyint(1) | YES | | NULL | |

  9. | id | varchar(36) | NO | PRI | NULL | |

  10. | ec2_id | varchar(255) | YES | | NULL | |

  11. | user_id | varchar(255) | YES | | NULL | |

  12. | project_id | varchar(255) | YES | | NULL | |

  13. | host | varchar(255) | YES | | NULL | |

  14. | size | int(11) | YES | | NULL | |

  15. | availability_zone | varchar(255) | YES | | NULL | |

  16. | status | varchar(255) | YES | | NULL | |

  17. | attach_status | varchar(255) | YES | | NULL | |

  18. | scheduled_at | datetime | YES | | NULL | |

  19. | launched_at | datetime | YES | | NULL | |

  20. | terminated_at | datetime | YES | | NULL | |

  21. | display_name | varchar(255) | YES | | NULL | |

  22. | display_description | varchar(255) | YES | | NULL | |

  23. | provider_location | varchar(256) | YES | | NULL | |

  24. | provider_auth | varchar(256) | YES | | NULL | |

  25. | snapshot_id | varchar(36) | YES | | NULL | |

  26. | volume_type_id | varchar(36) | YES | | NULL | |

  27. | source_volid | varchar(36) | YES | | NULL | |

  28. | bootable | tinyint(1) | YES | | NULL | |

  29. | provider_geometry | varchar(255) | YES | | NULL | |

  30. | _name_id | varchar(36) | YES | | NULL | |

  31. | encryption_key_id | varchar(36) | YES | | NULL | |

  32. | migration_status | varchar(255) | YES | | NULL | |

  33. | replication_status | varchar(255) | YES | | NULL | |

  34. | replication_extended_status | varchar(255) | YES | | NULL | |

  35. | replication_driver_data | varchar(255) | YES | | NULL | |

  36. | consistencygroup_id | varchar(36) | YES | MUL | NULL | |

  37. | provider_id | varchar(255) | YES | | NULL | |

  38. | multiattach | tinyint(1) | YES | | NULL | |

  39. | previous_status | varchar(255) | YES | | NULL | |

  40. | cluster_name | varchar(255) | YES | | NULL | |

  41. | group_id | varchar(36) | YES | MUL | NULL | |

  42. | service_uuid | varchar(36) | YES | MUL | NULL | |

  43. | shared_targets | tinyint(1) | YES | | NULL | |

  44. +-----------------------------+--------------+------+-----+---------+-------+

  45. 39 rows in set (0.00 sec)

多后端存储(Multi-Backend Storage)

从 Havana 开始 Cinder 支持通过启用多个不同类型的存储后端(Backend),并为每一个 Backend 启动一个 cinder-volume service 服务进程。对于用户而言,只需要通过对配置文件的修改即可实现。

e.g. 同时启用 LVM 和 GlusterFS 后端存储

  1. # /etc/cinder/cinder.conf


  2. [DEFAULT]

  3. ...

  4. enabled_backends =lvmdriver,glusterfs


  5. [lvmdriver]

  6. volume_driver = cinder.volume.drivers.lvm.LVMISCSIDriver

  7. volume_group = cinder-volumes

  8. volume_backend_name =LVM_iSCSI


  9. [glusterfs]

  10. volume_driver =cinder.volume.drivers.glusterfs.GlusterfsDriver

  11. glusterfs_shares_config =/etc/cinder/glusterfs_shares

  12. glusterfs_mount_point_base =/var/lib/nova

  13. glusterfs_disk_util = df

  14. glusterfs_qcow2_volumes = true

  15. glusterfs_sparsed_volumes = true

  16. volume_backend_name = GlusterFS

  • volume_backend_name:会被 Volume Type 功能用到,通过 --propertyvolume_backend_name 属性来设置。

  • 多个 backends 之间可以设置相同 volumebackendname,这样 cinder-scheduler 可以按照指定的调度算法在相同 volumebackendname 的多个 backends 之内选择一个最合适的 backend。

  • 启用了多后端存储后,cinder-volume service 服务进程会 fock 出相应数量的子进程,可以通过指令 openstack volume service list 查看。

NOTE:开发调试阶段不建议开启多后端存储或者建议使用 remote_pdb 进行调试。

Tranfer Volume

Tranfer Volume 将 Volume 的拥有权从一个 Tenant 用户转移到另一个 Tenant 用户。该功能的本质就是 修改了 volume 的 tenant id 属性(os-vol-tenant-attr:tenant_id)而已。

  1. 在 Volume 所属的 Tenant 用户使用命令 cinder transfer-create 创建 tranfer 时候会产生 transfer id  authkey

  2. 在另一个期望接管该 Volume 的 Tenant 用户使用命令 cinder transfer-accept 接受 transfer 时将 1 中的 transfer id  auth_key 填入即可。

Migrate Volume

Migrate Volume 即将 Volume 从一个 Backend 迁移至另一个 Backend。

if 如果 Volume 没有 Attach:

  • 迁移:如果是同一个存储设备上不同 Backend 之间迁移,需要存储 Driver 支持本地 Migrate。

  • 克隆:如果是不同存储设备上的 Backend 之间的 Volume 迁移,或者存储 Driver 不支持本地 Backend 之间的迁移,那么 Cinder 会启用克隆操作进行迁移。

    • 首先创建一个新的 Volume

    • 然后从旧 Volume 的数据拷贝到新 Volume

    • 最后旧 Volume 删除

else Volume 已经被 Attach 到虚拟机:

  • 克隆:同上

NOTEmigration-policy 有两个选项 never 和 on-demand,如果设置了 never,是无法实现 Volume 迁移的。

Mutli-Attach

Mutli-Attach – Support for attaching a single Cinder volume to multile VM instances.

OpenStack’s Mutli-Attach Feature 在早前的版本的定义是:允许一个 RO volume 经 iSCSI/FC 被 Attached 到多个 VM;直到 Queens 版本之后,Mutil-Attach 的定义被更新为:「The ability to attach a volume to multiple hosts/servers simultaneously is a use case desired for active/active or active/standby scenarios.」,即 Volume 支持以 RO/RW 的方式被挂载到多个 VM。

官方文档:Volume multi-attach: Enable attaching a volume to multiple servers

Multi-Attach 最直接的价值就是支撑核心业务数据盘的高可用冗余特性,比如说:一个 Volume 同时挂载在两个 VM 上,当 ACTIVE VM 失联时,可以由另外的 PASSIVE VM 继续访问(RO、R/W)这个卷。

Multi-Attach RO:以 RO 模式将 Volume Attach 到另一个主机/服务器。问题在于,要求使用者(e.g. KVM)知道如何在 Volume 上设置只读模式和强制执行只读模式。

Multi-Attach RW:以 RW 模式将 Volume Attach 到另一个主机/服务器。这种情况下,多个 Attachment(Volume 与 Instance 的隐射关系)之间没有区别,它们都被视为独立项目,并且都是读写卷。

NOTE:目前,所有 Volume 都可以以 RW 模式 Attach,包括 Boot From Volume 模式。

代码实现原理

  • 首先通过定义一个 multiattach=True 标志的 Volume.

  • Attach 时,Nova 需要支持即使在 Volume in-use status 下仍然可以 Attach。Nova 为每一个 Attachment 指定 RO 或 RW 类型。Cinder 需要支持 Volume status 为 available 或 in-use 状态下仍然可以 Attach。multiattach 字段设置可以被 Attach 多次。Attach 之后将 Volume 和 Instance 的关系(m:n)记录到 Attachment 表中。Libvirt 需要将这个 Volume 设置为 shareable 标签,这样 Hypervisor 就不会在 Volume 上设置独占锁以及相关针对 VM 的 SELinux 隔离设置了。

  • Detach 时,Nova 需要传递 attachmentid 到 clinderclient,告知 Cinder 哪一个 Attahment 需要被 Detach。然后 Cinder 结合 instanceid 与 attachmentid 执行 Detach Volume。如果 Cinder 设置了 multiattach=True 但又没有传入 attachmentid 的话就应该 Detach 失败。

使用

  1. # 创建 Multi-Attach Volume Type

  2. cinder type-create multiattach

  3. cinder type-key multiattach set multiattach="<is> True"


  4. # Create Volume

  5. cinder create 10 --name multiattach-volume --volume-type <volume_type_uuid>


  6. # multiattach-volume

  7. nova volume-attach test01 <volume_uuid>

  8. nova volume-attach test02 <volume_uuid>

对于版本比较旧的 Cinder(< Q)可以使用下述方式进行 Multi-Attach

  1. [root@overcloud-controller-0 ~]# cinder create --allow-multiattach --name vol_shared 1

  2. +--------------------------------+--------------------------------------+

  3. | Property | Value |

  4. +--------------------------------+--------------------------------------+

  5. | attachments | [] |

  6. | availability_zone | nova |

  7. | bootable | false |

  8. | consistencygroup_id | None |

  9. | created_at | 2019-03-04T02:44:51.000000 |

  10. | description | None |

  11. | encrypted | False |

  12. | id | 240a94b6-6f8c-4797-9379-eebeda984c95 |

  13. | metadata | {} |

  14. | migration_status | None |

  15. | multiattach | True |

  16. | name | vol_shared |

  17. | os-vol-host-attr:host | None |

  18. | os-vol-mig-status-attr:migstat | None |

  19. | os-vol-mig-status-attr:name_id | None |

  20. | os-vol-tenant-attr:tenant_id | f745745cebce4a609f074b0121ae3a53 |

  21. | replication_status | disabled |

  22. | size | 1 |

  23. | snapshot_id | None |

  24. | source_volid | None |

  25. | status | creating |

  26. | updated_at | None |

  27. | user_id | 11b816e454384d038472c7c89d2544f4 |

  28. | volume_type | None |

  29. +--------------------------------+--------------------------------------+


  30. [root@overcloud-controller-0 ~]# openstack volume show vol_shared

  31. +--------------------------------+------------------------------------------------+

  32. | Field | Value |

  33. +--------------------------------+------------------------------------------------+

  34. | attachments | [] |

  35. | availability_zone | nova |

  36. | bootable | false |

  37. | consistencygroup_id | None |

  38. | created_at | 2019-03-04T02:44:51.000000 |

  39. | description | None |

  40. | encrypted | False |

  41. | id | 240a94b6-6f8c-4797-9379-eebeda984c95 |

  42. | migration_status | None |

  43. | multiattach | True |

  44. | name | vol_shared |

  45. | os-vol-host-attr:host | hostgroup@tripleo_iscsi#tripleo_iscsi_fanguiju |

  46. | os-vol-mig-status-attr:migstat | None |

  47. | os-vol-mig-status-attr:name_id | None |

  48. | os-vol-tenant-attr:tenant_id | f745745cebce4a609f074b0121ae3a53 |

  49. | properties | |

  50. | replication_status | disabled |

  51. | size | 1 |

  52. | snapshot_id | None |

  53. | source_volid | None |

  54. | status | available |

  55. | type | None |

  56. | updated_at | 2019-03-04T02:44:52.000000 |

  57. | user_id | 11b816e454384d038472c7c89d2544f4 |

  58. +--------------------------------+------------------------------------------------+



  59. [root@overcloud-controller-0 ~]# openstack server add volume VM1 vol_shared

  60. [root@overcloud-controller-0 ~]# openstack server add volume VM2 vol_shared


  61. [root@overcloud-controller-0 ~]# openstack volume show 87213d56-8bb3-494e-9477-5cea3f0fed83

  62. +--------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

  63. | Field | Value |

  64. +--------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

  65. | attachments | [{u'server_id': u'6cd7f5e2-9782-446c-b9fe-c9423341ac23', u'attachment_id': u'136ce8d6-e82e-4db6-8742-6c4d0d4fa410', u'attached_at': u'2019-03-04T07:34:58.000000', u'host_name': None, u'volume_id': |

  66. | | u'87213d56-8bb3-494e-9477-5cea3f0fed83', u'device': u'/dev/vdb', u'id': u'87213d56-8bb3-494e-9477-5cea3f0fed83'}, {u'server_id': u'e9131a29-c32e-4f8f-a1ac-7e8d09bb09d3', u'attachment_id': u'502cc9b7-cc3e-4796-858b-940ba6423788', |

  67. | | u'attached_at': u'2019-03-04T07:30:12.000000', u'host_name': None, u'volume_id': u'87213d56-8bb3-494e-9477-5cea3f0fed83', u'device': u'/dev/vdb', u'id': u'87213d56-8bb3-494e-9477-5cea3f0fed83'}, {u'server_id': u'120d49e5-8942-4fec- |

  68. | | a54c-6e2ab5ab4bf2', u'attachment_id': u'd0fd3744-6135-485f-a358-c5e333658a32', u'attached_at': u'2019-03-04T07:36:25.000000', u'host_name': None, u'volume_id': u'87213d56-8bb3-494e-9477-5cea3f0fed83', u'device': u'/dev/vdb', u'id': |

  69. | | u'87213d56-8bb3-494e-9477-5cea3f0fed83'}] |

  70. | availability_zone | nova |

  71. | bootable | false |

  72. | consistencygroup_id | None |

  73. | created_at | 2019-03-04T07:20:40.000000 |

  74. | description | None |

  75. | encrypted | False |

  76. | id | 87213d56-8bb3-494e-9477-5cea3f0fed83 |

  77. | migration_status | None |

  78. | multiattach | True |

  79. | name | vol_shared |

  80. | os-vol-host-attr:host | hostgroup@tripleo_iscsi#tripleo_iscsi_fanguiju |

  81. | os-vol-mig-status-attr:migstat | None |

  82. | os-vol-mig-status-attr:name_id | None |

  83. | os-vol-tenant-attr:tenant_id | f745745cebce4a609f074b0121ae3a53 |

  84. | properties | attached_mode='rw', readonly='False' |

  85. | replication_status | disabled |

  86. | size | 1 |

  87. | snapshot_id | None |

  88. | source_volid | None |

  89. | status | in-use |

  90. | type | None |

  91. | updated_at | 2019-03-04T07:36:26.000000 |

  92. | user_id | 11b816e454384d038472c7c89d2544f4 |

  93. +--------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

VolumeType

Volume Type 常被用来满足用户对 Volume 的一些自定义需求,例如:选择存储后端,开启 Thin/Thick Provisioning(精简置备与厚置备),Deduplication(去重)或 Compression(压缩)等高级功能。

EXAMPLE 1:假设有两个 Cinder 存储池,后端名(backendname)分别是 ssdpool 与 hdd_pool。如何实现创建 Volume 时指定后端的存储池?

  1. # 1. 分别创建两个 volume type:ssd-type 与 hdd-type

  2. cinder type create "ssd-type"

  3. cinder type create "hdd-type"


  4. # 2. 为这两个 volume type 指定后端名

  5. cinder type-key "ssd-type" set volume_backend_name=ssd_pool

  6. cinder type-key "hdd-type" set volume_backend_name=hdd_pool


  7. # 3. 指定 volume type 创建 volume

  8. cinder create --name --volume-type ssd-type

  9. cinder create --name --volume-type hdd-type

如此 Cinder 就会根据 volume type 所具有的 extra spec key(e.g. volumebackendname:ssdpool),将 Volume 创建到 backendname 所对应的 Cinder Backend 上(Backend Section 会定义 backend_name 配置项)。

EXAMPLE 2:开启 Thin Provisioning(精简置备),首先需要注意的是,Thin Provisioning 需要后端存储设备支持。如果使用 Ceph 作为存储后端,则不需要考虑开启此功能,因为 Ceph 的 rbd device 默认就是 Thin Provisioning 且无法选择。

  1. # 1. 创建一个 volume type:thin-type

  2. cinder type create "thin-type"


  3. # 2. 为 thin-type 设定 extra-spec:

  4. cinder type-key "thin-type" set provisioning:type=thin

  5. cinder type-key "thin-type" set thin_provisioning_support="True"


  6. # 3. 使用 thin-type 创建 volume

  7. cinder create --name --volume-type thin-type

EXAMPLE:如果切换 Volume 的 Thin/Thick provisioning?使用 Cinder Retype 功能,Retype 常用来改变 Volume 的 volume type,从而达到切换或开关某项功能(特性)的作用。

  1. # retype Changes the volume type for a volume.


  2. usage: cinder retype [--migration-policy <never|on-demand>]

  3. <volume> <volume-type>

Volume Type 可以用来选择后端存储,那 retype 就可以用来改变 Volume 的后端存储池,所以 retype 可以被用来实现 Volume 的迁移功能。e.g. 用户有一个属于 ssdpool 的 Volume,想把其迁移至 hddpool 里,可以用 retype 命令实现:

  1. cinder retype --migration-policy on-demand hdd-type

存储 QoS

在《[Cinder] 存储 Qos》一文中已经说到,这里不再赘述。简单演示一个示例:

  1. # 1. 创建两个 QoS 规格与 volume type:

  2. cinder qos-create qos1 write_iops_sec=500

  3. cinder qos-create qos2 write_iops_sec=50000

  4. cinder type create "qos1-type"

  5. cinder type create "qos2-type"


  6. # 2. 将 QoS 规格与 volume type 关联:

  7. cinder qos-associate


  8. # 3. 创建一个 volume,并设置其 QoS 规格为 qos1

  9. cinder create --name --volume-type qos1-type


  10. # 4. 重新设置 volume 的 QoS 规格为 qos2

  11. cinder retype qos2-type

设置或更改 Volume 的 QoS 需要注意两点:

  1. QoS 规格可以设置 consumer 为 front-end、back-end 或 both,front-end 表示 Volume 的 QoS 是在 Hypervisor(QEMU-KVM)设置并生效的,这种情况下改变 Volume 的 QoS 规格后,需要重新 Attach 才能生效。

  2. 更改 Volume 的 QoS,为什么要用 retype?这是因为如果直接修改 QoS 的设置项(e.g. writeiopssec),会直接影响到所有使用了该 QoS 的 Volumes。所以建议通过 retype 来更改 Volume 的 QoS,避免造成群体性伤害。

Replication

Replication(复制)、Snapshot(快照)、Backup(备份)是存储领域最为常见的数据保护三剑客。Cinder 从 Juno 版开始支持 Replication。

在衡量数据保护技术优劣时,通常使用 RPO(Recovery Point Objective,复原点目标,当服务恢复后得来的数据所对应的时间点)与 RTO(Recovery Time Objective,复原时间目标,企业可容许服务中断的时间长度)两个指标。优秀的数据保护技术可以使得 RPO=0 且 RTO 趋近于 0。Replication 与 Snapshot 相比,它不依赖于数据源,可以做到异地容灾;而与 Backup 相比,它轻量,可以恢复到任意时间点数据源。Replication 技术因其可靠的数据保护作用,常常被用来构建高可用数据中心。 简单来说,

Replication 通过在两个 Volume(卷)之间创建一个 Session(会话),并进行数据同步。这两个 Volume 可以处于同一台存储阵列中,也可以处于两个异地的存储阵列中。但同时只有一个 Volume 会提供生产服务,当提供生产服务的 Primary Volume 发生故障时,立即切换至备用的 Secondary Volume,从而保证业务的连续性。Replication 根据数据同步的方式分为 Sync(同步)与 Async(异步)两种模式。Sync 模式下,数据在写入 Primary Volume 与 Secondary Volume 后才可用;Async 模式反之,响应时间更短。因此,只有 Sync Replication 可以提供 RPO=0 的保障,但 Volume 性能却见不得是最高的。

Cinder Replication 操作对象:

Failover:是一个具体的 Backend,我们成为 Replication Backend(以区别于承载业务的 Cinder Backend)。当用于生产的某个 Cinder Backend 发生故障时,其中所有的 Volume 将不可访问,从而导致业务中断。这时可以考虑其进行 Cinder Replication 的 failover-host 操作以恢复被中断的业务。在进行 failover-host 操作时需要指定的 backend-id 就是 Replication 目的端(Replication Backend),当 failover-host 操作完成后,Cinder Backend 上所有的 Volume 将由目的端上的 Volume 替代,提供生产服务。当然了,前提是运管人员为 Cinder Backend 上所有的 Volume 都启用了 Replication 功能,如果 Cinder Backend 上的某些 Volume 没有开启 Replication 功能,那么这些 Volume 将不会 Failover 到目的端。

Replication 目的端:Cinder 支持一对多的 Replication,即一个 Cinder Backend 可以 Replicate 到多个目的端,但并非所有后端存储设备都支持一对多的 Relication。Replication 目的端的信息需要在 cinder.conf 里配置,其中 backend_id 是必须配置的,该参数用于进行 failover-host 操作时指定目的端。Replication 目的端一般是灾备数据中心的存储阵列,用来做主业务数据中心的冗余设备。


  1. # Multi opt of dictionaries to represent the aggregate mapping between source

  2. # and destination back ends when using whole back end replication. For every

  3. # source aggregate associated with a cinder pool (NetApp FlexVol), you would

  4. # need to specify the destination aggregate on the replication target device. A

  5. # replication target device is configured with the configuration option

  6. # replication_device. Specify this option as many times as you have replication

  7. # devices. Each entry takes the standard dict config form:

  8. # netapp_replication_aggregate_map =

  9. # backend_id:<name_of_replication_device_section>,src_aggr_name1:dest_aggr_name1,src_aggr_name2:dest_aggr_name2,...


Failback:当 Cinder Backend 源端的故障排除后,可以对该 Cinder Backend 执行 Failback 操作,即将生产业务从 Replication 目的端重新接管回阿里。这样 Cinder Backend 源端的 Volume 就可以继续提供服务了。而且在故障处理期间,向 Replication 目的端的 Volumes 写入的数据也会同步回源端。

Sync/Async:目前 Cinder 并不支持显示的设置 Replication 的同步模式,这是因为 Replication 极度依赖实际的后端存储设备功能集以及驱动程序的实现。一般而言,各存储厂商的具体实现中,会使用 volume type 中的 extra spec key 来设置 Volume 的 Replication Sync/Async 模式。

Step 1. 创建开启 Replication 的 Volume

  • 首先需要创建支持 Replication 的 volume type

  1. cinder type-create replication-type

  2. cinder type-key replication-type set replication_enabled=True

  • 使用 replication volume type 创建 volume

  1. cinder create --volume-type replication_type --name vol001 100

Step 2. 发生故障时对指定的 Cinder Backend 执行 Failover 操作

  1. cinder failover-host --backend-id storage-node2@replication2

Step 3. 待 Cinder Backend 源端故障清理完毕,执行 Failback 操作

  1. cinder failover-host --backend-id <cinder_backend> <replication_backend>

  2. # e.g.

  3. cinder failover-host --backend-id default storage-node2@replication2

Cinder 的高可用部署架构

  • 无状态服务(cinder-api、cinder-scheduler、cinder-volume)使用多活(无状态服务利于横向扩展,高并发):Active/Active(A/A)

  • 有状态服务使用主备:Active/Passive(A/P)

  • cinder-api + cinder-scheduler 都部署在 Controller,3 个 Controller 同时共享同一个 VIP 实现多活

  • 一个存储设备可以对应多个 cinder-volume,结合 cinder-volume 分布式锁实现多活,分布式锁可以避免不同的 cinder-scheduler 同时调用到同一个 cinder-volume,从而避免多活部署的 cinder-volume 同时操作后端存储设备,简而言之就是避免并发操作带来(cinder-volume 与后端存储设备之间的)数据不一致性。锁是为了通过互斥访问来保证共享资源在并发环境中的数据一致性。分布式锁主要解决的是分布式资源访问冲突的问题,保证数据的一致性(Etcd、Zookeeper)。

Cinder 的分布式锁

在了解 Cinder 的分布式锁之前,先了解 Cinder 的本地锁。显然 Volume 资源本身也是一种共享资源,也需要处理并发访问冲突的问题,比如:删除一个 Volume 时,另一个线程正在基于该 Volume 创建快照;又或者同时有两个线程都在执行 Volume 挂载操作。cinder-volume 也是使用锁机制来实现 Volume 资源的并发访问的,Volume 的删除、挂载、卸载等操作都会对 Volume 上锁。在 Newton 版本以前,cinder-volume 的锁实现是基于本地文件实现的,使用了 Linux 的 flock 工具进行锁的管理。Cinder 执行加锁操作默认会从配置指定的 lockpath 目录下创建一个命名为 cinder-volume_uuid-{action} 的空文件,并对该文件使用 flock 加锁。flock 只能作用于同一个操作系统的文件锁。即使使用共享存储,另一个操作系统也不能通过 flock 工具判断该空文件是否有锁。可见 Cinder 使用的是本地锁。

本地锁的局限,只能够保证同一个 cinder-volume 下的共享资源(e.g. Volume)的数据一致性,也就只能使用 A/P 主备的模式进行高可用,不能管理分布式 cinder-volume 下共享资源,导致了 cinder-volume 不支持多实例高可用的问题。所以为了避免 cinder-volume 服务宕机,就需要引入自动恢复机制来进行管理。比如: Pacemaker,Pacemaker 轮询判断 cinder-volume 的存活状态,一旦发现挂了,Pacemaker 会尝试重启服务。如果 Pacemaker 依然重启失败,则尝试在另一台主机启动该服务,实现故障的自动恢复。

显然,这种方式是初级而原始的。你或许会想到引入分布式锁,比如 Zookeeper、Etcd 等服务,但这需要用户自己部署和维护一套 DLM,无疑增加了运维的成本,并且也不是所有的存储后端都需要分布式锁。Cinder 社区为了满足不同用户、不同场景的需求,并没有强制用户部署固定的 DLM,而是采取了非常灵活的可插除方式,就是 Tooz 库。

引入了 Tooz 库之后,当用户不需要分布式锁时,只需要指定后端为本地文件即可,此时不需要部署任何 DLM,和引入分布式锁之前的方式保持一致,基本不需要执行大的变更。当用户需要 cinder-volume 支持 AA 时,可以选择部署一种 DLM,比如 Zookeeper 服务。Cinder 对 Tooz 又封装了一个单独的 coordination 模块,其源码位于 cinder/coordination.py,当代码需要使用同步锁时,只需要在函数名前面加上 @coordination.synchronized 装饰器即可,方便易用,并且非常统一。

Tooz 库

社区为了解决项目中的分布式问题,开发了一个非常灵活的通用框架,项目名为 Tooz,它是一个 Python 库,提供了标准的 coordination API,其主要目标是解决分布式系统的通用问题,比如节点管理、主节点选举以及分布式锁等。简而言之,Tooz 实现了非常易用的分布式锁接口。

Tooz 封装了一套锁管理的库或者框架,只需要简单调用 lock、trylock、unlock 即可完成实现,不用关心底层细节,也不用了解后端到底使用的是 Zookeeper、Redis 还是 Etcd。使用 Tooz 非常方便,只需要三步:

  1. 与后端 DLM 建立连接,获取 coordination 实例。

  2. 声明锁名称,创建锁实例。

  3. 使用锁:

  1. coordinator = coordination.get_coordinator('zake://', b'host-1’)

  2. coordinator.start()

  3. #Create a lock

  4. lock = coordinator.get_lock("foobar”)

  5. with lock:

  6. print("Do something that is distributed”)

  7. coordinator.stop()



相关阅读:

Rocky版新功能集锦之二:Cinder

OpenStack Cinder mutil-attach技术探秘

OpenStack存储篇:Cinder进阶之retype

授人以鱼不如授人以“渔”——OpenStack之Cinder Replication浅析


关于九州云99Cloud

九州云成立于2012年,是中国早期从事开放基础架构服务的专业公司。公司成立六年,秉承“开源 · 赋能变革”的理念,不断夯实自身实力,先后为政府、金融、运营商、能源、制造业、商业、交通、物流、教育、医疗等各大行业的企业级客户提供高质量的开放基础架构服务。目前拥有国家电网、南方电网广东公司、中国人民银行、中国银联、中国移动、中国电信、中国联通、中国资源卫星、eBay、国际陆港集团、中国人寿、万达信息、东风汽车、诺基亚等众多重量级客户,被用户认可为最值得信赖的合作伙伴。

3月技术周 | Cinder 架构分析、高可用部署与核心功能解析

3月技术周 | Rocky Octavia 实现与分析(四):Amphora 与 Octavia 的安全通信实现

前言

在前面的章节中我们记录了 LoadBalancer、Listener、Pool、Member 等等 Octavia 核心资源对象的创建流程,本篇我们在此之上继续讨论处于 LB Management Network 上的 Amphorae 虚拟机是如何与处于 OpenStack Management Network 上的 Octavia Worker 进行安全通信的。

为什么 Octavia 需要自建 CA 证书?

首先我们提出一个问题:为什么 Octavia 需要自建 CA 而不使用 OpenStack 的通用认证体系?

答案是:For production use the ca issuing the client certificate and the ca issuing the server certificate need to be different so a hacker can’t just use the server certificate from a compromised amphora to control all the others.

简而言之,Octavia 自建 CA 证书主要有两个必要:

  • amphora-agent 没有加入 OpenStack 鉴权体系,需要证书来保证通讯安全

  • 防止恶意用户利用 amphora 作为 “肉鸡” 攻击 OpenStack 的内部网络

基于自建 CA 实现的 SSL 通信

Octavia 提供了自动化脚本通过 OpenSSL 指令来创建 CA 中心并自签发 CA 根证书。执行下述指令即可完成:

  1. $ source /opt/rocky/octavia/bin/create_certificates.sh /etc/octavia/certs/ /opt/rocky/octavia/etc/certificates/openssl.cnf

NOTE:自签发即自己担保自己,用自己的私钥对自己的 CSR 进行签发。只有顶级认证角色才会自签发,所以也称为根证书,本质是签发服务器证书的公钥。

所谓 CA,在操作系统上的载体只是一个文件目录(Directory),包含了各类型秘钥的证书。CA 在信任系统中充当第三方信托机构的角色,提供证书签发和管理服务,可以有效解决非对称加密系统中常见的中间人攻击问题。更多关于 CA 中心为内容可以参考《使用 OpenSSL 自建 CA 并签发证书》,这里不再赘述。

3月技术周 | Rocky Octavia 实现与分析(四):Amphora 与 Octavia 的安全通信实现

Octavia 自建的 CA 中心

  1. $ ll /etc/octavia/certs/

  2. total 44

  3. -rw-r--r-- 1 stack stack 1294 Oct 26 12:51 ca_01.pem

  4. -rw-r--r-- 1 stack stack 989 Oct 26 12:51 client.csr

  5. -rw-r--r-- 1 stack stack 1708 Oct 26 12:51 client.key

  6. -rw-r--r-- 1 stack stack 4405 Oct 26 12:51 client-.pem

  7. -rw-r--r-- 1 stack stack 6113 Oct 26 12:51 client.pem

  8. -rw-r--r-- 1 stack stack 71 Oct 26 12:51 index.txt

  9. -rw-r--r-- 1 stack stack 21 Oct 26 12:51 index.txt.attr

  10. -rw-r--r-- 1 stack stack 0 Oct 26 12:51 index.txt.old

  11. drwxr-xr-x 2 stack stack 20 Oct 26 12:51 newcerts

  12. drwx------ 2 stack stack 23 Oct 26 12:51 private

  13. -rw-r--r-- 1 stack stack 3 Oct 26 12:51 serial

  14. -rw-r--r-- 1 stack stack 3 Oct 26 12:51 serial.old

  • newcerts dir:存放 CA 签署(颁发)过的数字证书

  • private dir:存放 CA 的私钥

  • serial file:存放证书序列号(e.g. 01),每新建一张证书,序列号会自动加 1

  • index.txt file:存放证书信息

  • ca_01.pem PEM file:CA 证书文件

  • client.csr file:Server CSR 证书签名请求文件

  • client.key file:Server 私钥文件

  • client-.pem:PEM 编码的 Server 证书文件

  • client.pem:结合了 client-.pem 和 client.key 的文件

列举 Octavia 与 CA 认证相关的配置项

  • 应用于 Create Amphora Flow 中的 TASK:GenerateServerPEMTask,生成 Amphora 私钥并签发 Amphora 证书。

  1. [certificates]

  2. ca_private_key_passphrase = foobar

  3. ca_private_key = /etc/octavia/certs/private/cakey.pem

  4. ca_certificate = /etc/octavia/certs/ca_01.pem

  • 应用于 Octavia Worker 的 AmphoraAPIClient,拿着 CA 根证书(是 Amphora 证书的公钥,可以解开 Amphora 证书得到 Amphora 的公钥)和 Amphora 证书向 amphora-agent 发起 SSL 通信。

  1. [haproxy_amphora]

  2. server_ca = /etc/octavia/certs/ca_01.pem

  3. client_cert = /etc/octavia/certs/client.pem

  • 应用于 Task:CertComputeCreate,指定 CA 根证书的路径

  1. [controller_worker]

  2. client_ca = /etc/octavia/certs/ca_01.pem

Amphora Agent 启动加载证书

首先看为 Amphorae 生成证书的代码实现:

  1. # /opt/rocky/octavia/octavia/controller/worker/tasks/cert_task.py


  2. class GenerateServerPEMTask(BaseCertTask):

  3. """Create the server certs for the agent comm


  4. Use the amphora_id for the CN

  5. """


  6. def execute(self, amphora_id):

  7. cert = self.cert_generator.generate_cert_key_pair(

  8. cn=amphora_id,

  9. validity=CERT_VALIDITY)


  10. return cert.certificate + cert.private_key

Octavia Certificates 功能模块提供了 local_cert_generator(default)anchor_cert_generator 两种证书生成器,通过配置项 [certificates]cert_generator 选用。

  1. # /opt/rocky/octavia/octavia/certificates/generator/local.py


  2. @classmethod

  3. def generate_cert_key_pair(cls, cn, validity, bit_length=2048,

  4. passphrase=None, **kwargs):

  5. pk = cls._generate_private_key(bit_length, passphrase)

  6. csr = cls._generate_csr(cn, pk, passphrase)

  7. cert = cls.sign_cert(csr, validity, **kwargs)

  8. cert_object = local_common.LocalCert(

  9. certificate=cert,

  10. private_key=pk,

  11. private_key_passphrase=passphrase

  12. )

  13. return cert_object

上述 LocalCertGenerator.generate_cert_key_pair Method 的语义是:

  1. 生成 Amphora 私钥

  2. 生成 Amphora 证书签名请求(CSR)

  3. 向 CA 中心申请签署 Amphora证书

属于常规的证书创建流程,与 create_certificates.sh 脚本的区别在于,Octavia Certificates 应用了 cryptography python 库而非 OpenSSL 来实现。

TASK:GenerateServerPEMTask 最终 return 了 Amphora 私钥和证书,然后实现 TASK:CertComputeCreate 将这些文件注入到 Amphora 虚拟机。登录 Amphora 即可查看这些文件,路径记录在配置文件中:

  1. # /etc/octavia/amphora-agent.conf


  2. [amphora_agent]

  3. # Octavia Worker 的证书

  4. agent_server_ca = /etc/octavia/certs/client_ca.pem

  5. # Amphora 的私钥和证书

  6. agent_server_cert = /etc/octavia/certs/server.pem

Gunicorn HTTP Server 启动时就会将证书文件加载, 加载证书的 options 如下:

  1. options = {

  2. 'bind': bind_ip_port,

  3. 'workers': 1,

  4. 'timeout': CONF.amphora_agent.agent_request_read_timeout,

  5. 'certfile': CONF.amphora_agent.agent_server_cert,

  6. 'ca_certs': CONF.amphora_agent.agent_server_ca,

  7. 'cert_reqs': True,

  8. 'preload_app': True,

  9. 'accesslog': '/var/log/amphora-agent.log',

  10. 'errorlog': '/var/log/amphora-agent.log',