从服务化到云原生-配置

配置(Configguration)对于每个工程师来说都不陌生,相信没有哪个系统是不提供配置参数的。

本文讲解由单机应用的本地配置向分布式应用的配置中心演进过程,以及配置中心的一些核心概念

1 本地配置

在集中式系统架构的单机应用时代,配置大多数通过属性文件的形式存储以Key=Value的形态出现。当然也有使用XML或YAML等更加复杂的方式进行配置(比如Spring的配置文件application.xml)但开发工程师更倾向于将他们归类为代码部分,真正可以动态修改的配置应该是简单、易于理解的、易于修改的。

在单机时代,配置文件就够用了。运维工程师如果想修改配置,登录生产机器,用VIM本文编辑,然后重启应用,或者用定时任务从新加载配置文件就可以生效。

2 配置集中化

服务器增加导致运维工作量增加,分布式系统很难使用本地配置。采用集中化的方式,也就是散落在每台服务器上运维操作集中于一点统一处理,然后程序通过远程通信或异步消息分发到各个服务器。对系统配置进行修改是运维工程师的重要工作之一,所以对配置进行统一管理是大势所趋。

本地配置存在的问题:

  •  配置工作量大;
  •  配置修改遗漏,导致各个节点配置环境不一致;
  •  各个节点配置不一致的时间差长(各个服务器操作时间不同);
  •  配置修改无法动态生效(定时重新加载或重启应用);
  •  直接修改配置文本信息产生错误难于校验;

配置中心能解决上述问题,还能提供额外的便利:

  •  配置工作量少,单点修改,全局生效;
  •  配置修改不容易遗漏;
  •  各个节点配置不一致的时间差比较短;
  •  配置信息可以像业务数据一样被持久化保存,方便恢复,方便搭建环境,方便最终修改历史;
  • 多个系统上线时,配置检查、沟通协调容易;
  •  配置信息放置于应用程序之外,更容易保持应用的无状态化,为容器化和微服务化部署方案提供了强有力的支持。

3 配置值中心和注册中心

很多人认为配置中心和注册中心是可以相互替换的两个同义词,因为他们的使用场景非相似。另外,当前很多开源产品,如zookeeper、etcd等,都同时支持这两种场景,这也更加容易让人误以为他们就是同一事物。但事实上,他们还是有着本质区别的。

注册中心与配置中心的关注点不完全相同。注册中心用于分布式系统的服务质量,多用于管理运行在当前集群中的服务的状态,需要随时进行动态更新。而配置中心则不然,它关注的是配置本身,相比于状态,配置是更加静态和具象的事物。配置的三个要素是快速传播、变更稀疏、环境相关

快速传播:分布式场景下,各个服务节点都需要得到一致的数据,无论是配置还是状态,一旦发生改变往往要求集群中的所有节点同时感知变更

变更稀疏:配置发生变更的情况非常少,因此配置中心对读性能进行优化,而对写要求稍低

环境相关:一般耳熟能详的系统环境有开发环境、测试环境、线上环境,不同的环境对应着不同的配置,但注册中心所关注的应用程序的运行状态和环境是没有任何关系

3.1 读性能

采用配置中心方案后,就必须要考虑远程调用导致的性能下降和配置中心本身的单节点访问压力的问题

解决方案:缓存。

因为配置信息是可穷举的,不可能是海量的,所以一次性杯加载进本地缓存是非常方便的

缓存分为位于配置中心的集中式缓存和位于应用端的本地缓存

  • 集中式缓存:配置信息读远大于写。每次读取磁盘影响性能,缓存到内存。

    • 优点:能访问最新的数据,数据一致性好,提升访问效率;
    • 缺点:没有缓解配置中心的访问压力。
  • 本地缓存:客户端缓存,尽量访问本地,只有在配置发生变化的时候才读配置中心,更新缓存。

    • 优点:减少远程调用,提升访问效率,缓解了配置中心压力。
    • 缺点:数据存在多份,可能不一致。

补充:

缓存击穿:缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞,缓存还有可能降低效率;

3.2 变更实时性

如果使用本地缓存,数据就会存在多个副本,配置中心数据发生变更时,如何将配置信息实时通知给应用客户端。

一般有两种方式:监听实时同步

  • 监听:配置中心的客户端都需要与配置中心建立长连接。配置变化时,配置变化了,主动推送各个客户端,客户端更新缓存。
    • 优点:实时性高;
    • 缺点: 长连接比较消耗系统资源。并且长连接一旦断了,还要从新连接,容错等。

保持长连接有效的方法是:心跳监听服务,一旦发现连接不可用则销毁连接建立新连接。为了保证应用客户端能正确接收到信息变更请求,也需要让客户端给予反馈,不反馈就一直发。客户端要实现幂等性(在应答式通信系统中,可能存存在多发请求的情况; 比如kafka重复消费数据)。 

  • 实时同步:客户端主动定时去询问配置中心。如果发现缓存和配置中心不一致,就更新缓存。这样使用短连接就可以。
  • 优点:节省连接资源,降低服务中心的压力。
  • 缺点:间隔时间长,则配置更新不及时;间隔时间短,则配置中心压力过大,并且做很多无用功。

其他方法;设置缓存失效时间。 

3.3 可用性

配置中心是整个分布式系统的核心,一旦配置中心不可用,整个系统将会受到极大影响

那么如何提升配置中心的可用性呢?服务冗余、缓存

  • 服务冗余:做服务集群,数据备份

    • 1 基于主节点提供服务:主从模式,主节点提供服务,从节点冗余数据备份
    • 2 基于对等节点提供服务:节点对等,在访问量非常大的情况下可以有效分流(但对于配置中心,有点小题大做)
  • 缓存:也是服务冗余的一种,但只是冗余数据,而不是整个服务

    • 优点:提升读取配置信息的性能,可以在配置中心节点全失效时提供应急使用,也叫离线模式。
    • 缺点:缓存更新不了了。

 

3.4 数据一致性

分布式架构下,数据一致性如何保证

