0%

智能指针,从其本质上说,就是要控制对象的销毁时机。换句话讲就是何时调用对象的析构函数。从C++11开始引入三个智能指针(unique_ptr,shared_ptr,weak_ptr),准确的说是四种(还有auto_ptr),但从C++17开始auto_ptr被移除了。所以就剩下上述三种了。所有的智能指针都包含在memory头文件中。

首先,很久以前的C++是没有智能指针的,用户创建在堆上的内存,智能自己显示的释放,如果没有释放就会造成内存泄漏。这种特点导致C++的使用成本很高,为了降低成本,引入了智能指针unique_ptrshared_ptr

unique_ptr

unique_ptr采用的是传递所有权的方式来控制对象的销毁时机。如其名字所示,其对象是独享的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Simple {
private:
int m_a;
public:
void Show() {
printf("Hello Simple %d\n", m_a);
}
void showfunc() {
printf("Hello Simple func \n");
}
Simple(int n) {
m_a = n;
printf("Simple Construct\n");
}
Simple(const Simple& p) {
printf("Simple Copy Construct\n");
}
~Simple() {
printf("Simple Destroy\n");
}
};

void unique_test() {
// unique ptr
auto s_ptr = std::make_unique<Simple>(1);
s_ptr->Show();
std::unique_ptr<Simple> s_copy_ptr = std::move(s_ptr);
s_copy_ptr->Show();
//std::unique_ptr<Simple> s_ptr_2(s_ptr); // 无法编译通过
//std::unique_ptr<Simple> s_ptr_2 = s_ptr; // 无法编译通过
s_ptr->showfunc();
//s_ptr->Show(); // 当所有权变更后就不该再用之前的指针访问对象资源。
}
  • unique_ptr禁用拷贝构造
    由于unique_ptr禁用了拷贝构造函数unique_ptr(const unique_ptr&) = delete;,所以一切试图触发拷贝构造函数的操作都会引发编译错误。
  • unique_ptr所有权一旦变更就不能使用原指针访问对象资源
    上述代码中有两处使用了原指针访问对象资源,第一处s_ptr->showfunc();没有报错,可以正常打印;s_ptr->Show();中使用到了成员变量m_a所以会导致报错“this空指针”。这是由于std::move的赋值操作触发了unique_ptr中的Move构造函数(unique_ptr(unique_ptr&&) = default;),从而将s_ptr中的成员清空。所以再次访问原对象指针,就会出错。

shared_ptr

shared_ptr

shared_ptr采用的是引用计数的机制来控制对象的销毁时机。如其名字所示,其对象是共享的。当计数器等于0是,调用对象的析构函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void shared_test_inner(std::shared_ptr<Simple> *steal_ptr) {
// share ptr
auto s_ptr = std::make_shared<Simple>(1);
s_ptr->Show();
std::shared_ptr<Simple> s_copy_ptr(s_ptr);
s_copy_ptr->Show();
std::shared_ptr<Simple> s_ptr_2 = s_ptr;
s_ptr_2->Show();
// try steal
*steal_ptr = s_ptr;
}

void shared_test() {
shared_test_leak();
std::shared_ptr<Simple> s_ptr;
shared_test_inner(&s_ptr);
s_ptr->Show();
}

尽可能使用make_shared创建shared_ptr,如果使用std::shared_ptr<Simple> s_ptr(new Simple(1))创建shared_ptr,需要分配两次内存,一次是new Simple(1);另一次是shared_ptr的引用计数。make_shared只分配一次。

现在创建一个SimpleBack类,然后再让SimpleSimpleBack循环引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class SimpleBack;

class Simple {
private:
int m_a;
public:
void Show() {
printf("Hello Simple %d\n", m_a);
}
void showfunc() {
printf("Hello Simple func \n");
}
Simple(int n) {
m_a = n;
printf("Simple Construct\n");
}
Simple(const Simple& p) {
printf("Simple Copy Construct\n");
}
~Simple() {
printf("Simple Destroy\n");
}
std::shared_ptr<SimpleBack> m_sb;
};

class SimpleBack {
public:
std::shared_ptr<Simple> m_s;
~SimpleBack() {
printf("SimpleBack Destroy\n");
}
};

void shared_test_leak() {
auto s = std::make_shared<Simple>(2);
auto sb = std::make_shared<SimpleBack>();

s->m_sb = sb;
sb->m_s = s;
// 通过打印语句可以看到 s和sb的析构函数并没有调用
}

通过析构函数函数的打印语句可以看出ssb并没有被析构,这说明ssb泄漏了。

为了解决shared_ptr在循环依赖中内存泄漏的问题,推出了weak_ptr

weak_ptr

weak_ptr不会增加引用计数,不能直接操作对象的内存(需要先调用lock接口),需要和shared_ptr配套使用。

将上述代码改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class SimpleBack;

class Simple {
private:
int m_a;
public:
void Show() {
printf("Hello Simple %d\n", m_a);
}
void showfunc() {
printf("Hello Simple func \n");
}
Simple(int n) {
m_a = n;
printf("Simple Construct\n");
}
Simple(const Simple& p) {
printf("Simple Copy Construct\n");
}
~Simple() {
printf("Simple Destroy\n");
}
std::shared_ptr<SimpleBack> m_sb;
};

class SimpleBack {
public:
// 将循环引用的其中一个改成weak_ptr
std::weak_ptr<Simple> m_s;
~SimpleBack() {
printf("SimpleBack Destroy\n");
}
};

void shared_test_leak() {
auto s = std::make_shared<Simple>(2);
auto sb = std::make_shared<SimpleBack>();

s->m_sb = sb;
sb->m_s = s;
}

通过析构函数的打印语句可以看出,ssb的析构函数在shared_test_lead()调用结束后被调用。

那么,weak_ptr的使用是不是也像shared_ptr一样呢?不是的。weak_ptr需要与shared_ptr配合使用,看一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void weak_test() {
std::weak_ptr<Simple> w;
{
auto s = std::make_shared<Simple>(3);
w = s;
auto s2 = w.lock();
if(s2 != nullptr) {
s2->Show();
}
}
if(w.expired()) {
printf("object 's' is destroied. \n");
}
}
  • lock
    若对象已被析构,则返回一个空的shared_ptr;否则返回实际的shared_ptr
  • expired
    若对象已被析构,则返回true;否则返回false

参考&鸣谢

书接上文“设计模式——创建型模式”,上回说到前人创造出了很多创建型的模式,这回我们说说结构型模式。先以教科书形式介绍一下。

结构型模式(Structural Pattern)描述如何将类或者对 象结合在一起形成更大的结构,就像搭积木,可以通过 简单积木的组合形成复杂的、功能更为强大的结构。

  • 类结构型模式
    类结构型模式关心类的组合,由多个类可以组合成一个更大的系统,在类结构型模式中一般只存在继承关系和实现关系。
  • 对象结构型模式
    类与对象的组合,通过关联关系使得在一 个类中定义另一个类的实例对象,然后通过该对象调用其方法。

根据“合成复用原则”,在系统中尽量使用关联关系来替代继 承关系,因此大部分结构型模式都是对象结构型模式。

能看得懂,但是不够生动。接下来我们让它生动、活泼一下。

在元祖王朝赛博坦帝国的一个边远矿业基地,矿区提纯运输车形态的矿工钢锁被工作调动到了这个位于某个卫星上的矿厂。本来与世无争的钢锁,遇到了一起恐怖事件,由此改变了他的火种(心灵)变形形态(肉体)人生轨迹(命运)。他就是我们今天的主角变形金刚。变形金刚分为两派,一为博派(Autobots),一为狂派(Decepticons),二者都有变形的能力。

让我们抽象一下,是否可以将Transforms的变形能力抽化出来,然后再让博派和狂派分别去实现自己的变形方法。

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Transformer {
public:
virtual void transform() = 0;
};

class Autobot {
public:
void AutobotsTransform() {
printf("Autobots Transform, Please...\n");
}
};

class TransformerAdapter : public Transformer {
private:
Autobot *m_a;
public:
TransformerAdapter(Autobot *a) {
m_a = a;
}
void transform();
};

这就是“适配器模式”


适配器模式

再教科书一下。。。

adapter_1

adapter_2


刚刚收到一个需求,要为每一个变形金刚做一个自己独有的变形特效,而且在变形的过程中要加入背景音乐。所以我们需要修改一下之前的“适配器”设计模式,增加一层背景效果层,然后将背景效果传递给变形金刚,当变形金刚变形的时候将效果播放出来就好了。

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Background {
public:
virtual void Display() = 0;
};

class TransformBackground : public Background {
public:
void Display() {
printf("ki ka ka ka......\n");
}
};

class Transformer {
protected:
Background *bg;
public:
virtual void transform() = 0;
};