一致性三种方案

  • ACID(强一致性)

  • BASE:BASE是Basically Available(基本可用,响应时间上有损失,功能有损失Soft state(软状态,软状态是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同的数据副本之间进行数据同步的过程存在延时Eventually consistent(最终一致性,最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状三个短语的简写。 其核心思 想是即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方法来使系统达到最终一致性。最终一致性是一种特殊的弱一致性:系统能够保证在没有其他新的更新操作的情况下,数据最终一定能够达到一致的状态,因此所有客户端对系统的数据访问都能够获取到最新的值。同时,在没有发生故障的前提下,数据到达一致状态的时间延迟,取决于网络延迟、系统负载和数据复制方案设计等因素。

  • 状态机:状态机是表示实体的状态根据条件转移的数学模型。通过状态机模型,系统可以判断当前不一致状态,以及如何校正不一致状态到一致状态;

  • 基于状态机的数据一致性算法是:ZAB和Raft

  • 成熟的产品有zookeeper(基于ZAB算法);etcd(基于Raft算法)和Consul(基于Raft算法)

那么配置中心选择哪种方案更加合适呢?

配置中心并不需要ACID的事物,也不会有类似于关系型数据库那一复杂跨表的关联操作;对于BASE的最终一致性的柔性事物场景而言,一致性状态没有时间的保证,因此也不适合用于处理相对敏感的配置信息;通过状态机保证数据一致性的处理方式,无论是在一致性还是性能上,都更加适合配置中心。

设计模式-抽象工厂模式

在前面介绍的工厂方法模式是考虑的一类产品的生产,如手机工厂只生产手机,也就是说,工厂方法模式只考虑生产同等级产品,但是在现实生活中,许多工厂是综合型的工厂,手机工厂不仅仅只生产手机,还生产耳机、手机壳、充电宝等等

本节要介绍的抽象工厂模式将考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族

抽象工厂模式与工厂方法模式最大的区别:抽象工厂中每个工厂可以创建多种类的产品;而工厂方法每个工厂只能创建一类

1 模式介绍

1.1 定义

抽象工厂(AbstractFactory)模式的定义:是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。

1.2 模式组成

抽象工厂模式的主要角色如下。

组成(角色) 关系 作用
抽象产品(Product) 具体产品的父类 描述具体产品的公共接口
具体产品(Concrete Product) 抽象产品的子类;工厂类创建的目标类 描述生产的具体产品
抽象工厂(Creator) 具体工厂的父类 描述具体工厂的公共接口
具体工厂(Concrete Creator) 抽象工厂的子类;被外界调用 描述具体工厂;实现FactoryMethod工厂方法创建产品的实例

从图上面的图可以看出抽象工厂模式的结构同工厂方法模式的结构相似,不同的是其产品的种类不止一个,所以创建产品的方法也不止一个。

1.3 解决的问题

解决工厂方法模式的缺点

每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度,工厂方法可以解决这一问题

2 实例讲解:

继续工厂方法模式中手机专卖店Store卖手机的例子,不了解的可以去看看工厂方法模式中手机专卖店的例子。目前Store卖有两种手机,一个苹果手机,一个索尼手机,此时客户需要买耳机,那么专卖店就必须要进货,采购不同厂商品牌的耳机(苹果耳机、索尼耳机)。那么就必须要新增耳机工厂(苹果耳机工厂、索尼耳机工厂),而且还是不同品牌的耳机,假如后面又增加了一个品牌(比如小米)的手机、耳机、充电器、手机壳,此时使用工厂方法模式就要增加小米手机工厂、耳机工厂、充电器工厂、手机壳工厂,想想对这一大堆工厂的管理就很麻烦,那么使用抽象工厂模式就能解决这种问题

2.1 使用步骤

步骤1: 创建抽象产品类 ,定义具体产品的公共抽象接口;

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
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* 耳机
*
* @author mrxccc
* @create 2020/9/23
*/
public abstract class Headset {
/**
* 品牌
*/
protected String brand;

abstract void play();

public String getBrand() {
return brand;
}

public void setBrand(String brand) {
this.brand = brand;
}
}


/**
* @author mrxccc
* @create 2020/9/23
*/
public abstract class Phone {
/**
* 品牌
*/
protected String brand;

/**
* 操作系统
*/
protected String os;

/**
* 充电
*/
public abstract void charge();

public String getBrand() {
return brand;
}

public void setBrand(String brand) {
this.brand = brand;
}

public String getOs() {
return os;
}

public void setOs(String os) {
this.os = os;
}
}

步骤2: 创建抽象工厂类,定义具体工厂的公共接口

1
2
3
4
5
6
7
8
9
10
/** 
* 这里和工厂方法模式不同,定义了同一等级的不同产品
* @author mrxccc
* @create 2020/9/23
*/
public interface Factory {
Phone getPhone();

Headset getHeadset();
}

步骤3:创建具体工厂类(继承抽象工厂类),定义创建对应具体产品实例的方法;

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
/**
* 苹果工厂
* @author mrxccc
* @create 2020/9/23
*/
public class AppleFactory implements Factory{
@Override
public ApplePhone getPhone() {
ApplePhone applePhone = new ApplePhone();
applePhone.setBrand("Apple");
return applePhone;
}

@Override
public AppleHeadset getHeadset() {
AppleHeadset appleHeadset = new AppleHeadset();
appleHeadset.setBrand("Apple");
return appleHeadset;
}

}

/**
* 索尼工厂
*
* @author mrxccc
* @create 2020/9/23
*/
public class SonyFactory extends Factory {
@Override
public SonyPhone getPhone() {
SonyPhone sonyPhone = new SonyPhone();
sonyPhone.setBrand("Sony");
return sonyPhone;
}

@Override
public SonyHeadset getHeadset() {
SonyHeadset sonyHeadset = new SonyHeadset();
sonyHeadset.setBrand("Sony");
return sonyHeadset;
}

}

步骤4: 创建抽象产品类

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
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* @author mrxccc
* @create 2020/9/23
*/
public abstract class Phone {
/**
* 品牌
*/
protected String brand;

/**
* 操作系统
*/
protected String os;

/**
* 充电
*/
public abstract void charge();

public String getBrand() {
return brand;
}

public void setBrand(String brand) {
this.brand = brand;
}

public String getOs() {
return os;
}

public void setOs(String os) {
this.os = os;
}
}

/**
* 耳机
*
* @author mrxccc
* @create 2020/9/23
*/
public abstract class Headset {
/**
* 品牌
*/
protected String brand;

abstract void play();

public String getBrand() {
return brand;
}

public void setBrand(String brand) {
this.brand = brand;
}

}

步骤5: 创建具体产品类(继承抽象产品类), 定义生产的具体产品;

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
48
49
/**
* 苹果手机
*
* @author mrxccc
* @create 2020/9/23
*/
public class ApplePhone extends Phone {
@Override
public void charge() {
System.out.println("普通充电");
}
}
/**
* 索尼手机
*
* @author mrxccc
* @create 2020/9/23
*/
public class SonyPhone extends Phone {
@Override
public void charge() {
System.out.println("快充");
}
}
/**
* 苹果耳机
* @author mrxccc
* @create 2020/9/23
*/
public class AppleHeadset extends Headset{
@Override
void play() {
// Apple 耳机播放逻辑 ...
System.out.println("Apple 耳机播放完成");
}
}
/**
* 索尼耳机
*
* @author mrxccc
* @create 2020/9/23
*/
public class SonyHeadset extends Headset {
@Override
void play() {
// Sony 耳机播放逻辑...
System.out.println("Sony 耳机播放完成");
}
}

步骤6:外界通过调用具体工厂类的方法,从而创建不同具体产品类的实例

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
/**
* 专卖店D:抽象工厂模式
* @author mrxccc
* @create 2020/9/23
*/
public class StoreD {
private Factory factory;

public StoreD(Factory factory) {
super();
this.factory = factory;
}

/**
* 补充手机
*/
public void supplyPhone() {
Phone phone = factory.getPhone();
// 补充手机逻辑...
System.out.println("补充" + phone.getBrand() + "手机完成");
}

/**
* 补充耳机
*/
public void supplyHeadset() {
Headset headset = factory.getHeadset();
// 补充耳机逻辑...
System.out.println("补充" + headset.getBrand() + "耳机完成");
}

public static void main(String[] args) {
StoreD storeC = new StoreD(new SonyFactory());
storeC.supplyPhone();
storeC.supplyHeadset();
}
}

结果

1
2
补充Sony手机完成
补充Sony耳机完成

3.应用场景

抽象工厂模式最早的应用是用于创建属于不同操作系统的视窗构件。如java 的 AWT 中的 Button 和 Text 等构件在 Windows 和 UNIX 中的本地实现是不同的。

抽象工厂模式通常适用于以下场景:

  1. 当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空调等。
  2. 系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋。
  3. 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。

4.模式扩展

抽象工厂模式的扩展有一定的“开闭原则”倾斜性:

  1. 当增加一个新的产品族(是产品族,不是某个产品)时只需增加一个新的具体工厂,不需要修改原代码,满足开闭原则。
  2. 当产品族中需要增加一个新种类的产品时,则所有的工厂类都需要进行修改,不满足开闭原则。

另一方面,当系统中只存在一个等级结构的产品时,抽象工厂模式将退化到工厂方法模式。

源码:设计模式

其他设计模式介绍:

设计模式-工厂方法模式

设计模式-抽象工厂模式

设计模式-建造者模式

更多精彩内容:mrxccc

Gitlab Runner的安装与配置

Runner

Runner就像一个个的工人,而Gitlab-CI就是这些工人的一个管理中心,所有工人都要在Gitlab-CI里面登记注册,并且表明自己是为哪个工程服务的。当相应的工程发生变化时,Gitlab-CI就会通知相应的工人执行软件集成脚本。如下图所示:

gitlab里面的runner叫Gitlab-Runner,Gitlab-Runner是配合Gitlab-CI进行使用的。一般地,Gitlab里面的每一个工程都会定义一个属于这个工程的软件集成脚本,用来自动化地完成一些软件集成工作。当这个工程的仓库代码发生变动时,比如有人push了代码,GitLab就会将这个变动通知Gitlab-CI。这时Gitlab-CI会找出与这个工程相关联的Runner,并通知这些Runner把代码更新到本地并执行预定义好的执行脚本(也就是在Job执行流程那个图中所示的第三步:script),所以,Gitlab-Runner就是一个用来执行软件集成脚本script的东西。

Runner类型

Gitlab-Runner可以分类两种类型:Shared Runner(共享型)Specific Runner(指定型)

  • Shared Runner:这种Runner(工人)是所有工程都能够用的。只有系统管理员能够创建Shared Runner。
  • Specific Runner:这种Runner(工人)只能为指定的工程服务。拥有该工程访问权限的人都能够为该工程创建Shared Runner。

什么情况下需要注册Shared Runner?

  1. 比如,GitLab上面所有的工程都有可能需要在公司的服务器上进行编译、测试、部署等工作,这个时候注册一个Shared Runner供所有工程使用就很合适。

什么情况下需要注册Specific Runner?

  1. 比如,我可能需要在我个人的电脑或者服务器上自动构建我参与的某个工程,这个时候注册一个Specific Runner就很合适。

Runner搭建

1、linux操作系统,安装docker环境。(如果安装了devops,docker环境可以不用安装)

2、安装gitlab-runner

1. Runner 安装

GitLab Runner 10之前,gitlab-runner的名称叫gitlab-ci-multi-runner,安装命令如下

1
2
3
4
5
6

# For Debian/Ubuntu
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.deb.sh | sudo bash

# For RHEL/CentOS
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.rpm.sh | sudo bash

GitLab Runner 10及其以上,可执行文件已重命名为gitlab-runner

linux系统,可以通过以下命令安装

1
2
3
4
5
# For Debian/Ubuntu/Mint
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash

# For RHEL/CentOS/Fedora
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh | sudo bash

如果要安装特定版本的GitLab Runner:

1
2
3
4
5
6
7
# for DEB based systems
apt-cache madison gitlab-runner
export GITLAB_RUNNER_DISABLE_SKEL=true; sudo -E apt-get install gitlab-runner=10.0.0

# for RPM based systems
yum list gitlab-runner --showduplicates | sort -r
export GITLAB_RUNNER_DISABLE_SKEL=true; sudo -E yum install gitlab-runner-10.0.0-1

授予其执行权限:

1
sudo chmod +x /usr/local/bin/gitlab-runner

创建一个GitLab CI用户:

1
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash

安装并作为服务运行:

1
2
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start

2. 获取Runner注册Token

安装好Runner之后,需要向Gitlab进行注册,注册Runner需要GitLab-CI的url和token。可根据需求注册选择所需类型Runner。这里介绍spercific runners为例

图中的Url和Token是runner链接到仓库的两个重要参数

3.runner配置

执行注册runner的命令(如果你是安装的gitlab-ci-multi-runner):sudo gitlab-ci-multi-runner register

执行注册runner的命令(如果你是安装的gitlab-runner):sudo gitlab-runner register

接下来会提示你输入一系列配置内容

注意,在要求输入tag时,想好tag的名字,这个就相当于你的runner的id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
## 输入url
a、Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com )

## 输入token
b、Please enter the gitlab-ci token for this runner

## 写个描述
c、Please enter the gitlab-ci description for this runner

## 这个tag很重要,好好想个名字并记住,随后在ci配置中需要对应上。
d、Please enter the gitlab-ci tags for this runner (comma separated)

## ci没有配置tags时是否执行这个runner?建议采用默认值。
e、Whether to run untagged builds [true/false]

## 是否只对当前工程有效?理论上讲只有“Shared runners”才有效。选true。
f、Whether to lock Runner to current project [true/false]

## 选择一个执行器。我们接来下的方案是基于shell的,输入shell。
g、Please enter the executor: virtualbox, docker+machine, kubernetes, parallels, docker-ssh, shell, ssh, docker-ssh+machine, docker:

注册完成后,会出现一个runner,我这里注册了两个,所以会有两个tag

runner 左边会有一个小绿点,表示该runner是能正常执行的

image-20201103104204166

Gitlab-runner的配置

GitLab-CI会为这个Runner生成一个唯一的token,以后Runner就通过这个token与GitLab-CI进行通信。

那么,问题来了。注册好了的Runner的信息存放在哪儿了呢?

原来,Runner的信息是存放在一个配置文件里面的,配置文件的格式一般是.toml。这个配置文件的存放位置有以下几种情况:

  • 在类Unix操作系统下(0.5.0之后版本)
    • 如果是以root用户身份运行gitlab-runner register,那么配置文件默认是/etc/gitlab-runner/config.toml
    • 如果是以非root用户身份运行gitlab-runner register,那么配置文件默认是~/.gitlab-runner/config.toml
  • 在其他操作系统下以及0.5.0之前版本

配置文件默认在当前工作目录下./config.toml

一般情况下,使用默认的配置文件存放Runner的配置信息就可以了。当然,如果你有更细化的分类需求,你也可以在注册的时候通过-c或–config选项指定配置文件的位置。具体查看register命令的使用方法:gitlab-runner register --help

问题:如果不运行gitlab-runner register命令,直接在配置文件里面添加Runner的配置信息可以吗?

回答:当然不可以。因为gitlab-ci-runner register的作用除了把Runner的信息保存到配置文件以外,还有一个很重要的作用,那就是向GitLab-CI发出请求,在GitLab-CI中登记这个Runner的信息并且获取后续通信所需要的token。

让注册好的Runner运行起来

Runner注册完成之后还不行,还必须让它运行起来,否则它无法接收到GitLab-CI的通知并且执行软件集成脚本。怎么让Runner运行起来呢?gitlab-runner提供了这样一条命令gitlab-runner run-single

以上的就是在linux机器上直接安装runner的过程,关于gitlab ci怎么使用runner执行我们想要执行的任务,可参考这篇文章:

Gitlab-CICD最简单明了的入门教程

使用docker安装gitlab runner:Gitlab Runner的容器化安装与使用

更多精彩内容:mrxccc

Gitlab-runner的容器化安装与使用

gitlab-runner有多种安装方式,具体详见 gitlab-runner安装 ,本文采用的docker的方式进行runner的部署

为什么用docker安装gitlab-runner?

  • 1.runner机器经常改变
  • 2.每个作业都处于干净的环境中,没有过去的历史记录。并发任务执行正常,因为每个构建都有自己的 Docker 引擎实例,因此它们不会相互冲突

开始安装:

前提安装好docker环境

1.创建并启动gitlab-runner容器

1
2
3
4
5
sudo docker run -d --name gitlab-runner --restart always   
-v /var/run/docker.sock:/var/run/docker.sock
-v /srv/gitlab-runner/config:/etc/gitlab-runner
-v /home/gitlab-runner/shell:/home/gitlab-runner/shell
-v /root/build_cache:/cache gitlab/gitlab-runner:latest

注:

  1. 第一个 -v 实现docker.sock的挂载(因为后续要实现docker in docker的使用,即 docker executor);
  2. 第二个 -v 实现gitlab-runner的配置挂载(可选)
  3. 第三个 -v 实现gitlab-runner cache目录的挂载(可选),cache相关请参考 docker executor 下的cache部分。
  4. 第四个 -v 实现本地shell目录挂载(可选)

修改gitlab-runner时区为宿主机时区,因为有可能在gitlab-容器中使用到时间,可以执行该命令:

1
sudo docker cp /etc/localtime gitlab-runner:/etc/localtime

2.注册runner

1
2
3
4
5
6
7
8
9
10
11
docker exec gitlab-runner gitlab-runner register -n \
--url https://git-pd.megvii-inc.com/ \
--registration-token qZXDG-z3uPGvsVn-5SNx \
--tag-list runInDk \
--executor docker \
--docker-image mrxccc/maven-jdk8-docker:latest \
--docker-volumes /root/.m2:/root/.m2 \
--docker-volumes /root/.npm:/root/.npm \
--docker-volumes /var/run/docker.sock:/var/run/docker.sock \
--docker-volumes /home/gitlab-runner/shell:/home/gitlab-runner/shell \
--description "runInDk"

注:

  • url 为 gitlab的地址;
  • registration-token 为项目下的token,可通过 gitlab -> project -> settings -> ci/cd 下获得
  • executor,使用 docker 作为 executor;
  • description 为 runner 的描述信息,请自定义;
  • docker-image 为 executor 所采用的自己构建的 docker 镜像,该镜像有maven、java、docker、bash环境(会被gitlab-ci.yml中的配置覆盖)。
  • docker-volumes ,因采用 “docker executor” ,必须挂载宿主机的docker.sock,
  • 挂载.m2文件夹,是为了避免maven每次编译项目时都重新下载jar包。(后面.gitlab-ci.yml文件中使用了maven镜像)

挂载.m2文件夹这一步很重要,不然每次下载依赖包会特别浪费时间

如果想要在docker in docker,也就是在docker容器中使用宿主机的docker,要挂载docker.sock文件,关于为什么要挂载docker.sock,可以参考这篇文章:docker系列-docker.sock探究

mrxccc/maven-jdk8-docker镜像的dockerfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM maven:3.6.0-jdk-8-alpine
MAINTAINER mrxccc@qq.com
ENV TZ=Asia/Shanghai
#安装docker
RUN echo http://dl-cdn.alpinelinux.org/alpine/latest-stable/community >> /etc/apk/repositories &&\
apk update &&\
apk add docker --no-cache &&\
apk add openrc --no-cache
#设置时区
RUN apk add --no-cache tzdata &&\
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone &&\
rm -rf /var/cache/apk/*
#安装sudo
RUN apk add sudo --no-cache
CMD service docker start

3.编写gitlab-ci

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
48
49
variables:
MAVEN_CLI_OPTS: "-s ./mvn-settings.xml --batch-mode"
SOFT_VERSION: '1.0'
SOFT_VERSION_EXT: 'alpha'
NEW_SOFT_VERSION_EXT: 'beta'

stages:
- verify
- build
- dockerpush

before_script:
- pwd


#单元测试
unit-test:
stage: verify
script:
- mvn $MAVEN_CLI_OPTS test
tags:
- runInDk

#java编译打包docker镜像
java-package:
stage: build
tags:
- runInDk
cache:
key: ${CI_PIPELINE_ID} #根据pipeline id缓存
paths:
- ${CI_PROJECT_DIR}
script:
#registry 是仓库名,imagename是镜像名,CI_PIPELINE_ID是pipeline id
- docker build registry/imagename-${CI_PIPELINE_ID}:$SOFT_VERSION

#push镜像
docker-push:
stage: dockerpush
tags:
- runInDk
cache:
key: ${CI_PIPELINE_ID}
paths:
- ${CI_PROJECT_DIR}
script:
#registry 是仓库名,imagename是镜像名,CI_PIPELINE_ID是pipeline id
- docker push registry/imagename-${CI_PIPELINE_ID}:$SOFT_VERSION

更多参数操作,可以用:sudo docker exec gitlab-runner gitlab-runner register –help 进行查看

更多精彩内容:mrxccc

docker系列-docker远程访问

docker配置远程访问

Docker Daemon 默认情况下是只允许本地访问的,不允许远程访问。本文将首先介绍 Docker Daemon 的连接方式,然后说明如何配置远程访问。即实现通过本地 docker 客户端访问远程主机的 docker 服务端,以此来监控远程主机上的 Docker 容器。

如何配置:

有两种方式:

第一种(推荐)

是使用 systemctl edit docker 来调用文本编辑器修改指定的单元或单元实例,ubuntu 默认调用的是 nano 编辑器,不是很好用,如果不熟悉 nano 编辑器的操作可以使用 vim 编辑器。

主要也就是新建或修改 /etc/systemd/system/docker.service.d/override.conf,其内容如下:

1
2
3
4
##Add this to the file for the docker daemon to use different ExecStart parameters (more things can be added here)
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd

解释一下:

默认情况下使用 systemd 时,docker.service 的设置为:ExecStart=/usr/bin/dockerd -H fd://,这将覆盖写到 daemon.json 中的任何 hosts 。通过在 override.conf 文件中将 ExecStart 仅仅定义为:ExecStart=/usr/bin/dockerd,这将会使用在 daemon.json 中设置的 hosts 。这个文件中的第一行ExecStart= 必须要有,因为它将用于清除默认的 ExecStart 参数。如果是修改 docker.service 的文件而不是创建 override.conf,那么下次 systemd 重启时,docker.service 文件也会被重新创建。

然后在 /etc/docker/daemon.json (没有就新建一个,下文统一简称 daemon.json)中写入以下内容

1
2
3
4
5
6
{
"hosts":[
"unix:///var/run/docker.sock",
"tcp://0.0.0.0:2375"
]
}

该文件必须符合 json 规范写法,否则 Docker 将不能启动

重新加载 daemon 并重启 docker 服务:

1
2
$ sudo systemctl daemon-reload
$ sudo systemctl restart docker.service

检查端口监听:

1
2
$ sudo netstat -ntlp |grep dockerd
tcp6 0 0 :::2375 :::* LISTEN 2439/dockerd

在远程主机上面通过 tcp socket 来访问本机的 Docker Daemon 服务:

1
2
3
$ docker -H  192.168.205.10:2375 images

$ docker -H 192.168.205.10:2375 ps

其中 192.168.1.130 是开放了远程访问的主机的 IP。

第二种

这种就很简单暴力,直接修改/lib/systemd/system/docker.service文件,注释掉默认的 ExecStart 并添加新的 ExecStart 配置:

1
2
# ExecStart=/usr/bin/dockerd -H fd://
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock

然后重启 docker.service:

1
2
$ sudo systemctl daemon-reload
$ sudo systemctl restart docker.service

这样 dockerd 就开始监听 tcp 端口 2375 了

Docker 与 Dockerd 的交互

Docker 客户端与 dockerd 之间就是通过 REST 的方式通信的。前面我们已经让 dockerd 监听 tcp 端口了,所以我们可以使用 curl 来代替 docker 客户端。这里我们简单的演示如何请求 dockerd 从 docker hub 上下载 hello-world 镜像:

1
$ curl '127.0.0.1:2375/images/create?fromImage=hello-world&tag=latest' -X POST
1
2
3
{"status":"Pulling from library/hello-world","id":"latest"}
{"status":"Digest: sha256:8c5aeeb6a5f3ba4883347d3747a7249f491766ca1caa47e5da5dfcf6b9b717c0"}
{"status":"Status: Image is up to date for hello-world:latest"}

如果去看看 Engine API,你会发现其它的请求也都是用类似方式发送的,更多API可以参考官方文档,目前最新的版本是v1.40:https://docs.docker.com/engine/api/v1.40/

更多精彩内容:mrxccc

docker系列-docker.sock探究

预备知识

搞清楚/var/run/docker.sock参数的前提是了解docker的client+server架构,如下图

可见在电脑上运行的docker由client和server组成,我们输入docker version命令实际上是通过客户端将请求发送到同一台电脑上的Doceker Daemon服务,由Docker Daemon返回信息,客户端收到信息后展示在控制台上。

可是这个又跟docker.sock有什么关系呢?别急,我们再了解一下docker一些比较重要的组件

Docker 的主要组件

安装 docker ,其实是安装了 docker 客户端、dockerd 等一系列的组件,其中比较重要的有下面几个。

Docker CLI(docker)
docker 程序是一个客户端工具,用来把用户的请求发送给 docker daemon(dockerd)。

该程序的安装路径为:

1
/usr/bin/docker

Dockerd
docker daemon(dockerd),一般也会被称为 docker engine。

该程序的安装路径为:

1
/usr/bin/dockerd

Containerd
在宿主机中管理完整的容器生命周期:容器镜像的传输和存储、容器的执行和管理、存储和网络等。

该程序的安装路径为:

1
/usr/bin/docker-containerd

Containerd-shim
它是 containerd 的组件,是容器的运行时载体,主要是用于剥离 containerd 守护进程与容器进程,引入shim,允许runc 在创建和运行容器之后退出,并将 shim 作为容器的父进程,而不是 containerd 作为父进程,这样做的目的是当 containerd 进程挂掉,由于 shim 还正常运行,因此可以保证容器不受影响。此外,shim 也可以收集和报告容器的退出状态,不需要 containerd 来 wait 容器进程。我们在 docker 宿主机上看到的 shim 也正是代表着一个个通过调用 containerd 启动的 docker 容器。

该程序的安装路径为:

1
/usr/bin/docker-containerd-shim

RunC
RunC 是一个轻量级的工具,它是用来运行容器的,容器作为 runC 的子进程开启,在不需要运行一个 Docker daemon 的情况下可以嵌入到其他各种系统,也就是说可以不用通过 docker 引擎,直接运行容器。docker是通过Containerd调用 runC 运行容器的

该程序的安装路径为:

1
/usr/bin/docker-runc

从 hello world 开始

我们通过hello-world镜像分析来进行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[vagrant@docker-host docker]$ docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/

For more examples and ideas, visit:
https://docs.docker.com/get-started/

上面的输出信息指出,hello-world 容器的运行经历了如下四步:

  1. Docker 客户端向 docker daemon 发送请求
  2. Docker daemon 从 Docker Hub 上拉取镜像
  3. Docker daemon 使用镜像运行了一个容器并产生了输出
  4. Docker daemon 把输出的内容发送给了 docker 客户端

这是一个很抽象也很容器理解的过程,但是我们还想知道更多:docker daemon 是如何创建并运行容器的?
其实容器部分的操作和管理都被 dockerd 外包给 containerd 了,下图描述了运行一个容器时各个组件之间的关系:

进入正题

Docker Daemon 的连接方式

  1. UNIX 域套接字

    默认就是这种方式, 会生成一个 /var/run/docker.sock 文件, UNIX 域套接字用于本地进程之间的通讯, 这种方式相比于网络套接字效率更高, 但局限性就是只能被本地的客户端访问。

  2. tcp 端口监听

    服务端开启端口监听 dockerd -H IP:PORT , 客户端通过指定IP和端口访问服务端 docker -H IP:PORT 。通过这种方式, 任何人只要知道了你暴露的ip和端口就能随意访问你的docker服务了, 这是一件很危险的事, 因为docker的权限很高, 不法分子可以从这突破取得服务端宿主机的最高权限。

什么是unix socket?

unix socket可以让一个程序通过类似处理一个文件的方式和另一个程序通信,这是一种进程间通信的方式(IPC)。
当你在host上安装并且启动好docker,docker daemon 会自动创建一个socket文件并且保存在/var/run/docker.sock目录下。docker daemon监听着socket中即将到来的链接请求(可以通过-H unix:///var/run/docker.sock设定docker daemon监听的socket文件,-H参数还可以设定监听tcp:port或者其它的unix socket),当一个链接请求到来时,它会使用标准IO来读写数据。

docker.sock 是docker client 和docker daemon 在localhost进行通信的socket文件。
可以直接call这个socket文件来拉去镜像,创建容器,启动容器等一系列操作。(其实就是直接call docker daemon API而不是通过docker client的方式去操控docker daemon)。

官方说明

我们从Docker Daemon进行,查看它的官方文档

上图是Docker Daemon的配置参数

翻译过来就是:–host=[]指定Docker守护程序将在何处侦听客户端连接。如果未指定,则默认为/var/run/docker.sock

所以docker客户端只要把请求发往这里,daemon就能收到并且做出响应。

按照上面的解释来推理:我们也可以向/var/run/docker.sock发送请求,也能达到docker psdocker images这样的效果;

验证

1.查看镜像:

  • docker-cli方式:

    1
    2
    3
    4
    5
    6
    [vagrant@docker-host ~]$ docker images
    REPOSITORY TAG IMAGE ID CREATED SIZE
    spring_demo latest b7c0355e0a01 6 days ago 151MB
    nginx latest f35646e83998 2 weeks ago 133MB
    hello-world latest bf756fb1ae65 9 months ago 13.3kB
    ampregistry:5000/sng-biz-base-alpine 2.1.0 54c2c81fffbe 17 months ago 137MB
  • 请求到Docker Daemon:

    该请求返回的是json串,使用| jq .是为了格式化json,方便查看

    jq是linux里面的一个json格式化工具,如果没有安装可以去掉后面的| jq .代码,拷贝返回的json串手动格式化也可以

    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
    48
    49
    50
    51
    52
    [vagrant@docker-host ~]$ curl -s --unix-socket /var/run/docker.sock http:/images/json | jq .
    [
    {
    "VirtualSize": 151316494,
    "Size": 151316494,
    "Containers": -1,
    "Created": 1603359433,
    "Id": "sha256:b7c0355e0a01f296e6ac94071b19c227839c8c19fecb2376dd8677ecbbe48017",
    "Labels": null,
    "ParentId": "sha256:4a5e4dd8fb964affb9750f01f66fb5f381988fcdbf8928902ce3f77b14fe990a",
    "RepoDigests": null,
    "RepoTags": [
    "spring_demo:latest"
    ],
    "SharedSize": -1
    },
    {
    "VirtualSize": 132861270,
    "Size": 132861270,
    "Containers": -1,
    "Created": 1602578384,
    "Id": "sha256:f35646e83998b844c3f067e5a2cff84cdf0967627031aeda3042d78996b68d35",
    "Labels": {
    "maintainer": "NGINX Docker Maintainers <docker-maint@nginx.com>"
    },
    "ParentId": "",
    "RepoDigests": [
    "nginx@sha256:ed7f815851b5299f616220a63edac69a4cc200e7f536a56e421988da82e44ed8"
    ],
    "RepoTags": [
    "nginx:latest"
    ],
    "SharedSize": -1
    },
    {
    "VirtualSize": 13336,
    "Size": 13336,
    "Containers": -1,
    "Created": 1578014497,
    "Id": "sha256:bf756fb1ae65adf866bd8c456593cd24beb6a0a061dedf42b26a993176745f6b",
    "Labels": null,
    "ParentId": "",
    "RepoDigests": [
    "hello-world@sha256:8c5aeeb6a5f3ba4883347d3747a7249f491766ca1caa47e5da5dfcf6b9b717c0"
    ],
    "RepoTags": [
    "hello-world:latest"
    ],
    "SharedSize": -1
    }
    ]

2.查看容器:

先运行一个nginx容器:docker run --name mynginx -p 80:80 -d nginx

  • docker-cli方式:

    1
    2
    CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                NAMES
    919b34a22018 nginx "/docker-entrypoint.…" About a minute ago Up About a minute 0.0.0.0:80->80/tcp mynginx
  • 请求Docker Daemon:

    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
    48
    49
    50
    [vagrant@docker-host ~]$ curl -s --unix-socket /var/run/docker.sock http:/containers/json | jq .
    [
    {
    "Mounts": [],
    "NetworkSettings": {
    "Networks": {
    "bridge": {
    "DriverOpts": null,
    "MacAddress": "02:42:ac:11:00:02",
    "GlobalIPv6PrefixLen": 0,
    "GlobalIPv6Address": "",
    "IPv6Gateway": "",
    "IPAMConfig": null,
    "Links": null,
    "Aliases": null,
    "NetworkID": "89d9f74c2884582696971298095e85c9c7df332a356c4b3a4dc09807eb6d457f",
    "EndpointID": "53d712df1ad9bd9093b8492c1abace5fe72785cd80189aa4c64d7652dff38490",
    "Gateway": "172.17.0.1",
    "IPAddress": "172.17.0.2",
    "IPPrefixLen": 16
    }
    }
    },
    "HostConfig": {
    "NetworkMode": "default"
    },
    "Status": "Up 11 minutes",
    "State": "running",
    "Id": "919b34a2201822a04fb1160c1c2369580d7b9ef9e7f1e9c77ad4da8761649fb9",
    "Names": [
    "/mynginx"
    ],
    "Image": "nginx",
    "ImageID": "sha256:f35646e83998b844c3f067e5a2cff84cdf0967627031aeda3042d78996b68d35",
    "Command": "/docker-entrypoint.sh nginx -g 'daemon off;'",
    "Created": 1603879410,
    "Ports": [
    {
    "Type": "tcp",
    "PublicPort": 80,
    "PrivatePort": 80,
    "IP": "0.0.0.0"
    }
    ],
    "Labels": {
    "maintainer": "NGINX Docker Maintainers <docker-maint@nginx.com>"
    }
    }
    ]

如果去看看 Engine API,你会发现其它的请求也都是用类似方式发送的,更多API可以参考官方文档,目前最新的版本是v1.40:https://docs.docker.com/engine/api/v1.40/

至此,我们对docker的client、server架构有了清楚的认识:Docker Daemon相当于一个server,监听来自/var/run/docker.sock的请求,然后做出各种响应,例如返回镜像列表,创建容器。

更多精彩内容:mrxccc

设计模式-工厂方法模式

工厂方法模式是为了弥补简单工厂模式的不足并且继承它的优点而延生出的一种设计模式,它能更好的符合开闭原则的要求。

1 模式介绍

1.1 定义

工厂方法模式,又称工厂模式、多态工厂模式和虚拟构造器模式,通过定义工厂父类负责定义创建对象的公共接口,而子类则负责生成具体的对象。

1.2 解决的问题

发现简单工厂模式存在一系列问题:
工厂类集中了所有实例(产品)的创建逻辑,一旦这个工厂不能正常工作,整个系统都会受到影响;
违背“开放 - 关闭原则”,一旦添加新产品就不得不修改工厂类的逻辑,这样就会造成工厂逻辑过于复杂。

1.3 举个例子:

大众汽车公司想必大家都不陌生,它旗下也有不少汽车品牌。大众汽车公司就好比一个汽车工厂,负责生产和销售汽车。它可以为客户提供一个客户需要的汽车。但是,如果客户需要的汽车大众公司目前还没有,但是公司想要盈利,就必须为此而设计汽车,在这种情况下,大众公司就要新添加一种汽车,同时要修改公司内部的生产环境(也就是工厂类的代码)。这就是简单工厂模式的运行情况。简单而言,就是工厂类(汽车公司)什么都要干,要修改必须大动干戈。因而一定程度上违背了开闭原则。而工厂方法模式则不一样,大众汽车公司不在总公司生产汽车,而是成立分公司,收购别的公司,成立具有针对性的汽车工厂专门生产对应的汽车。若客户的大量需求得不到满足,则总公司就另外成立新的二级公司(新品牌汽车的工厂)生产汽车,从而在不修改具体工厂的情况下引进新的产品。正如大众集团的收购一样。以下为简单工厂模式和工厂方法模式的区别:

如果使用简单工厂是这样的模式

如果使用工厂方法是这样的模式

1.4 模式组成

组成(角色) 关系 作用
抽象产品(Product) 具体产品的父类 描述具体产品的公共接口
具体产品(Concrete Product) 抽象产品的子类;工厂类创建的目标类 描述生产的具体产品
抽象工厂(Creator) 具体工厂的父类 描述具体工厂的公共接口
具体工厂(Concrete Creator) 抽象工厂的子类;被外界调用 描述具体工厂;实现FactoryMethod工厂方法创建产品的实例

2 实例讲解:

手机专卖店Store卖手机,目前Store卖有两种手机,一个苹果手机,一个索尼手机,此时如果想从手机工厂进货“小米”手机,那么手机总工厂就必须修改内部实现

简单工厂模式由SimplePhoneFactory,集中获取不同的手机实例对象。

工厂方法模式由PhoneFactory的两个实现,分别获取不同的手机实例对象。

2.1 使用步骤

步骤1: 创建抽象产品类 ,定义具体产品的公共抽象接口;

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
public abstract class Phone {
/**
* 品牌
*/
protected String brand;

/**
* 操作系统
*/
protected String os;

/**
* 充电
*/
public abstract void charge();

public String getBrand() {
return brand;
}

public void setBrand(String brand) {
this.brand = brand;
}

public String getOs() {
return os;
}

public void setOs(String os) {
this.os = os;
}
}

步骤2: 创建抽象工厂类,定义具体工厂的公共接口

1
2
3
public interface PhoneFactory {
Phone getPhone();
}

步骤3:创建具体工厂类(继承抽象工厂类),定义创建对应具体产品实例的方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 苹果手机工厂
public class ApplePhoneFactory implements PhoneFactory {
@Override
public ApplePhone getPhone() {
ApplePhone applePhone = new ApplePhone();
applePhone.setBrand("Apple");
return applePhone;
}
}

// 索尼手机工厂
public class SonyPhoneFactory implements PhoneFactory {
@Override
public SonyPhone getPhone() {
SonyPhone sonyPhone = new SonyPhone();
sonyPhone.setBrand("Sony");
return sonyPhone;
}
}
// 更多其他手机工厂。。。

步骤4: 创建具体产品类(继承抽象产品类), 定义生产的具体产品;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 苹果手机
public class ApplePhone extends Phone {
@Override
public void charge() {
System.out.println("普通充电");
}
}

// 索尼手机
public class SonyPhone extends Phone {
@Override
public void charge() {
System.out.println("快充");
}
}

步骤5:外界通过调用具体工厂类的方法,从而创建不同具体产品类的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 手机专卖店
public class StoreB {
private PhoneFactory phoneFactory;

public StoreB(PhoneFactory phoneFactory) {
super();
this.phoneFactory = phoneFactory;
}

/**
* 补充手机
*/
public void supplyPhone() {
Phone phone = phoneFactory.getPhone();
// 补充手机逻辑...
System.out.println("补充" + phone.getBrand() + "手机完成");
}

public static void main(String[] args) {
StoreB storeB = new StoreB(new SonyPhoneFactory());
storeB.supplyPhone();
}

}

结果:

1
补充Sony手机完成

优点:

  • 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程;
  • 在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则;

缺点:

  • 每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度

总结:工厂模式可以说是简单工厂模式的进一步抽象和拓展,在保留了简单工厂的封装优点的同时,让扩展变得简单,让继承变得可行,增加了多态性的体现。

源码:设计模式

其他设计模式介绍:

设计模式-工厂方法模式

设计模式-抽象工厂模式

设计模式-建造者模式

更多精彩内容:mrxccc

Docker系列-容器的相关操作

容器是 Docker 又一核心概念。
简单的说,容器是独立运行的一个或一组应用,以及它们的运行态环境。对应的,虚拟机可以理解为模拟运行的一整套操作系统(提供了运行态环境和其他系统环境)和跑在上面的应用。

常用容器命令

  • 列出当前所有容器:docker ps [OPTIONS]
  • 新建并启动容器:docker run [OPTIONS] 镜像名或镜像ID [COMMAND] [ARG]
  • 启动容器:docker start 容器ID或容器名
  • 重启容器:docker restart 容器ID或容器名
  • 停止容器:docker stop 容器ID或容器名
  • 删除已容器:docker rm容器ID或容器名
  • 查看容器日志:docker logs -f –t 容器名或容器ID

新建并启动容器

所需要的命令主要为 docker run。

例如,下面的命令输出一个 “Hello World”,之后终止容器。

1
2
$ docker run ubuntu:14.04 /bin/echo 'Hello world'
Hello world

这跟在本地直接执行 /bin/echo ‘hello world’ 几乎感觉不出任何区别。

一行命令启动一个nginx

1
docker run --name my-nginx -p 80:80 -d nginx

参数说明:

  • –name nginx-test:容器名称。
  • -p 8080:80: 端口进行映射,将本地 8080 端口映射到容器内部的 - 80 端口。
  • -d nginx: 设置容器在在后台一直运行。

进入容器

在使用 -d 参数时,容器启动后会进入后台。

某些时候需要进入容器进行操作,包括使用 docker attach 命令或 docker exec 命令,推荐大家使用 docker exec 命令,原因会在下面说明。

下面的命令则启动一个 bash 终端,允许用户进行交互。

1.attach 命令

docker attach 是 Docker 自带的命令。下面示例如何使用该命令。

1
2
3
4
5
6
7
8
9
$ docker run -dit ubuntu
243c32535da7d142fb0e6df616a3c3ada0b8ab417937c853a9e1c251f499f550

$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
243c32535da7 ubuntu:latest "/bin/bash" 18 seconds ago Up 17 seconds nostalgic_hypatia

$ docker attach 243c
root@243c32535da7:/#

!!!注意: 如果从这个 容器中 exit,会导致容器的停止。 如果想正常退出但不关闭容器,请按 Ctrl+P+Q进行退出容器

2.exec 命令

-i -t 参数
docker exec 后边可以跟多个参数,这里主要说明 -i -t 参数。

其中,-t 选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上, -i 则让容器的标准输入保持打开。
只用 -i 参数时,由于没有分配伪终端,界面没有我们熟悉的 Linux 命令提示符,但命令执行结果仍然可以返回。

当 -i -t 参数一起使用时,则可以看到我们熟悉的 Linux 命令提示符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ docker run -dit ubuntu
69d137adef7a8a689cbcb059e94da5489d3cddd240ff675c640c8d96e84fe1f6

$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
69d137adef7a ubuntu:latest "/bin/bash" 18 seconds ago Up 17 seconds zealous_swirles

$ docker exec -i 69d1 bash
ls
bin
boot
dev
...

$ docker exec -it 69d1 bash
root@69d137adef7a:/#

如果从这个 容器 中 exit,不会导致容器的停止。这就是为什么推荐大家使用 docker exec 的原因。

更多参数说明请使用 docker exec –help 查看。

Docker 删除容器

可以使用 docker container rm 来删除一个处于终止状态的容器。例如

1
2
$ docker container rm  trusting_newton
trusting_newton

如果要删除一个运行中的容器,可以添加 -f 参数。Docker 会发送 SIGKILL 信号给容器。

清理所有处于终止状态的容器

用 docker container ls -a 命令可以查看所有已经创建的包括终止状态的容器,如果数量太多要一个个删除可能会很麻烦,用下面的命令可以清理掉所有处于终止状态的容器。

1
$ docker container prune

当利用 docker run 来创建容器时,Docker 在后台运行的标准操作包括:

  • 检查本地是否存在指定的镜像,不存在就从公有仓库下载
  • 利用镜像创建并启动一个容器
  • 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
  • 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
  • 从地址池配置一个 ip 地址给容器
  • 执行用户指定的应用程序
  • 执行完毕后容器被终止

更多精彩内容:mrxccc

Docker系列-镜像的相关操作

获取镜像

Docker Hub 上有大量的高质量的镜像可以用,这里我们就说一下怎么获取这些镜像。

从 Docker 镜像仓库获取镜像的命令是 docker pull。其命令格式为:

1
docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]

具体的选项可以通过 docker pull --help 命令看到,这里我们说一下镜像名称的格式。

  • Docker 镜像仓库地址:地址的格式一般是 <域名/IP>[:端口号]。默认地址是 Docker Hub。
  • 仓库名:如之前所说,这里的仓库名是两段式名称,即 <用户名>/<软件名>。对于 Docker Hub,如果不给出用户名,则默认为 library,也就是官方镜像。
    比如:
1
2
3
4
5
6
7
8
9
$ docker pull ubuntu:16.04
16.04: Pulling from library/ubuntu
bf5d46315322: Pull complete
9f13e0ac480c: Pull complete
e8988b5b3097: Pull complete
40af181810e7: Pull complete
e6f7c7e5c03e: Pull complete
Digest: sha256:147913621d9cdea08853f6ba9116c2e27a3ceffecf3b492983ae97c3d643fbbe
Status: Downloaded newer image for ubuntu:16.04

列出镜像

要想列出已经下载下来的镜像,可以使用 docker image ls或者 docker images命令。

1
2
3
4
5
6
7
8
9
10
11
12
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
flask-hello latest 2921286533e1 17 minutes ago 924MB
python 3.6 2dfb6d103623 5 days ago 914MB
redis latest 987b78fc9e38 6 days ago 104MB
caijiacheng0707/hello-c latest eca19e80e3c4 13 days ago 861kB
wordpress latest fd5f88e17621 2 weeks ago 541MB
mysql 5.7 f965319e89de 3 weeks ago 448MB
mysql latest a7a67c95e831 3 weeks ago 541MB
nginx latest 602e111c06b6 4 weeks ago 127MB
python 2.7-alpine 8579e446340f 4 weeks ago 71.1MB
arm32v7/python 2.7-slim b532061a7f5b 4 weeks ago 107MB