class Decepticon : public Transformer {
public:
Decepticon(Background* b) {
bg = b;
}
void transform() {
bg->Display();
printf("Decepticon Transform, Please...\n");
}
};

这就是“桥模式”


桥模式

再教科书一下。。。

bridge_1

** 优点: **

  • 分离抽象接口及其实现部分。
  • 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
  • 实现细节对客户透明,可以对用户隐藏实现细节。

bridge_2

** 缺点:**

  • 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程
  • 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。

又来需求了,现在我们需要变形金刚开口说话,以后还需要变形金刚会开炮。总结一下,变形是一个基本的功能,然后先增加一个说话的功能,如果有需要以后还可以增加开炮的功能。每增加一个功能不修改之前的代码,因为修改已经测试过的代码会给程序的稳定性带来隐患。

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Transformer {
protected:
Background *bg;
public:
virtual void transform() = 0;
};

class Bumblebee : public Transformer {
public:
void transform() {
printf("Bumblebee Transform, Please...\n");
}
};

class TransformerDecorator : public Transformer {
protected:
Transformer* tf;
public:
virtual void transform() {
tf->transform();
}
};

class BumblebeeSay : public TransformerDecorator {
public:
BumblebeeSay(Transformer* t) {
this->tf = t;
}
void say() {
printf("wuwuwuwu...\n");
}
void transform() {
TransformerDecorator::transform();
say();
}
};

这就是“装饰模式”


装饰模式

再教科书一下。。。

装饰模式(Decorator Pattern) :动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。

decorator_1

  • 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。
  • 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。
  • 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。
  • 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”

decorator_2

  • 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。

战争一触即发,为了取得胜利,我们需要快速的生产变形金刚,需要弄一个map,存储已经创建好的,当有战事发生时,从map里直接取出进行战斗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Transformer {
protected:
Background *bg;
public:
virtual void transform() = 0;
};

class Bumblebee : public Transformer {
public:
void transform() {
printf("Bumblebee Transform, Please...\n");
}
};

class TransformerFlyweight {
private:
std::map<std::string,Transformer*> members;
public:
Transformer* get(std::string key){
Transformer* tf = nullptr;
auto search = members.find(key);
if(search != members.end()) {
tf = search->second;
} else {
// new
tf = new Bumblebee();
members.insert(std::make_pair(key, tf));
}
return tf;
}
};

这就是“享元模式”


享元模式

再教科书一下。。。

享元模式是一个考虑系统性能的设计模式,通过使用享元模式可以节约内存空间,提高系统的性能。运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。

flyweight_1

** 优点 **

  • 享元模式的优点在于它可以极大减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份。
  • 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。

flyweight_2

** 缺点 **

  • 享元模式使得系统更加复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。

变形金刚出了个Bug,若在每次变形过程中收到攻击,变形金刚将毫无防御能力。所以需要增加一个防护罩,在变形前开启,在变形后关闭,这样增加变形金刚的变形过程中的防御能力。

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Transformer {
protected:
Background *bg;
public:
virtual void transform() = 0;
};

class Bumblebee : public Transformer {
public:
void transform() {
printf("Bumblebee Transform, Please...\n");
}
};

class TransformerProxy : public Transformer {
private:
Transformer *m_tf;
void save_on() {
printf("saving...\n");
}
void save_off() {
printf("save done\n");
}
public:
TransformerProxy(Transformer *tf) {
m_tf = tf;
}
void transform() {
save_on();
m_tf->transform();
save_off();
}
};

这就是“代理模式”


代理模式

proxy_1

** 优点 **

  • 代理模式能够协调调用者和被调用者,在一定程度上降低了系 统的耦合度。
  • 远程代理使得客户端可以访问在远程机器上的对象,远程机器 可能具有更好的计算性能与处理速度,可以快速响应并处理客户端请求。
  • 虚拟代理通过使用一个小对象来代表一个大对象,可以减少系 统资源的消耗,对系统进行优化并提高运行速度。
  • 保护代理可以控制对真实对象的使用权限。

proxy_2

** 缺点 **

  • 由于在客户端和真实主题之间增加了代理对象,因此 有些类型的代理模式可能会造成请求的处理速度变慢。
  • 实现代理模式需要额外的工作,有些代理模式的实现 非常复杂。

参考&鸣谢

设计模式是前人在工作总结出来的一些设计经验。后人使用这些经验进行设计开发,可以减少设计缺陷。常用的设计模式有23个,这23个设计模式分为三类,分别是“创建型模式”、“行为型模式”、“结构型模式”。

创建型模式

顾名思义,用来创建生成对象的设计模式。创建型模式包括:“单例模式”、“工厂模式”和“构建者模式”,其中工厂模式又分为“简单工厂模式”、“工厂方法模式”和“抽象工厂模式”。

故事是这样开始的……

在很久很久以前,有个Product他是这样定义的

1
2
3
4
class ProductA {
public:
void Show();
};

那时候还没有设计模式,人们创建对象一般都是用new在堆上分配或者直接在栈上定义。

Example:

1
2
3
4
5
int main() {
ProductA *p = new ProductA();
p->Show();
return 0;
}

后来,使用这个Product对象的人越来越多,每个人都想new一下,而且Product又不想被频繁的new,于是前人想了个办法将Product的对象定义为一个static的变量,然后每次在new之前判断一下,看是否需要new

Example:

1
2
3
4
5
6
7
8
9
10
11
class SingleInstance {
private:
static ProductA *m_p;
public:
static ProductA* CreateProduct() {
if(m_p == NULL) {
m_p = new ProductA();
}
return m_p;
}
};

这就是“单例模式”。


单例模式

  • 某个类只能有一个实例
  • 它必须自行创建这个实例
  • 它必须自行向整个系统提供这个实例

single_1

  • 单例类的构造函数为私有
  • 提供一个自身的静态私有成员变量
  • 提供一个公有的静态工厂方法

single_2


后来,前人又开发了ProductBProductAProductB同属于Product,于是我们的product变成了这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Product {
public:
virtual void Show() = 0;
};

class ProductA : public Product {
public:
void Show();
};

class ProductB : public Product {
public:
void Show();
};

为了方便创建对象,前人决定用"PA"代表ProductA"PB"代表ProductB,再创建一个工厂,用来创建对象,在创建对象时候指定PA 或者PB,工厂根据指定的内容来决定是创建ProductA还是ProductB

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
enum PD_TYPE {
PA,
PB
};

class SimpleFactory {
public:
Product* CreateProduct(PD_TYPE pt);
};

Product* SimpleFactory::CreateProduct(PD_TYPE pt){
Product *p = NULL;
switch(pt) {
case PA:
{
p = new ProductA();
break;
}
case PB:
{
p = new ProductB();
break;
}
}
}
return p;

这就是“简单工厂模式”。


简单工厂模式

  • 根据参数的不同返回不同类的实例
  • 简单工厂模式专门定义一个类来负责创建其他类的实例
  • 被创建的实例通常都具有共同的父类

simple_factory_1

simple_factory_2


随着Product种类的不断增加,我们需要不断的修改SimpleFactory::CreateProduct方法,这样会增加SimpleFactory::CreateProduct出错的风险,所以前人按照不同的产品创建了不同的工厂,这样以后在增加新的产品只需要增加对应的工厂就可以了。

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MethodFactory {
public:
virtual Product* CreateProduct() = 0;
};

class MethodFactoryPA : public MethodFactory {
public:
Product* CreateProduct();
};

class MethodFactoryPB : public MethodFactory {
public:
Product* CreateProduct();
};

Product* MethodFactoryPA::CreateProduct(){
return new ProductA();
}

Product* MethodFactoryPB::CreateProduct(){
return new ProductB();
}

这就是“工厂方法模式”


工厂方法模式

method_factory_1

工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成。

method_factory_2


从现在开始我们要将产品变多ProductA1ProductA2ProductB1ProductB2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ProductA1 : public Product {
public:
void Show();
};

class ProductA2 : public Product {
public:
void Show();
};

class ProductB1 : public Product {
public:
void Show();
};

class ProductB2 : public Product {
public:
void Show();
};

要求工厂生产ProductA1时,也要生产ProductB1;生产ProductA1时,也要生产ProductB2

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class AbstractFactory {
public:
virtual Product* CreateProductA() = 0;
virtual Product* CreateProductB() = 0;
};

class AbstractFactoryOne : public AbstractFactory {
public:
Product* CreateProductA();
Product* CreateProductB();
};

class AbstractFactoryTwo : public AbstractFactory {
public:
Product* CreateProductA();
Product* CreateProductB();
};

Product* AbstractFactoryOne::CreateProductA() {
return new ProductA1();
}

Product* AbstractFactoryOne::CreateProductB() {
return new ProductB1();
}