列表包含了 仓库名、标签、镜像 ID、创建时间 以及 所占用的空间。

其中仓库名、标签在之前的基础概念章节已经介绍过了。镜像 ID 则是镜像的唯一标识,一个镜像可以对应多个标签。

镜像体积

如果仔细观察,会注意到,这里标识的所占用空间和在 Docker Hub 上看到的镜像大小不同。比如,ubuntu:16.04 镜像大小,在这里是 127 MB,但是在 Docker Hub 显示的却是 50 MB。这是因为 Docker Hub 中显示的体积是压缩后的体积。在镜像下载和上传过程中镜像是保持着压缩状态的,因此 Docker Hub 所显示的大小是网络传输中更关心的流量大小。而 docker image ls 显示的是镜像下载到本地后,展开的大小,准确说,是展开后的各层所占空间的总和,因为镜像到本地后,查看空间的时候,更关心的是本地磁盘空间占用的大小。

另外一个需要注意的问题是,docker image ls 列表中的镜像体积总和并非是所有镜像实际硬盘消耗。由于 Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于 Docker 使用 Union FS,相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。

你可以通过以下命令来便捷的查看镜像、容器、数据卷所占用的空间。

1
2
3
4
5
6
$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 10 3 2.59GB 2.418GB (93%)
Containers 4 2 127.4kB 0B (0%)
Local Volumes 5 1 478.1MB 478.1MB (100%)
Build Cache 0 0 0B 0B