Product* AbstractFactoryTwo::CreateProductA() {
return new ProductA2();
}

Product* AbstractFactoryTwo::CreateProductB() {
return new ProductB2();
}

这就是“抽象工厂模式”。


抽象工厂模式

abstract_factory_1

与工厂方法很类似,区别在于工厂方法用于一个产品的构建,抽象工厂适用于多个产品构建

abstract_factory_2


抽象工厂模式可以让我们批量生产产品了,但是抽象工厂只能生产固定种类的产品,如果我们要让ProductA1ProductA2ProductB1ProductB2随意组合,生成不同的套餐,然后再进行生产。前人想了这样一个办法。。。

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Builder {
public:
virtual Product* BuildProduct() = 0;
};

class ProductABuilder : public Builder{
public:
Product* BuildProduct();
};

class Director {
private:
Builder* builder;
public:
void setBuilder(Builder* b);
Product* build();
};

Product* ProductABuilder::BuildProduct() {
return new ProductA();
}

void Director::setBuilder(Builder* b) {
this->builder = b;
}

Product* Director::build() {
return this->builder->BuildProduct();
}

这就是“建造者模式”


建造者模式

builder_1

builder_2

** To be continue …**

SPDK(Storage Performance Development Kit)是Intel发布的存储性能开发工具集

用户使用现在的固态设备,比如Intel® SSD DC P3700 Series Non-Volatile Memory Express(NVMe)驱动,面临一个主要的挑战:因为吞吐量和延迟性能比传统的磁盘好太多,现在总的处理时间中,存储软件占用了更大的比例。换句话说,存储软件栈的性能和效率在整个存储系统中越来越重要。随着存储设备继续发展,它将面临远远超过正在使用的软件体系结构的风险(即存储设备受制于相关软件的不足而不能发挥全部性能)

原理

  • 用户态运行
    避免内核上下文切换和中断将会节省大量的处理开销,允许更多的时钟周期被用来做实际的数据存储。无论存储算法(去冗,加密,压缩,空白块存储)多么复杂,浪费更少的时钟周期总是意味着更好的性能和延迟。这并不是说内核增加了不必要的开销;相反,内核增加了那些可能不适用于专用存储堆栈的通用计算用例的相关开销。SPDK的指导原则是通过消除每一处额外的软件开销来提供最少的延迟和最高的效率。
  • 轮询模式取代中断模式(Polled Mode Drivers, PMDs)
    在传统的I/O模型中,应用程序提交读写请求后睡眠,一旦I/O完成,中断就会将其唤醒。PMDs的工作方式不同,应用程序提交读写请求后继续执行其他工作,以一定的时间间隔回头检查I/O是否已经完成。这种方式避免了中断带来的延迟和开销,并使得应用程序提高了I/O的效率。
    在机械盘时代,中断开销只占整个I/O时间的一个很小的百分比,因此给系统带来了巨大的效率提升。然而,在固态设备的时代,持续引入更低延迟的持久化设备,中断开销成为了整个I/O时间中不能被忽视的部分。这个问题在更低延迟的设备上只会越来越严重。系统已经能够每秒处理数百万个I/O,所以消除数百万个事务的这种开销,能够快速地复制到多个内核中。数据包和数据块被立即分发,等待时间减小到最少,使得延迟更低,一致性延迟更多(抖动更少),吞吐量也得到提高。
  • 无锁机制
    在IO路径上避免采用任何锁机制进行同步,降低时延并提升吞吐量

架构

introduction-to-the-storage-performance-development-kit-spdk-fig2

Hardware Drivers

NVMe Driver

SPDK的基础组件,这个高优化无锁的驱动提供了高扩展性,高效性和高性能。

Inter QuickData Technology

Intel I/O Acceleration Technology(Inter IOAT,英特尔I/O加速技术),这是一种基于Xeon处理器平台上的copy offload引擎。通过提供用户空间访问,减少了DMA数据移动的阈值,允许对小尺寸I/O或NTB的更好利用。

Back-end Block Devices

NVMe-oF Initiator

本地SPDK NVMe驱动和NVMe-oF启动器共享一套共同的API命令。这意味着,比如本地/远程复制非常容易实现。

Ceph RADOS Block Device

Ceph RBD 成为SPDK的后端设备

Blobstore Block Device

由SPDK Blobstore分配的块设备,是虚拟机或数据库可以与之交互的虚拟设备。这些设备得到SPDK基础架构的优势,意味着零拷贝和令人难以置信的可扩展性。

Linux AIO

允许SPDK与内核设备(比如机械硬盘)交互。

Storage Services

Block Device Abstration Layer

这种通用的块设备抽象是连接到各种不同设备驱动和块设备的存储协议的粘合剂。还在块层中提供灵活的API用于额外的用户功能(磁盘阵列,压缩,去冗等等)。

Blobstore

为SPDK实现一个高精简的文件式语义(非POSIX)。这可以为数据库,容器,虚拟机或其他不依赖于大部分POSIX文件系统功能集(比如用户访问控制)的工作负载提供高性能基础。

Storage Protocols

iSCSI Target

建立了通过以太网的块流量规范,大约是内核LIO效率的两倍。现在的版本默认使用内核TCP/IP协议栈。

vhost-scsi target

KVM/QEMU的功能利用了SPDK NVMe驱动,使得访客虚拟机访问存储设备时延迟更低,使得I/O密集型工作负载的整体CPU负载减低。

NVMe-oF Target

实现了NVMe-oF规范。虽然这取决于RDMA硬件,NVMe-oF的目标可以为每个CPU核提供高达40Gbps的流量。

应用方案

spdk_component

网络前端

网络前端子组件包括DPDK网卡驱动和用户态网络服务UNS(这是一个Linux内核TCP/IP协议栈的替代品,能够突破通用TCP/IP协议栈的种种性能限制瓶颈)。DPDK在网卡侧提供了一个高性能的发包收包处理框架,在数据从网卡到操作系统用户态之间提供了一条快速通道。

处理框架

拿到了数据包内容,将iSCSI命令转换为SCSI块级命令。然而,在它将这些命令发到“后端”驱动之前,SPDK提供了一套API框架,让厂商能够插入自己定义的处理逻辑(架构图中绿色的方框)。通过这种机制,存储厂商可在这里实现例如缓存、去重、压缩、加密、RAID计算,或擦除码(Erasure Coding)计算等功能,使这些功能包含在SPDK的处理流程中。

后端

SPDK和物理块设备交互(读和写操作)。如前所述,SPDK提供了用户态的PMD,支持NVMe设备、Linux AIO设备(传统机械硬盘)、RAMDISK设备,以及利用到英特尔I/O加速技术的新设备(CBDMA)。这一系列后端设备驱动涵盖了不同性能的存储分层,保证SPDK几乎与每种存储应用形成关联。

使用及编译

编译安装

SPDK使用了DPDK中的一些功能,编译SPDK需要依赖DPDK,所以需要先编译安装DPDK

编译DPDK

1
2
3
# make config T=x86_64-native-linuxapp-gcc
# make
# make install

默认DPDK会安装到/usr/local目录下

编译SPDK

1
2
# ./configure --with-dpdk=/usr/local
# make

使用

nvme设备插入主机会被系统自动识别。若想使用spdk访问该设备必须现将nvme unbind。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 7.3T 0 disk
└─sda1 8:1 0 7.3T 0 part /
sdb 8:16 0 7.3T 0 disk
sdc 8:32 0 7.3T 0 disk
sdd 8:48 0 3.8G 0 disk
└─sdd1 8:49 0 3.8G 0 part /boot
nvme0n1 259:0 0 1.5T 0 disk

# cd <spdk_dir>/scripts
# sh setup.sh

# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 7.3T 0 disk
└─sda1 8:1 0 7.3T 0 part /
sdb 8:16 0 7.3T 0 disk
sdc 8:32 0 7.3T 0 disk
sdd 8:48 0 3.8G 0 disk
└─sdd1 8:49 0 3.8G 0 part /boot

参考&鸣谢

说到Ceph的通讯一定绕不开Messenger,无论是客户端到OSD,还是OSD到MON,或者OSD到OSD,都需要Messenger来协助完成各个模块间消息的发送、接收。Messenger有三种实现,分别是SimpleMessenger、AsyncMessenger、XioMessenger,本文以AsyncMessenger为例简单介绍一下其工作原理。

原理

asyncmessenger

  • processors线程数量由cct->_conf->ms_async_op_threads决定
  • NetworkStackworkersprocessors一一对应。
  • processors收到请求会调创建AsyncConnection,并存入调用AsyncConnection实例的accept方法,accept通过EventCenter将由NetworkStackworkers调用AsyncConnection实例的process方法。(哈哈哈,绕吧,有点儿晕了吧~~~)
  • processors处理完accept请求后,将AsyncConnection实例存入accepting_conns,等待NetworkStack处理完成。
  • AsyncConnectionprocessNetworkStackworkers线程调用,并构建Message消息通过dispatch_queuems_fast_dispatch发送到fast_dispatchers

Message 格式

CEPH_MSGR_TAG_MSG

message_format

  • tag
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #define CEPH_MSGR_TAG_READY         1  /* server->client: ready for messages */
    #define CEPH_MSGR_TAG_RESETSESSION 2 /* server->client: reset, try again */
    #define CEPH_MSGR_TAG_WAIT 3 /* server->client: wait for racing incoming connection */
    #define CEPH_MSGR_TAG_RETRY_SESSION 4 /* server->client + cseq: try again with higher cseq */
    #define CEPH_MSGR_TAG_RETRY_GLOBAL 5 /* server->client + gseq: try again with higher gseq */
    #define CEPH_MSGR_TAG_CLOSE 6 /* closing pipe */
    #define CEPH_MSGR_TAG_MSG 7 /* message */
    #define CEPH_MSGR_TAG_ACK 8 /* message ack */
    #define CEPH_MSGR_TAG_KEEPALIVE 9 /* just a keepalive byte! */
    #define CEPH_MSGR_TAG_BADPROTOVER 10 /* bad protocol version */
    #define CEPH_MSGR_TAG_BADAUTHORIZER 11 /* bad authorizer */
    #define CEPH_MSGR_TAG_FEATURES 12 /* insufficient features */
    #define CEPH_MSGR_TAG_SEQ 13 /* 64-bit int follows with seen seq number */
    #define CEPH_MSGR_TAG_KEEPALIVE2 14
    #define CEPH_MSGR_TAG_KEEPALIVE2_ACK 15 /* keepalive reply */
    #define CEPH_MSGR_TAG_CHALLENGE_AUTHORIZER 16 /* ceph v2 doing server challenge */
    ** 我觉得注释处的说明写的很清楚了,此处不做过多说明了。 **
  • header
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    struct ceph_msg_header {
    __le64 seq; /* message seq# for this session */
    __le64 tid; /* transaction id */
    __le16 type; /* message type */
    __le16 priority; /* priority. higher value == higher priority */
    __le16 version; /* version of message encoding */

    __le32 front_len; /* bytes in main payload */
    __le32 middle_len;/* bytes in middle payload */
    __le32 data_len; /* bytes of data payload */
    __le16 data_off; /* sender: include full offset;
    receiver: mask against ~PAGE_MASK */

    struct ceph_entity_name src;

    /* oldest code we think can decode this. unknown if zero. */
    __le16 compat_version;
    __le16 reserved;
    __le32 crc; /* header crc32c */
    } __attribute__ ((packed));
  • payload
    *** 未知 ***
  • middle
    *** 未知 ***
  • data
    具体传递的数据内容,数据大小由header中的data_len决定。
  • footer/old_footer
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /*
    * follows data payload
    * ceph_msg_footer_old does not support digital signatures on messages PLR
    */
    struct ceph_msg_footer_old {
    __le32 front_crc, middle_crc, data_crc;
    __u8 flags;
    } __attribute__ ((packed));

    struct ceph_msg_footer {
    __le32 front_crc, middle_crc, data_crc;
    // sig holds the 64 bits of the digital signature for the message PLR
    __le64 sig;
    __u8 flags;
    } __attribute__ ((packed));
    footerold_footer之差一个签名sig

分布式系统在极大提高可用性、容错性的同时,带来了一致性问题(CAP理论)。Raft算法能够解决分布式系统环境下的一致性问题。一致性是分布式系统容错的基本问题。一致性涉及多个服务器状态(Values)达成一致。 一旦他们就状态做出决定,该决定就是最终决定。 当大多数服务器可用时,典型的一致性算法会取得进展。

raft是工程上使用较为广泛的强一致性、去中心化、高可用的分布式协议。在这里强调了是在工程上,因为在学术理论界,最耀眼的还是大名鼎鼎的Paxos。但Paxos是:少数真正理解的人觉得简单,尚未理解的人觉得很难,大多数人都是一知半解。本人也花了很多时间、看了很多材料也没有真正理解。直到看到raft的论文,两位研究者也提到,他们也花了很长的时间来理解Paxos,他们也觉得很难理解,于是研究出了raft算法。

Leader选举

raft协议中,一个节点任一时刻处于leader, follower, candidate三个角色之一。

  • leader 接受客户端请求,并向Follower同步请求日志,当日志同步到大多数节点上后告诉Follower提交日志。
  • follower 接受并持久化Leader同步的日志,在Leader告之日志可以提交之后,提交日志。
  • candidate Leader选举过程中的临时角色。

election_state

每个节点以follower角色开始,如果follower超时没有收到leader的消息,它会进入candidate角色,并发起选举投票。如果candidate收到的票数超过半数以上,则切换为leader角色。如果发现其他节点比自己更新,则主动切换到follower。总之,系统中最多只有一个leader,如果在一段时间里发现没有leader,则大家通过选举-投票选出leaderleader会不停的给follower发心跳消息,表明自己的存活状态。如果leader故障,那么follower会转换成candidate,重新选出leader

election_term

leader是大家投票选举出来的,每个leader工作一段时间,然后选出新的leader继续负责。这根民主社会的选举很像,每一届新的履职期称之为一届任期,在raft协议中,也是这样的,对应的术语叫term。term(任期)以选举(election)开始,然后就是一段或长或短的稳定工作期(normal Operation)。从上图可以看到,任期是递增的,这就充当了逻辑时钟的作用;另外,term 3展示了一种情况,就是说没有选举出leader就结束了,然后会发起新的选举。

选举过程

正常情况下选举

5个节点一开始的状态都是Follower
election_flow_normal_1

在一个节点倒计时结束(Timeout) 后,这个节点的状态变成Candidate开始选举,它给其他几个节点发送选举请求(RequestVote)
election_flow_normal_2

其他四个节点都返回成功,这个节点的状态由Candidate变成了Leader,并在每个一小段时间后,就给所有的Follower发送一个 Heartbeat 以保持所有节点的状态,Follower收到Leader的Heartbeat后重设Timeout。
election_flow_normal_3

只要有超过一半的节点投支持票了,Candidate才会被选举为Leader,5个节点的情况下,3个节点 (包括Candidate本身) 投了支持就行。

Leader 出故障情况下的选举

election_flow_error_leader_1

leader出故障挂掉了,其他四个follower将进行重新选主。
election_flow_error_leader_2

4个节点的选主过程和5个节点的类似,在选出一个新的leader后,原来的Leader恢复了又重新加入了,这个时候怎么处理?在Raft里,第几轮选举是有记录的,重新加入的Leader是第一轮选举(Term 1)选出来的,而现在的Leader则是Term 2,所有原来的Leader会自觉降级为Follower
election_flow_error_leader_3
election_flow_error_leader_4
election_flow_error_leader_5
election_flow_error_leader_6

多个Candidate情况下的Leader选举

election_flow_mult_cand_1

有两个Follower同时Timeout,都变成了Candidate开始选举,分别给一个Follower发送了投票请求。
election_flow_mult_cand_2

两个Follower分别返回了ok,这时两个Candidate都只有2票,要3票才能被选成Leader
election_flow_mult_cand_3

两个Candidate会分别给另外一个还没有给自己投票的Follower发送投票请求。
election_flow_mult_cand_4

但是因为Follower在这一轮选举中,都已经投完票了,所以都拒绝了他们的请求。所以在Term 2没有Leader被选出来。
election_flow_mult_cand_5

这时,两个节点的状态是Candidate,两个是Follower,但是他们的倒计时器仍然在运行,最先Timeout的那个节点会进行发起新一轮Term 3的投票。
election_flow_mult_cand_6

两个Follower在Term 3还没投过票,所以返回OK,这时Candidate一共有三票,被选为了Leader
election_flow_mult_cand_7

如果Leader Heartbeat的时间晚于另外一个Candidate timeout的时间,另外一个Candidate仍然会发送选举请求。
election_flow_mult_cand_8

两个Follower已经投完票了,拒绝了这个Candidate的投票请求。
election_flow_mult_cand_9

Leader进行Heartbeat,Candidate收到后状态自动转为Follower,完成选举。
election_flow_mult_cand_10

日志复制

Raft 在实际应用场景中的一致性更多的是体现在不同节点之间的数据一致性,客户端发送请求到任何一个节点都能收到一致的返回,当一个节点出故障后,其他节点仍然能以已有的数据正常进行。在选主之后的复制日志就是为了达到这个目的。

log_replication_normal

正常情况下日志复制过程

log_replication_normal_step_1