过滤器参数

docker images 还支持强大的过滤器参数 –filter,或者简写 -f。之前我们已经看到了使用过滤器来列出虚悬镜像的用法,它还有更多的用法。比如,我们希望看到在 mysql:5.7 之后建立的镜像,可以用下面的命令:

1
2
3
4
5
6
7
$ docker image ls -f since=mysql:5.7
REPOSITORY TAG IMAGE ID CREATED SIZE
flask-hello latest 2921286533e1 15 minutes ago 924MB
python 3.6 2dfb6d103623 5 days ago 914MB
redis latest 987b78fc9e38 6 days ago 104MB
caijiacheng0707/hello-c latest eca19e80e3c4 13 days ago 861kB
wordpress latest fd5f88e17621 2 weeks ago 541MB

想查看某个位置之前的镜像也可以,只需要把 since 换成 before 即可。

1
2
3
4
5
6
$ docker image ls -f before=mysql:5.7
REPOSITORY TAG IMAGE ID CREATED SIZE
mysql latest a7a67c95e831 3 weeks ago 541MB
nginx latest 602e111c06b6 4 weeks ago 127MB
python 2.7-alpine 8579e446340f 4 weeks ago 71.1MB
arm32v7/python 2.7-slim b532061a7f5b 4 weeks ago 107MB