客户端发送请求给Leader,储存数据 “sally”,Leader先将数据写在本地日志,这时候数据还是Uncommitted (还没最终确认,红色表示)
log_replication_normal_step_2

Leader给两个Follower发送AppendEntries请求,数据在Follower上没有冲突,则将数据暂时写在本地日志,Follower的数据也还是Uncommitted。
log_replication_normal_step_3

Follower将数据写到本地后,返回OK。Leader收到后成功返回,只要收到的成功的返回数量超过半数(包含Leader),Leader将数据 “sally” 的状态改成Committed。( 这个时候Leader就可以返回给客户端了)
log_replication_normal_step_4

Leader再次给Follower发送AppendEntries请求,收到请求后,Follower将本地日志里Uncommitted数据改成Committed。这样就完成了一整个复制日志的过程,三个节点的数据是一致的
log_replication_normal_step_5

Network Partition情况下日志复制过程

在Network Partition的情况下,部分节点之间没办法互相通信,Raft 也能保证在这种情况下数据的一致性。

log_replication_np_1

Network Partition将节点分成两边,一边有两个节点,一边三个节点。
log_replication_np_2

两个节点这边已经有Leader了,来自客户端的数据“bob”通过Leader同步到Follower
log_replication_np_3

因为只有两个节点,少于3个节点,所以“bob”的状态仍是Uncommitted。所以在这里,服务器会返回错误给客户端
log_replication_np_4

另外一个Partition有三个节点,进行重新选主。客户端数据“tom”发到新的Leader,通过和上节网络状态下相似的过程,同步到另外两个Follower
log_replication_np_5

因为这个Partition有3个节点,超过半数,所以数据“tom”都Commit了。
log_replication_np_6
log_replication_np_7
log_replication_np_8

网络状态恢复,5个节点再次处于同一个网络状态下。但是这里出现了数据冲突“bob”和“tom”
log_replication_np_9

三个节点的Leader广播AppendEntries
log_replication_np_10

两个节点Partition的Leader自动降级为Follower,因为这个Partition的数据 “bob” 没有Commit,返回给客户端的是错误,客户端知道请求没有成功,所以Follower在收到AppendEntries请求时,可以把“bob“删除,然后同步”tom”,通过这么一个过程,就完成了在Network Partition情况下的复制日志,保证了数据的一致性。
log_replication_np_11
log_replication_np_12

安全性

Election safety

选举安全性,即任一任期内最多一个leader被选出。这一点非常重要,在一个复制集中任何时刻只能有一个leader。系统中同时有多余一个leader,被称之为脑裂(brain split),这是非常严重的问题,会导致数据的覆盖丢失。

  • 一个节点某一任期内最多只能投一票
  • 只有获得多数票的节点才会成为leader

log matching

如果两个节点上的某个log entry的log index相同且term相同,那么在该index之前的所有log entry应该都是相同的。在没有异常的情况下,log matching是很容易满足的,但如果出现了node crash,情况就会变得复杂了。

safety_log_matching
*** 上图的a-f不是6个follower,而是某个follower可能存在的六个状态 ***

  • leader日志少: a,b
  • leader日志多:c,d
  • 某些位置比leader多,某些日志比leader少:e,f

当出现了leaderfollower不一致的情况,leader强制follower复制自己的log。

leader completeness

  • 一个日志被复制到多数节点才算committed
  • 一个节点得到多数的投票才能成为leader,而节点A给节点B投票的其中一个前提是,B的日志不能比A的日志旧

State Machine Safety

如果节点将某一位置的log entry应用到了状态机,那么其他节点在同一位置不能应用不同的日志。简单点来说,所有节点在同一位置(index in log entries)应该应用同样的日志。

safety_state_machine

  • (a)时刻, s1是leader,在term2提交的日志只赋值到了s1 s2两个节点就crash了。
  • (b)时刻, s5成为了term 3的leader,日志只赋值到了s5,然后crash。
  • (c)时刻,s1又成为了term 4的leader,开始赋值日志,于是把term2的日志复制到了s3,此刻,可以看出term2对应的日志已经被复制到了多数节点上,因此是committed,可以被状态机应用。
  • (d)时刻,s1又crash了,s5重新当选,然后将term3的日志复制到所有节点,这就出现了一种奇怪的现象:被复制到大多数节点(或者说可能已经应用)的日志被回滚。

因为term4时的leader s1在(c)时刻提交了之前term2任期的日志。某个leader选举成功之后,不会直接提交前任leader时期的日志,而是通过提交当前任期的日志的时候“顺手”把之前的日志也提交了,具体怎么实现了,在log matching部分有详细介绍。那么问题来了,如果leader被选举后没有收到客户端的请求呢,论文中有提到,在任期开始的时候发立即尝试复制、提交一条空的log。

因此,在上图中,不会出现(C)时刻的情况,即term4任期的leader s1不会复制term2的日志到s3。而是如同(e)描述的情况,通过复制-提交term4的日志顺便提交term2的日志。如果term4的日志提交成功,那么term2的日志也一定提交成功,此时即使s1 crash,s5也不会重新当选。

参考&鸣谢

在数据中心领域,远程直接内存访问(英语:remote direct memory access,RDMA)是一种绕过远程主机操作系统内核访问其内存中数据的技术,由于不经过操作系统,不仅节省了大量CPU资源,同样也提高了系统吞吐量、降低了系统的网络通信延迟,尤其适合在大规模并行计算机集群中有广泛应用。在基于NVMe over Fabric的数据中心中,RDMA可以配合高性能的NVMe SSD构建高性能、低延迟的存储网络。

Red Hat和甲骨文公司等软件供应商已经在其最新产品中支持这些API,截至2013年,工程师也已开始开发基于以太网的RDMA网络适配器。Red Hat Enterprise Linux和Red Hat Enterprise MRG已支持RDMA。微软已在Windows Server 2012中通过SMB Direct支持RDMA。

RDMA 原理

传统的基于Socket套接字(TCP/IP协议栈)的网络通信需要经过操作系统协议栈。数据在系统中搬来搬去,因此占用了大量的CPU和内存资源,也加大了网络延时。RDMA解决了传统Socket通信的痛点,采用了Kernel Bypass的工作方式,减少了CPU和内存的占用,也降低了网络延时。

rdma_theory

目前RDMA有三种不同的硬件实现,分别是InfiniBand、iWARP(internet wide area RDMA Protocol)、RoCE(RDMA over Coverged Ethernet)。

rdma_theory_2

  • Infiniband
    支持RDMA的新一代网络协议。 由于这是一种新的网络技术,因此需要支持该技术的NIC和交换机。
  • RoCE
    一个允许在以太网上执行RDMA的网络协议。 其较低的网络标头是以太网标头,其较高的网络标头(包括数据)是InfiniBand标头。 这支持在标准以太网基础设施(交换机)上使用RDMA。 只有网卡应该是特殊的,支持RoCE。
    RoCE v1是一种链路层协议,允许在同一个广播域下的任意两台主机直接访问。
    RoCE v2是一种Internet层协议,即可以实现路由功能。
  • iWARP
    一个允许在TCP上执行RDMA的网络协议。 IB和RoCE中存在的功能在iWARP中不受支持。 这支持在标准以太网基础设施(交换机)上使用RDMA。

关键概念

** QP(Queue Pair) **

每对QP由Send Queue(SQ)和Receive Queue(RQ)构成,这些队列中管理着各种类型的消息。QP会被映射到应用的虚拟地址空间,使得应用直接通过它访问RNIC网卡。

** CQ(Complete Queue) **

  1. 完成队列包含了发送到工作队列(WQ)中已完成的工作请求(WR)。每次完成表示一个特定的 WR执行完毕(包括成功完成的WR和不成功完成的WR)。完成队列是一个用来告知应用程序已结束的工作请求的信息(状态、操作码、大小、来源)的机制。
  2. CQ有n个完成队列实体(CQE)。CQE的数量在CQ创建的时候被指定。
  3. 当一个CQP被轮询到,它就从CQ中被删除。
  4. CQ是一个CQE的先进选出(FIFO)队列。
  5. CQ能服务于发送队列、接收队列或者同时服务于这两种队列。多个不同QP中的工作请求(WQ)可联系到同一个CQ上。

** MR(Memory Region) **

  1. 内存注册机制允许应用程序申请一些连续的虚拟内存空间或者连续的物理内存空间,将这些内存空间提供给网络适配器作为虚拟的连续缓冲区,缓冲区使用虚拟地址。
  2. 内存注册进程锁定了内存页。(为了防止页被替换出去,同时保持物理和虚拟内存的映射)在注册期间,操作系统检查被注册块的许可。注册进程将虚拟地址与物理地址的映射表写入网络适配器。在注册内存时,对应内存区域的权限会被设定。权限包括本地写、远程读、远程写、原子操作、绑定。
  3. 每个内存注册(MR)有一个远程的和一个本地的key(r_key,l_key)。本地key被本地的HCA 用来访问本地内存,例如在接收数据操作的期间。远程key提供给远程HCA用来在RDMA操作期间允许远程进程访问本地的系统内存。同一内存缓冲区可以被多次注册(甚至设置不同的操作权限),并且每次注册都会生成不同的key。

** HCA **

  1. Opening an HCA 打开HCA,准备好HCA供消费者使用。一旦打开了一个HCA设备,只有关闭它以后,才能再次打开。
  2. HCA属性 HCA属性是设备特征,这些属性必须可以被消费者获取。
  3. 修改HCA属性 HCA允许修改一组==受限制的==HCA属性。这些可以修改的属性主要是性能信息和错误计数器管理性息。其他大部分属性或是不可修改的,或是通过General Services Interface / Fabric Management Interface进行操作。
  4. 关闭HCA 将HCA恢复到初始条件下,同时注销打开HCA时分配的资源。

** 寻址 **

  1. 源端地址 CI(Channel Interface)需要存储每个HCA有效的LID和GID。
  2. 目的地址 对于RC服务类型来说,目的地址被保存在本地QP的属性中。
  3. Loopback 由于自寻址的需要,HCA需要支持Loopback。Loopback仅支持于一个HCA中,同一个端口下的QP之间进行。

** Protection Domain **

  1. PD通过在QP/SRQ与MR之间建立联系,获得HCA访问主存的权限。此外,PD还可以用来关联QP和未绑定的内存窗口,用来控制HCA访问主系统内存。
  2. 分配保护域 当创建QP,注册MR,分配MW,创建Address Handle时需要分配PD。
  3. 释放保护域 如果PD仍然与任何队列对、内存区域、内存窗口、SRQ或地址句柄相关联,则不应释放它。如果尝试这样做,则谓词将立即返回一个错误。

RDMA 工作流程

  1. 当一个应用执行RDMA读或写请求时,不执行任何数据复制。在不需要任何内核内存参与的条件下,RDMA请求从运行在用户空间中的应用中发送到本地NIC(网卡)。
  2. NIC读取缓冲的内容,并通过网络传送到远程NIC。
  3. 在网络上传输的RDMA信息包含目标虚拟地址、内存钥匙和数据本身。请求既可以完全在用户空间中处理(通过轮询用户级完成排列) ,又或者在应用一直睡眠到请求完成时的情况下通过系统中断处理。RDMA操作使应用可以从一个远程应用的内存中读数据或向这个内存写数据。
  4. 目标NIC确认内存钥匙,直接将数据写人应用缓存中。用于操作的远程虚拟内存地址包含在RDMA信息中。

RDMA API

RDMA API (Verbs)主要有两种操作方式,One-Sided RDMA。包括RDMA Reads, RDMA Writes, RDMA Atomic。这种模式下的RDMA访问完全不需要远端机的任何确认;Two-Sided RDMA。包括RDMA Send, RDMA Receive。这种模式下的RDMA访问需要远端机CPU的参与。

Two-Side RDMA

  1. 首先,A和B都要创建并初始化好各自的QP,CQ
  2. A和B分别向自己的WQ中注册WQE,对于A,WQ=SQ,WQE描述指向一个等到被发送的数据;对于B,WQ=RQ,WQE描述指向一块用于存储数据的Buffer。
  3. A的RNIC异步调度轮到A的WQE,解析到这是一个SEND消息,从Buffer中直接向B发出数据。数据流到达B的RNIC后,B的WQE被消耗,并把数据直接存储到WQE指向的存储位置。
  4. AB通信完成后,A的CQ中会产生一个完成消息CQE表示发送完成。与此同时,B的CQ中也会产生一个完成消息表示接收完成。每个WQ中WQE的处理完成都会产生一个CQE。

** 双边操作与传统网络的底层Buffer Pool类似,收发双方的参与过程并无差别,区别在零拷贝、Kernel Bypass,实际上对于RDMA,这是一种复杂的消息传输模式,多用于传输短的控制消息。 **