此外,如果镜像构建时,定义了 LABEL,还可以通过 LABEL 来过滤。
默认情况下,docker image ls 会输出一个完整的表格,但是我们并非所有时候都会需要这些内容。比如,刚才删除虚悬镜像的时候,我们需要利用 docker image ls 把所有的虚悬镜像的 ID 列出来,然后才可以交给 docker image rm 命令作为参数来删除指定的这些镜像,这个时候就用到了 -q 参数

1
2
3
4
5
6
7
8
$ docker image ls -q
5f515359c7f8
05a60462f8ba
fe9198c04d62
00285df0df87
f753707788c5
f753707788c5
1e0c3dd64ccd

–filter 配合 -q 产生出指定范围的 ID 列表,然后送给另一个 docker 命令作为参数,从而针对这组实体成批的进行某种操作的做法在 Docker 命令行使用过程中非常常见,不仅仅是镜像,将来我们会在各个命令中看到这类搭配以完成很强大的功能。因此每次在文档看到过滤器后,可以多注意一下它们的用法。

另外一些时候,我们可能只是对表格的结构不满意,希望自己组织列;或者不希望有标题,这样方便其它程序解析结果等,这就用到了 Go 的模板语法。

比如,下面的命令会直接列出镜像结果,并且只包含镜像ID和仓库名:

1
2
3
4
5
6
7
8
$ docker image ls --format "{{.ID}}: {{.Repository}}"
5f515359c7f8: redis
05a60462f8ba: nginx
fe9198c04d62: mongo
00285df0df87: <none>
f753707788c5: ubuntu
f753707788c5: ubuntu
1e0c3dd64ccd: ubuntu

或者打算以表格等距显示,并且有标题行,和默认一样,不过自己定义列:

1
2
3
4
5
6
7
8
9
10
11


$ docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}"
IMAGE ID REPOSITORY TAG
5f515359c7f8 redis latest
05a60462f8ba nginx latest
fe9198c04d62 mongo 3.2
00285df0df87 <none> <none>
f753707788c5 ubuntu 16.04
f753707788c5 ubuntu latest
1e0c3dd64ccd ubuntu 14.04

Docker 删除本地镜像

如果要删除本地的镜像,可以使用 docker image rm 命令,其格式为:

1
docker image rm [选项] <镜像1> [<镜像2> ...]

像其它可以承接多个实体的命令一样,可以使用 docker image ls -q 来配合使用 docker image rm,这样可以成批的删除希望删除的镜像。我们在“镜像列表”章节介绍过很多过滤镜像列表的方式都可以拿过来使用。

比如,我们需要删除所有仓库名为 redis 的镜像:

1
docker image rm $(docker image ls -q redis)

或者删除所有在 mysql:5.7 之前的镜像:

1
docker image rm $(docker image ls -q -f before=mysql:5.7)

虚悬镜像

有时候在镜像列表中,还可以看到一个特殊的镜像,这个镜像既没有仓库名,也没有标签,均为 。:

1
<none>               <none>              00285df0df87        5 days ago          342 MB

一般有两种会产生虚悬镜像:
dockers pull: 原来有一个镜像 mongo:3.2,随着官方镜像维护,发布了新版本后,重新 docker pull mongo:3.2 时,mongo:3.2 这个镜像名被转移到了新下载的镜像身上,而旧的镜像上的这个名称则被取消,从而成为了
docker build:也同样可以导致这种现象。由于新旧镜像同名,旧镜像名称被取消,从而出现仓库名、标签均为 的镜像。这类无标签镜像也被称为 虚悬镜像(dangling image)

显示虚悬镜像

1
2
3
$ docker image ls -f dangling=true
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 00285df0df87 5 days ago 342 MB

删除虚悬镜像

一般来说,虚悬镜像已经失去了存在的价值,是可以随意删除的,可以用下面的命令删除。

1
docker rmi $(docker images -q -f dangling=true)

更多精彩内容:mrxccc

Docker系列-docker的三个基本概念

Docker 三个基本概念

镜像(Image)
容器(Container)
仓库(Repository)

Docker 引擎是一个包含以下主要组件的客户端服务器应用程序。

一种服务器,它是一种称为守护进程并且长时间运行的程序。
REST API用于指定程序可以用来与守护进程通信的接口,并指示它做什么。
一个有命令行界面 (CLI) 工具的客户端。
Docker 引擎组件的流程如下图所示:

在这里插入图片描述

2 Docker 系统架构

Docker 使用客户端-服务器 (C/S) 架构模式,使用远程 API 来管理和创建 Docker 容器。

Docker 容器通过 Docker 镜像来创建。

容器与镜像的关系类似于面向对象编程中的对象与类。

Docker 面向对象
容器 对象
镜像

3 Image (镜像)

那么镜像到底是什么呢?

Docker 镜像可以看作是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。(每个镜像都由很多层次构成,Docker 使用Union FS将这些不同的层结合到一个镜像中去)

镜像(Image)就是一堆只读层(read-only layer)的统一视角,也许这个定义有些难以理解,下面的这张图能够帮助读者理解镜像的定义。
在这里插入图片描述
从左边我们看到了多个只读层,它们重叠在一起。除了最下面一层,其它层都会有一个指针指向下一层。这些层是Docker 内部的实现细节,并且能够在主机的文件系统上访问到。统一文件系统 (union file system) 技术能够将不同的层整合成一个文件系统,为这些层提供了一个统一的视角,这样就隐藏了多层的存在,在用户的角度看来,只存在一个文件系统。我们可以在图片的右边看到这个视角的形式。

Container (容器)
容器 (container) 的定义和镜像 (image) 几乎一模一样,也是一堆层的统一视角,唯一区别在于容器的最上面那一层是可读可写的。
在这里插入图片描述
由于容器的定义并没有提及是否要运行容器,所以实际上,容器 = 镜像 + 读写层。

4 Repository (仓库)

Docker 仓库是集中存放镜像文件的场所。镜像构建完成后,可以很容易的在当前宿主上运行,但是, 如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry (仓库注册服务器)就是这样的服务。有时候会把仓库 (Repository) 和仓库注册服务器 (Registry) 混为一谈,并不严格区分。Docker 仓库的概念跟 Git 类似,注册服务器可以理解为 GitHub 这样的托管服务。实际上,一个 Docker Registry 中可以包含多个仓库 (Repository) ,每个仓库可以包含多个标签 (Tag),每个标签对应着一个镜像。所以说,镜像仓库是 Docker 用来集中存放镜像文件的地方类似于我们之前常用的代码仓库。

通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本 。我们可以通过<仓库名>:<标签>的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签.。

仓库又可以分为两种形式:

  • public(公有仓库)
  • private(私有仓库)

Docker Registry 公有仓库是开放给用户使用、允许用户管理镜像的 Registry 服务。一般这类公开服务允许用户免费上传、下载公开的镜像,并可能提供收费服务供用户管理私有镜像。

除了使用公开服务外,用户还可以在本地搭建私有 Docker Registry 。Docker 官方提供了 Docker Registry 镜像,可以直接使用做为私有 Registry 服务。当用户创建了自己的镜像之后就可以使用 push 命令将它上传到公有或者私有仓库,这样下次在另外一台机器上使用这个镜像时候,只需要从仓库上 pull 下来就可以了。

我们主要把 Docker 的一些常见概念如 Image , Container , Repository 做了详细的阐述,也从传统虚拟化方式的角度阐述了 docker 的优势,我们从下图可以直观地看到 Docker 的架构:
在这里插入图片描述

Docker 使用 C/S 结构,即客户端/服务器体系结构。 Docker 客户端与 Docker 服务器进行交互,Docker服务端负责构建、运行和分发 Docker 镜像。 Docker 客户端和服务端可以运行在一台机器上,也可以通过 RESTful 、 stock 或网络接口与远程 Docker 服务端进行通信。

最后通过一个表格总结一下:

标题 说明
镜像(Images) Docker 镜像是用于创建 Docker 容器的模板。
容器(Container) 容器是独立运行的一个或一组应用。
客户端(Client) Docker 客户端通过命令行或者其他工具使用 Docker API (https://docs.docker.com/reference/api/docker_remote_api) 与 Docker 的守护进程通信。
主机(Host) 一个物理或者虚拟的机器用于执行 Docker 守护进程和容器。
仓库(Registry) Docker 仓库用来保存镜像,可以理解为代码控制中的代码仓库。Docker Hub(https://hub.docker.com) 提供了庞大的镜像集合供使用。
Docker Machine Docker Machine是一个简化Docker安装的命令行工具,通过一个简单的命令行即可在相应的平台上安装Docker,比如VirtualBox、 Digital Ocean、Microsoft Azure。

更多精彩内容:mrxccc