RDMA通信流程

  1. 获取RDMA设备列表(ibv_get_device_list)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* 1 获取设备列表 */
    int num_devices;
    struct ibv_device **dev_list = ibv_get_device_list(&num_devices);
    if (!dev_list || !num_devices)
    {
    fprintf(stderr, "failed to get IB devices\n");
    rc = 1;
    goto main_exit;
    }
  2. 打开一个RDMA设备,获取一个上下文(ibv_open_device ibv_context)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* 2 打开设备,获取设备上下文 */
    struct ibv_device *ib_dev = dev_list[0];
    res.ib_ctx = ibv_open_device(ib_dev);
    if (!res.ib_ctx)
    {
    fprintf(stderr, "failed to open device \n");
    rc = 1;
    goto main_exit;
    }
  3. 释放RDMA设备列表占用的资源(ibv_free_device_list)
    1
    2
    3
    4
    /* 3 释放设备列表占用的资源 */
    ibv_free_device_list(dev_list);
    dev_list = NULL;
    ib_dev = NULL;
  4. 查询RDMA设备端口信息(ibv_query_port ibv_port_attr)
    1
    2
    3
    4
    5
    6
    7
    /* 4 查询设备端口状态 */
    if (ibv_query_port(res.ib_ctx, 1, &res.port_attr))
    {
    fprintf(stderr, "ibv_query_port on port failed\n");
    rc = 1;
    goto main_exit;
    }
  5. 分配一个Protection Domain (ibv_alloc_pd ibv_pd)
    1
    2
    3
    4
    5
    6
    7
    8
    /* 5 创建PD(Protection Domain) */
    res.pd = ibv_alloc_pd(res.ib_ctx);
    if (!res.pd)
    {
    fprintf(stderr, "ibv_alloc_pd failed\n");
    rc = 1;
    goto main_exit;
    }
  6. 创建一个Complete Queue (ibv_create_cq ibv_cq)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* 6 创建CQ(Complete Queue) */
    int cq_size = 10;
    res.cq = ibv_create_cq(res.ib_ctx, cq_size, NULL, NULL, 0);
    if (!res.cq)
    {
    fprintf(stderr, "failed to create CQ with %u entries\n", cq_size);
    rc = 1;
    goto main_exit;
    }
  7. 注册一块Memory Region (ibv_reg_mr ibv_mr)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /* 7 注册MR(Memory Region) */
    int size = MSG_SIZE;
    res.buf = (char *)malloc(size);
    if (!res.buf)
    {
    fprintf(stderr, "failed to malloc %Zu bytes to memory buffer\n", size);
    rc = 1;
    goto main_exit;
    }
    memset(res.buf, 0, size);

    int mr_flags = IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_READ | IBV_ACCESS_REMOTE_WRITE;
    res.mr = ibv_reg_mr(res.pd, res.buf, size, mr_flags);
    if (!res.mr)
    {
    fprintf(stderr, "ibv_reg_mr failed with mr_flags=0x%x\n", mr_flags);
    rc = 1;
    goto main_exit;
    }
    fprintf(stdout, "MR was registered with addr=%p, lkey=0x%x, rkey=0x%x, flags=0x%x\n",
    res.buf, res.mr->lkey, res.mr->rkey, mr_flags);
  8. 创建一个Queue Pair (ibv_create_qp ibv_qp)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /* 8 创建QP(Queue Pair) */
    struct ibv_qp_init_attr qp_init_attr;
    memset(&qp_init_attr, 0, sizeof(qp_init_attr));
    qp_init_attr.qp_type = IBV_QPT_RC;
    qp_init_attr.sq_sig_all = 1;
    qp_init_attr.send_cq = res.cq;
    qp_init_attr.recv_cq = res.cq;
    qp_init_attr.cap.max_send_wr = 1;
    qp_init_attr.cap.max_recv_wr = 1;
    qp_init_attr.cap.max_send_sge = 1;
    qp_init_attr.cap.max_recv_sge = 1;
    res.qp = ibv_create_qp(res.pd, &qp_init_attr);
    if (!res.qp)
    {
    fprintf(stderr, "failed to create QP\n");
    rc = 1;
    goto main_exit;
    }
    fprintf(stdout, "QP was created, QP number=0x%x\n", res.qp->qp_num);
  9. 交换控制信息 (使用Socket)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    /* 9 交换控制信息 */
    struct cm_con_data_t local_con_data; // 发送给远程主机的信息
    struct cm_con_data_t remote_con_data; // 接收远程主机发送过来的信息
    struct cm_con_data_t tmp_con_data;

    local_con_data.addr = htonll((uintptr_t)res.buf);
    local_con_data.rkey = htonl(res.mr->rkey);
    local_con_data.qp_num = htonl(res.qp->qp_num);
    local_con_data.lid = htons(res.port_attr.lid);
    if (sock_sync_data(server_ip, sizeof(struct cm_con_data_t), (char *)&local_con_data, (char *)&tmp_con_data) < 0)
    {
    fprintf(stderr, "failed to exchange connection data between sides\n");
    rc = 1;
    goto main_exit;
    }
    remote_con_data.addr = ntohll(tmp_con_data.addr);
    remote_con_data.rkey = ntohl(tmp_con_data.rkey);
    remote_con_data.qp_num = ntohl(tmp_con_data.qp_num);
    remote_con_data.lid = ntohs(tmp_con_data.lid);
    /* save the remote side attributes, we will need it for the post SR */
    res.remote_props = remote_con_data;
    fprintf(stdout, "Remote address = 0x%" PRIx64 "\n", remote_con_data.addr);
    fprintf(stdout, "Remote rkey = 0x%x\n", remote_con_data.rkey);
    fprintf(stdout, "Remote QP number = 0x%x\n", remote_con_data.qp_num);
    fprintf(stdout, "Remote LID = 0x%x\n", remote_con_data.lid);
  10. 转换QP状态(ibv_modify_qp)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    /* 10 转换QP状态 */
    // RESET -> INIT
    struct ibv_qp_attr attr;
    int flags;
    memset(&attr, 0, sizeof(attr));
    attr.qp_state = IBV_QPS_INIT;
    attr.port_num = 1; // IB 端口号
    attr.pkey_index = 0;
    attr.qp_access_flags = IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_READ | IBV_ACCESS_REMOTE_WRITE;
    flags = IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT | IBV_QP_ACCESS_FLAGS;
    rc = ibv_modify_qp(res.qp, &attr, flags);
    if (rc)
    fprintf(stderr, "failed to modify QP state to INIT\n");

    //INIT -> RTR(Ready To Receive)
    memset(&attr, 0, sizeof(attr));
    attr.qp_state = IBV_QPS_RTR;
    attr.path_mtu = IBV_MTU_256;
    attr.dest_qp_num = res.remote_props.qp_num;
    attr.rq_psn = 0;
    attr.max_dest_rd_atomic = 1;
    attr.min_rnr_timer = 0x12;
    attr.ah_attr.is_global = 0;
    attr.ah_attr.dlid = res.remote_props.lid;
    attr.ah_attr.sl = 0;
    attr.ah_attr.src_path_bits = 0;
    attr.ah_attr.port_num = 1;
    flags = IBV_QP_STATE | IBV_QP_AV | IBV_QP_PATH_MTU | IBV_QP_DEST_QPN | IBV_QP_RQ_PSN | IBV_QP_MAX_DEST_RD_ATOMIC | IBV_QP_MIN_RNR_TIMER;
    rc = ibv_modify_qp(res.qp, &attr, flags);
    if (rc)
    fprintf(stderr, "failed to modify QP state to RTR\n");

    //RTR -> RTS(Ready To Send)
    memset(&attr, 0, sizeof(attr));
    attr.qp_state = IBV_QPS_RTS;
    attr.timeout = 0x12;
    attr.retry_cnt = 6;
    attr.rnr_retry = 0;
    attr.sq_psn = 0;
    attr.max_rd_atomic = 1;
    flags = IBV_QP_STATE | IBV_QP_TIMEOUT | IBV_QP_RETRY_CNT | IBV_QP_RNR_RETRY | IBV_QP_SQ_PSN | IBV_QP_MAX_QP_RD_ATOMIC;
    rc = ibv_modify_qp(res.qp, &attr, flags);
    if (rc)
    fprintf(stderr, "failed to modify QP state to RTS\n");
    • 状态:RESET -> INIT -> RTR -> RTS
    • 要严格按照顺序进行转换
    • QP刚创建时状态为RESET
    • INIT之后就可以调用ibv_post_recv提交一个receive buffer了
    • 当QP进入RTR(ready to receive)状态以后,便开始进行接收处理
    • RTR之后便可以转为RTS(ready to send),RTS状态下可以调用ibv_post_send
  11. 创建发送任务/接收任务(ibv_send_wr/ibv_recv_wr)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /* 11 创建发送任务ibv_send_wr */
    struct ibv_send_wr sr;
    struct ibv_sge sge;
    struct ibv_send_wr *bad_wr = NULL;
    int rc;
    /* prepare the scatter/gather entry */
    memset(&sge, 0, sizeof(sge));
    sge.addr = (uintptr_t)res->buf;
    sge.length = MSG_SIZE;
    sge.lkey = res->mr->lkey;
    /* prepare the send work request */
    memset(&sr, 0, sizeof(sr));
    sr.next = NULL;
    sr.wr_id = 0;
    sr.sg_list = &sge;
    sr.num_sge = 1;
    sr.opcode = opcode;
    sr.send_flags = IBV_SEND_SIGNALED;
    if (opcode != IBV_WR_SEND)
    {
    sr.wr.rdma.remote_addr = res->remote_props.addr;
    sr.wr.rdma.rkey = res->remote_props.rkey;
    }
    • 该任务会被提交到QP中的SQ(Send Queue)中
    • 发送任务有三种操作:Send,Read,Write。Send操作需要对方执行相应的Receive操作;Read/Write直接操作对方内存,对方无感知。
    • 把要发送的数据的内存地址,大小,密钥告诉HCA
    • Read/Write还需要告诉HCA远程的内存地址和密钥
  12. 提交发送任务/接收任务(ibv_post_send/ibv_post_recv)
    1
    2
    3
    4
    rc = ibv_post_send(res->qp, &sr, &bad_wr);
    if (rc)
    fprintf(stderr, "failed to post SR\n");
    return rc;
  13. 轮询任务完成信息(ibv_poll_cq)
    1
    2
    3
    4
    5
    6
    7
    8
    /* 13 轮询任务结果 */
    struct ibv_wc wc;
    int poll_result;
    int rc = 0;
    do
    {
    poll_result = ibv_poll_cq(res->cq, 1, &wc);
    } while (poll_result == 0);

QP状态转换

rdma_qp_status

Reset State

  1. 该状态为QP新创建时的初始状态
  2. 在不删除QP的情况下,仅能通过Modify Queue Pair Attributes verb跳出该状态
  3. 该状态下向QP提交WR,将会返回错误,远端到来的消息也会直接被忽略
  4. 通过利用verbs修改QP属性,可以将任何状态的QP转换为Reset状态。

Initialized State

  1. 仅能从Reset状态进入该状态
  2. 在不删除QP的情况下,仅能通过Modify Queue Pair Attributes verb跳出该状态
  3. 该状态下,RQ可以接受WR,但不会处理远端到来的消息,并将到来的消息丢弃
  4. 该状态下,向SQ发送WR会返回错误。

Ready To Receive(RTR) State

  1. 在RTR状态下,RQ可以接受WR,从远端到来的消息也会正常处理
  2. 该状态下,向SQ发送WR会返回错误。

Ready To Send(RTS) State

  1. 在RTS状态下,请求端和应答端面向连接的服务类型的通道已经建立
  2. 仅能由RTR和SQD状态进入该状态
  3. 在不删除QP的情况下,仅能通过Modify Queue Pair Attributes verb跳出该状态
  4. 该状态下,向QP发送的WR会被正常处理,发送WR的verb也不会返回错误
  5. 该状态下,从远端到来的消息也正常处理。

Send Queue Drain(SQD) State

  1. 该状态下,向QP发送的WR会被正常处理,发送WR的verb也不会返回错误
  2. 该状态下,从远端到来的消息也正常处理
  3. 仅能由RTS进入该状态
  4. 当转移到该状态时,未处理的消息不能再处理,未处理完的消息必须处理完
  5. 当所有应答都已收到时,如果有事件提醒的请求,则会生成一个附加的异步事件
    a. 消费者可以利用异步事件来确定状态转移的完成
    b. 为确保安全的修改QP的属性,必须在接收到异步事件后在进行属性的更改。
  6. 该状态下,提交到QP的WR会入队,但不会被处理
  7. 在SQ还没有Drained之前,SQD到RTS的状态转换不被允许,在该状况下转移,CI会报告一个立即的错误。

Send Queue Error(SQE) State

  1. 该状态适用于除RC QP之外的所有QP
  2. 该状态仅会由RTS跳入,当处理SQ的WR时,发生完成错误(Completion Error),会造成该转移
  3. 在该状态下,RQ可以接受WR,从远端到来的消息也会正常处理
  4. 发生了完成错误的WR必须通过CQ返回正确的完成错误码
  5. 由于SQ中的WR可能部份或全部执行,因此,接收端的状态是未知的
  6. 在SQ中导致完成错误的WR的下一条WR,必须通过CQ返回刷新错误(Flush Error)的完成状态
  7. 该状态下,可以通过Modify Queue Pair Attributes verb跳到RTS状态、Reset状态、Error状态。

Error State

  1. QP上的所有正常处理全部停止
  2. 由于WR发生完成错误,导致跳入该状态时,必须通过CQ返回完成错误码
  3. 该状态下,从远端收到的数据会被丢弃
  4. 在QP中导致完成错误的WR的下一条WR,必须通过CQ返回刷新错误(Flush Error)的完成状态。

One-Side RDMA

  1. 首先A、B建立连接,QP已经创建并且初始化。
  2. 数据被存档在A的buffer地址VA,注意VA应该提前注册到A的RNIC,并拿到返回的r_key,相当于RDMA操作这块buffer的权限。
  3. A把数据地址VA,key封装到专用的报文传送到B,这相当于A把数据buffer的操作权交给了B。同时A在它的WQ中注册进一个WR,以用于接收数据传输的B返回的状态。
  4. B在收到A的送过来的数据VA和r_key后,RNIC会把它们连同存储地址VB到封装RDMA READ,这个过程A、B两端不需要任何软件参与,就可以将A的数据存储到B的VB虚拟地址。
  5. B在存储完成后,会向A返回整个数据传输的状态信息。

** 单边操作传输方式是RDMA与传统网络传输的最大不同,只需提供直接访问远程的虚拟地址,无须远程应用的参与其中,这种方式适用于批量数据传输。 **

参考&鸣谢

radosgw_rgw_auth.png

** 版本 **
mimic-13.2.6

原理

RGW认证机制中涉及的类分为两类,Strategy类和Engine类。

Strategy(认证策略)

  • 通过Strategy::add_engine向Strategy注册Engine,注册过程中需要指定Control
  • Control 包括REQUISITESUFFICIENTFALLBACK
    REQUISITE 表示这个Engine是一个必要条件,若一个注册的Engine返回失败,则立即终止Strategy的认证过程,并且不再对其它注册的Engine进行认证;
    SUFFICIENT 表示这个Engine是一个充要条件,若一个注册的Engine返回成功,则Strategy完成。然而一个Engine的失败,不会终止整个Strategy,直到所有Engine都返回失败;
    FALLBACKSUFFICIENT类似,所有注册的Engine返回失败,返回result_t::deny(reason = -EACCES);
    具体可见strategy_handle_rejectedstrategy_handle_deniedstrategy_handle_granted三个方法。
  • 一个Strategy可以包含多个Strategy和Engine。当验证过程遇到Strategy时,会进行递归调用,直到Engine返回验证结果。
  • 认证的入口为Strategy::apply,由Strategy::apply调用Strategy的authenticate方法开始逐层递归认证。

Engine(认证引擎)

  • Engine处理具体的认证请求,分为S3AnonymousEngineLDAPEngineLocalEngine
  • Engine的认证状态包括DENIEDGRANTEDREJECTED
    DENIED 没有REJECTED那么强烈的认证失败;
    GRANTED 认证成功;
    REJECTED 认证失败,不需要再尝试其它Engine了;

没钱、没钱、没钱,重要的事情说三遍,因为没钱,所以买不起正版的golang IDE,只能使用免费的轻量级的工具完成golang开发任务。

那么,Coding可以使用vim。debug呢?以前用gdb,据说出了个dlv,据说这个dlv可以调试goroutine。抱着试试看的心态尝试一下。

dlv

安装

1
go get -u github.com/derekparker/delve/cmd/dlv

配置

配置文件在~/.dlv/config.yml,推荐修改其中的max-string-len,此配置为debug时,查看string变量的内容,最大显示多长,对于一些超长的字符串,会显示不下。所以为了看到更为完整的内容,建议将其设置为max-string-len: 640

当然,此配置也可以在debug过程中动态修改,详细请见help中的config命令。

使用

启动dlv

1
dlv debug *.go

进入交互界面后,可以使用help查看命令,b设置断点,llist代码,基本使用与gdb很相似,如此用户体验还是不错的。

gdb

Todo…

参考&鸣谢

使用官方ceph-container工程构建私人定制的ceph/daemon镜像。本文以mimic版本为例,在对mimic进行二次开发后,将生成的rpm包更新到yum源中,再生成新的ceph-release中使用该yum源。

获取官方ceph-container

1
git clone git@github.com:ceph/ceph-container.git

安装私有Ceph.repo

修改获取ceph-releaseRPM包地址,编辑文件./ceph-releases/ALL/centos/daemon-base/__DOCKERFILE_INSTALL__

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
yum install -y epel-release && \
yum install -y jq && \
bash -c ' \
if [ -n "__GANESHA_PACKAGES__" ]; then \
echo "[ganesha]" > /etc/yum.repos.d/ganesha.repo ; \
echo "name=ganesha" >> /etc/yum.repos.d/ganesha.repo ; \
if [[ "${CEPH_VERSION}" =~ master|^wip* ]]; then \
REPO_URL=$(curl -s "https://shaman.ceph.com/api/search/?project=nfs-ganesha&distros=centos/__ENV_[BASEOS_TAG]__&flavor=ceph_master&ref=next&sha1=latest" | jq -a ".[0] | .url"); \
echo "baseurl=$REPO_URL/\$basearch/" >> /etc/yum.repos.d/ganesha.repo ; \
elif [[ "${CEPH_VERSION}" == nautilus ]]; then \
echo "baseurl=http://download.ceph.com/nfs-ganesha/rpm-V2.8-stable/$CEPH_VERSION/\$basearch/" >> /etc/yum.repos.d/ganesha.repo ; \
else \
echo "baseurl=http://download.ceph.com/nfs-ganesha/rpm-V2.7-stable/$CEPH_VERSION/\$basearch/" >> /etc/yum.repos.d/ganesha.repo ; \
fi ; \
echo "gpgcheck=0" >> /etc/yum.repos.d/ganesha.repo ; \
echo "enabled=1" >> /etc/yum.repos.d/ganesha.repo ; \
fi ; \
if [ -n "__ISCSI_PACKAGES__" ]; then \
for repo in tcmu-runner python-rtslib; do \
curl -s -L https://shaman.ceph.com/api/repos/$repo/master/latest/__ENV_[BASEOS_REPO]__/__ENV_[BASEOS_TAG]__/repo > /etc/yum.repos.d/$repo.repo ; \
done ; \
if [[ "${CEPH_VERSION}" =~ master|^wip* ]]; then \
curl -s -L https://shaman.ceph.com/api/repos/ceph-iscsi/master/latest/__ENV_[BASEOS_REPO]__/__ENV_[BASEOS_TAG]__/repo > /etc/yum.repos.d/ceph-iscsi.repo ; \
elif [[ "${CEPH_VERSION}" == nautilus ]]; then \
curl -s -L https://download.ceph.com/ceph-iscsi/3/rpm/el__ENV_[BASEOS_TAG]__/ceph-iscsi.repo -o /etc/yum.repos.d/ceph-iscsi.repo ; \
else \
curl -s -L https://download.ceph.com/ceph-iscsi/2/rpm/el__ENV_[BASEOS_TAG]__/ceph-iscsi.repo -o /etc/yum.repos.d/ceph-iscsi.repo ; \
fi ; \
fi' && \
yum update -y && \
rpm --import 'https://download.ceph.com/keys/release.asc' && \
bash -c ' \
if [[ "${CEPH_VERSION}" =~ master|^wip* ]] || ${CEPH_DEVEL}; then \
REPO_URL=$(curl -s "https://shaman.ceph.com/api/search/?project=ceph&distros=centos/__ENV_[BASEOS_TAG]__&flavor=default&ref=${CEPH_VERSION}&sha1=latest" | jq -a ".[0] | .url"); \
RELEASE_VER=0 ;\
else \
RELEASE_VER=1 ;\
REPO_URL="http://10.100.13.112/rpm-${CEPH_VERSION}/el__ENV_[BASEOS_TAG]__/"; \
fi && \
rpm -Uvh "$REPO_URL/noarch/ceph-release-1-${RELEASE_VER}.el__ENV_[BASEOS_TAG]__.noarch.rpm" ' && \
yum install -y __CEPH_BASE_PACKAGES__ && \
bash -c ' \
if [[ "${CEPH_VERSION}" =~ master|^wip* ]] || ${CEPH_DEVEL}; then \
yum install -y python-pip ; \
pip install -U remoto ; \
yum remove -y python-pip ; \
fi '

REPO_URL使用您指定的yum源。

指定版本构建镜像

1
make FLAVORS=mimic-13.2.6-1.gba13b2d.el7,centos,7 build

参考&鸣谢