Jinva's Blog

记录与分享

Article Template

📝 主旨内容

观点1

引用的话语

观点2

引用的话语

🤗 总结归纳

总结文章的内容

📎 参考文章

  • 一些引用
  • 引用文章

赞赏


Laravel Eloquent 模型关联速查表

一张 Laravel 的 Eloquent ORM 5.5 速查表

一对一关联

细节展示

在这个展示中,我们有 2 个模型(所有者和汽车)及两张表 (所有者和汽车)。

商业逻辑:

一个所有者可以拥有一台车。
一台车可以属于一个所有者。

关联图:

![](/images/blog/Untitled 1-20261027142052-f3twjtz.png)

关联细节:

汽车表必须储存所有者 ID。

Eloquent模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Owner
{
  public function car()
  {
      return $this->hasOne(Car::class);
  }
}
class Car
{
  public function owner()
  {
      return $this->belongsTo(Owner::class);
  }
}

数据库迁移:

1
2
3
4
5
6
7
8
9
10
Schema::create('owners', function (Blueprint $table) {
  $table->increments('id');
  $table->string('name');
});
Schema::create('cars', function (Blueprint $table) {
  $table->increments('id');
  $table->string('name');
  $table->integer('owner_id')->unsigned()->index()->nullable();
  $table->foreign('owner_id')->references('id')->on('owners');
});

储存记录

1
2
3
4
// 在 Owner 及 Car 之间建立关联
$owner->car()->save($car);
// 在 Car 及 Owner 之間建立关联
$car->owner()->associate($owner)->save();

获取记录

1
2
3
4
// 获取 Owner Car
$owner->car;
// 获取 Car Owner
$car->owner;

一对多关联

示例细节:

在此示例中,我们有两个模型:Thief(小偷)和 Car(车),和两张表:
thieves 和 cars。

业务规则:

小偷可以投走多辆车。
车只能被一个小偷偷走。

关系图:

![](/images/blog/Untitled 2-20261027142052-kwjhrnh.png)

关系细节:

车辆表应该储存小偷的 ID。

Eloquent 模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Thief
{
  public function cars()
  {
      return $this->hasMany(Car::class);
  }
}
class Car
{
  public function thief()
  {
      return $this->belongsTo(Thief::class);
  }
}

数据迁移:

1
2
3
4
5
6
7
8
9
10
Schema::create('thieves', function (Blueprint $table) {
  $table->increments('id');
  $table->string('name');
});
Schema::create('cars', function (Blueprint $table) {
  $table->increments('id');
  $table->string('name');
  $table->integer('thief_id')->unsigned()->index()->nullable();
  $table->foreign('thief_id')->references('id')->on('thieves');
});

储存记录:

1
2
3
4
5
6
7
8
9
// 创建介于小偷与车之间的关联。
$thief->cars()->saveMany([
  $car1,
  $car2,
]);
// 或者在单一模型调用 save() 方法。
$thief->cars()->save($car);
// 创建介于 Car 和 Thief 的关联。
$car->thief()->associate($thief)->save();

检索记录:

1
2
3
4
// 获取小偷所偷的车
$thief->cars;
// 获取偷盗某辆车的小偷
$car->thief;

多态一对多关系

演示细节:

在这个演示中,我们有三个模型(男,女和车),和三张表(男,女和车)。

业务规则:

一个男人(买家)可以买很多汽车。
一个女人(买家)可以买很多汽车。
这个汽车可以被一个买家购买(男人或女人)。

关系图:

![](/images/blog/Untitled 3-20261027142052-1yjyy4y.png)

Eloquent模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Man
{
  public function cars()
  {
      return $this->morphMany(Car::class, 'buyer');
  }
}
class Woman
{
  public function cars()
  {
      return $this->morphMany(Car::class, 'buyer');
  }
}
class Car
{
  public function buyer()
  {
      return $this->morphTo();
  }
}

数据库迁移:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Schema::create('men', function (Blueprint $table) {
  $table->increments('id');
  $table->string('name');
});
Schema::create('women', function (Blueprint $table) {
  $table->increments('id');
  $table->string('name');
});
Schema::create('cars', function (Blueprint $table) {
  $table->increments('id');
  $table->string('name');
  $table->integer('buyer_id')->unsigned()->index()->nullable();
  $table->string('buyer_type')->nullable();
  // 或者使用 $table->morphs(‘buyer’); 代替 "buyer_id" 和 "buyer_type"
});

储存记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 在买家 (男人 / 女人) 和汽车之间建立联系。
$man->cars()->saveMany([
  $car1,
  $car2,
]);
$woman->cars()->saveMany([
  $car1,
  $car2,
]);
// 或者为单个模型使用 save() 函数。
$man->cars()->save($car);
$woman->cars()->save($car);
//创建汽车与买家之间的关系 ( 男人/女人 )。
$car1->buyer()->associate($man)->save();
$car2->buyer()->associate($woman)->save();

检索记录:

1
2
3
4
5
// 获取买家 (男人 / 女人)的汽车
$men->cars
$women->cars
// 获取这辆车的买家 (男人 / 女人)
$car->buyer

多对多关联

示例展示:

在这个示例中,我们有两个模型(司机和汽车),和三张表
(司机,汽车和名为car_driver的中间关联表)。

业务规则:

一个司机可以驾驶很多辆车。
一辆车可以被很多个司机驾驶。

关系图:

![](/images/blog/Untitled 4-20261027142052-wjfv6qm.png)

关联详情:

关联表 “car_driver” 应该保存驾驶员ID和汽车ID。

关联模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Driver
{
  public function cars()
  {
      return $this->belongsToMany(Car::class);
  }
}
class Car
{
  public function drivers()
  {
      return $this->belongsToMany(Driver::class);
  }
}

数据库迁移:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Schema::create('drivers', function (Blueprint $table) {
  $table->increments('id');
  $table->string('name');
});
Schema::create('cars', function (Blueprint $table) {
  $table->increments('id');
  $table->string('name');
});
Schema::create('car_driver', function (Blueprint $table) {
  $table->increments('id');
  $table->integer('car_id')->unsigned()->index();
  $table->foreign('car_id')->references('id')->on('cars')->onDelete('cascade');
  $table->integer('driver_id')->unsigned()->index();
  $table->foreign('driver_id')->references('id')->on('drivers')->onDelete('cascade');
});

储存记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//创建Driver和Car之间的关联。
$driver->cars()->attach([
  $car1->id,
  $car2->id,
]);
//或者使用sync()函数防止重复关联。
$driver->cars()->sync([
  $car1->id,
  $car2->id,
]);
//创建Car和Driver之间的关联。
$car->drivers()->attach([
  $driver1->id,
  $driver2->id,
]);
//或者使用sync()函数防止重复关联。
$car->drivers()->sync([
  $driver1->id,
  $driver2->id,
]);

检索记录:

1
2
3
4
// Get Driver Car
$driver->cars
// Get Car Drivers
$car->drivers

多对多多态关联

展示细节:

在这个展示中我们有三个模型(代客,所有者及汽车)和4张表(代客,车主,汽车及司机)。

商业逻辑:

一个代驾(司机)可以驾驶很多车辆
一个车主(司机)可以驾驶很多
车车一台车可以被很多个司机驾驶(代驾和/或车主)

关联图:

![](/images/blog/Untitled 5-20261027142052-0r0898p.png)

关联细节:

中间表「司机」应该储存驾驶人ID,驾驶人类型及车辆ID。
驾驶是一个模型集合的代称(代客及所有者),而且不限定两个模型。驾驶人类型是模型的真正名称。

Eloquent模型:

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
class Valet
{
  public function cars()
  {
      return $this->morphToMany(Car::class, 'driver');
  }
}
class Owner
{
  public function cars()
  {
      return $this->morphToMany(Car::class, 'driver');
  }
}
class Car
{
  public function valets()
  {
      return $this->morphedByMany(Valet::class, 'driver');
  }

  public function owners()
  {
      return $this->morphedByMany(Owner::class, 'driver');
  }
}

数据库迁移:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Schema::create('valets', function (Blueprint $table) {
  $table->increments('id');
  $table->string('name');
});
Schema::create('owners', function (Blueprint $table) {
  $table->increments('id');
  $table->string('name');
});
Schema::create('drivers', function (Blueprint $table) {
  $table->increments('id');
  $table->integer('driver_id')->unsigned()->index();
  $table->string('driver_type');
  // 或使用 $table->morphs(‘driver’) 來取代「driver_id」和「driver_type」
  $table->integer('car_id')->unsigned()->index();
  $table->foreign('car_id')->references('id')->on('cars')->onDelete('cascade');
});

储存记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 在 driver(Valet / Owner)和 Car 間建立關聯
$valet->cars()->saveMany([$car1, $car2]);
$owner->cars()->saveMany([$car1, $car2]);
// 或使用 save() 方法來儲存單一模型
$valet->cars()->save($car1);
$owner->cars()->save($car1);
// 在 Car 和 driver(Valet / Owner)間建立關聯
$car->valets()->attach([
  $valet1->id,
  $valet2->id,
]);
$car->owners()->attach([
  $owner1->id,
  $owner2->id,
]);
// 或是用 sync() 方法來避免重複關聯
$car->valets()->sync([
  $valet1->id,
  $valet2->id,
]);
$car->owners()->sync([
  $owner1->id,
  $owner2->id,
]);

检索记录:

1
2
3
4
5
6
7
// 取得 driver(Valet / Owner)的 Cars
$valet->cars
$owner->cars

// 取得 Car 的 drivers(Valet 及 Owner)
$car->owners
$car->valets

![](/images/blog/Untitled 6-20261027142052-o6aw58v.png)

原文作者的 Twitter Mahmoud Zalt

原文地址:​https://hackernoon.com/eloquent-relationships-cheat-sheet-5155498c209

赞赏


Ubuntu-Server系统swappiness参数在内存与交换分区间优化

swappiness 值的大小对如何使用swap分区是有着很大的联系。swappiness=0 的时候表示最大限度使用物理内存,然后才是 swap空间,swappiness=100 的时候表示积极的使用swap分区,并且把内存上的数据及时的搬运到swap空间里面。linux的基本默认设置为60,具体如下:

也就是说,你的内存在使用到100-60=40%的时候,就开始出现有交换分区的使用。大家知道,内存的速度会比磁盘快很多,这样子会加大系统 IO,同时造的成大量页的换进换出,严重影响系统的性能,所以我们在操作系统层面,要尽可能使用内存,对该参数进行调整。

临时调整的方法如下,我们调成10:

1
$ sysctl vm.swappiness=10

这只是临时调整的方法,重启后会回到默认设置的

要想永久调整的话,需要在 /etc/sysctl.conf 修改,追加内容:

1
vm.swappiness=10

使其生效:

1
sudo sysctl -p

这样便设置完成了。

赞赏


MySQL慢查询优化、索引优化、以及表等优化总结

MySQL 优化概述

MySQL数据库常见的两个瓶颈是:CPU和I/O的瓶颈。

CPU在饱和的时候一般发生在数据装入内存或从磁盘上读取数据时候。

磁盘I/O瓶颈发生在装入数据远大于内存容量的时候,如果应用分布在网络上,那么查询量相当大的时候那么平瓶颈就会出现在网络上。

我们可以用mpstat, iostat, sar和vmstat来查看系统的性能状态。除了服务器硬件的性能瓶颈,对于MySQL系统本身,我们可以使用工具来优化数据库的性能。

MySQL 优化方案

Mysql的优化,大体可以分为三部分:索引的优化,sql语句的优化,表的优化

索引优化

1. 索引

一般的应用系统,读写比例在10:1左右,而且插入操作和一般的更新操作很少出现性能问题,在生产环境中,我们遇到最多的也是最容易出现问题的,还是一些复杂的查询操作,因此对查询语句的优化是重中之重,加速查询最好的方法就是索引。

索引:简单的说,相当于图书的目录,可以帮助用户快速的找到需要的内容。

在MySQL中也叫做“键”,是存储引擎用于快速找到记录的一种数据结构。能够大大提高查询效率。特别是当数据量非常大,查询涉及多个表时,使用索引往往能使查询速度加快成千上万倍。

总结:索引的目的在于提高查询效率,与我们查询图书所用的目录是一个道理:先定位到章,然后定位到该章下的一个小结,然后找到页数。相似的例子还有:查字典,查地图等。

2. 索引类型

  1. 普通索引

是最基本的索引,它没有任何限制。

  1. 唯一索引

与前面的普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。

  1. 主键索引

指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。

  1. 全文索引

主要用来查找文本中的关键字,而不是直接与索引中的值相比较。fulltext索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的where语句的参数匹配。fulltext索引配合match against操作使用,而不是一般的where语句加like。它可以在create table,alter table ,create index使用,不过目前只有char、varchar,text 列上可以创建全文索引。值得一提的是,在数据量较大时候,现将数据放入一个没有全局索引的表中,然后再用CREATE index创建fulltext索引,要比先为一张表建立fulltext然后再将数据写入的速度快很多。

3. 索引优化

  • 只要列中含有NULL值,就最好不要在此例设置索引,复合索引如果有NULL值,此列在使用时也不会使用索引
  • 尽量使用短索引,如果可以,应该制定一个前缀长度
  • 对于经常在where子句使用的列,最好设置索引,这样会加快查找速度
  • 对于有多个列where或者order by子句的,应该建立复合索引
  • 对于like语句,以%或者‘-’开头的不会使用索引,以%结尾会使用索引
  • 尽量不要在列上进行运算(函数操作和表达式操作)
  • 尽量不要使用not in和<>操作

SQL 慢查询的优化

![](/images/blog/Untitled 1-20261027142052-t41idmt.png)

1. 如何捕获低效sql

1)slow_query_log

这个参数设置为ON,可以捕获执行时间超过一定数值的SQL语句。

2)ong_query_time

当SQL语句执行时间超过此数值时,就会被记录到日志中,建议设置为1或者更短。

3)slow_query_log_file

记录日志的文件名。

4)log_queries_not_using_indexes

这个参数设置为ON,可以捕获到所有未使用索引的SQL语句,尽管这个SQL语句有可能执行得挺快。

2. 慢查询优化的基本步骤

  1. 先运行看看是否真的很慢,注意设置SQL_NO_CACHE
  2. where条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的where都应用到表中返回的记录数最小的表开始查起,单表每个字段分别查询,看哪个字段的区分度最高
  3. explain查看执行计划,是否与1预期一致(从锁定记录较少的表开始查询)
  4. order by limit 形式的sql语句让排序的表优先查
  5. 了解业务方使用场景
  6. 加索引时参照建索引的几大原则
  7. 观察结果,不符合预期继续从1开始分析

3.优化原则

  • 查询时,能不要就不用,尽量写全字段名
  • 大部分情况连接效率远大于子查询
  • 多使用explain和profile分析查询语句
  • 查看慢查询日志,找出执行时间长的sql语句优化
  • 多表连接时,尽量小表驱动大表,即小表 join 大表
  • 在千万级分页时使用limit
  • 对于经常使用的查询,可以开启缓存

数据库表优化

  • 表的字段尽可能用NOT NULL
  • 字段长度固定的表查询会更快
  • 把数据库的大表按时间或一些标志分成小表
  • 将表拆分

数据表拆分:主要就是垂直拆分和水平拆分。

水平切分:将记录散列到不同的表中,各表的结构完全相同,每次从分表中查询, 提高效率。

垂直切分:将表中大字段单独拆分到另外一张表, 形成一对一的关系。

总结:

Mysql的优化主要就在于:索引的优化,sql语句的优化,表的优化,在高并发网络环境下,除了优化数据库外,还会涉及到分布式缓存,CDN,数据库读写分离等高并发优化技术。

以上就是MySQL的优化技术方案,以下是最新阿里P8架构师谈架构设计系列。

赞赏


实用 Docker 速查大全:快速认识 Docker 的概念和使用方法

为何使用 Docker

「通过 Docker,开发者可以使用任何语言任何工具创建任何应用。“Dockerized” 的应用是完全可移植的,能在任何地方运行 - 不管是同事的 OS X 和 Windows 笔记本,或是在云端运行的 Ubuntu QA 服务,还是在虚拟机运行的 Red Hat 产品数据中心。

Docker Hub 上有 13000+ 的应用,开发者可以从中选取一个进行快速扩展开发。Docker 跟踪管理变更和依赖关系,让系统管理员能更容易理解开发人员是如何让应用运转起来的。而开发者可以通过 Docker Hub 的共有 / 私有仓库,构建他们的自动化编译,与其他合作者共享成果。

Docker 帮助开发者更快地构建和发布高质量的应用。」—— 什么是 Docker

系统环境

我用的是 Oh My Zsh 和 Docker 插件,它可以自动补全 Docker 命令。你的环境可能有所不同。

Linux

Docker 对于 Linux 内核版本的 最低要求 为 ​3.10.x。

MacOS

10.8「Mountain Lion」或更新版本。

检查版本

时刻关注你当前正在使用的 Docker 版本是十分重要的,这能够帮助你了解可用的特性。同时,可以让你在查找镜像时选择使用的版本。接下来让我们看看如何操作。

获取 Docker 服务版本:

1
docker version --format '{{.Server.Version}}'

你也可以输出原始的 JSON 数据:

1
docker version --format '{{json .}}'

安装

Linux

Docker 官方提供了快速、易用的安装脚本:

1
curl -sSL https://get.docker.com/ | sh

如果你不想执行一个不明不白的 Shell 脚本,那么请看 安装说明,选择你在用的发行版本。

如果你是一个 Docker 超新手,那么你应当先去看看 系列教程。

macOS

下载并安装 Docker Community Edition。如果你在使用 Homebrew-Cask,只需在命令行输入 ​brew cask install docker​ 即可。下载安装 Docker Toolbox 亦可。Docker For Mac 很赞,但是它的安装过程与 VirtualBox 不太一样。详情请查阅 比较。

注意:Docker Toolbox 已经过时。你应当使用 Docker Community Edition,详见 Docker Toolbox

安装好 Docker Community Edition 后,点击 Launchpad 内的 Docker 图标。接着即可启动容器了:

1
docker run hello-world

好了,现在你有了一个运行中的 Docker 容器了。

容器 (Container)

关于 Docker 进程隔离的基础。容器 (Container) 之于虚拟机 (Virtual Machine) 就好比线程之于进程。或者你可以把他们想成是「吃了类固醇的 chroots」。

生命周期

通常情况下,不使用任何命令行选项启动一个容器,该容器将会立即启动并停止。若需保持其运行,你可以使用 ​docker run -td container_id​ 命令。选项 ​-t​ 表示分配一个 pseudo-TTY 会话,​-d 表示自动将容器与终端分离(也就是说在后台运行容器,并输出容器 ID)。

如果你需要一个临时容器,可使用 ​docker run --rm 会在容器停止之后删除它。

如果你需要映射宿主机 (host) 的目录到 Docker 容器内,可使用 ​docker run -v $HOSTDIR:$DOCKERDIR​。详见 卷标 (Volumes) 一节。

如果你想同时删除与容器相关联的卷标,那么在删除容器的时候必须包含 ​-v​ 选项,像这样 ​docker rm -v。

从 Docker 1.10 起,其内置一套各容器独立的 日志引擎,每个容器可以独立使用。你可以使用 ​docker run --log-driver=syslog​ 来自定义日志引擎(例如以上的 ​syslog)。

启动和停止

如果你想将容器的端口 (ports) 暴露至宿主机,请见 暴露端口 一节。

关于 Docker 实例崩溃后的重启策略,详见 本文。

CPU 限制

你可以限制 CPU 资源占用,无论是指定百分比,或是特定核心数。

例如,你可以设置 ​cpu-shares。该配置看起来有点奇怪 – 1024 表示 100% CPU,因此如果你希望容器使用所有 CPU 内核的 50%,应将其设置为 512:

1
docker run -ti --c 512 agileek/cpuset-test

更多信息请参阅 https://goldmann.pl/blog/2014/09/11/resour...。

通过 ​cpuset-cpus 可使用特定 CPU 内核。

1
docker run -ti --cpuset-cpus=0,4,6 agileek/cpuset-test

请参阅 https://agileek.github.io/docker/2014/08/0... 获取更多细节以及一些不错的视频。

注意,Docker 在容器内仍然能够 看到 全部 CPU – 它仅仅是不使用全部而已。请参阅 https://github.com/docker/docker/issues/20... 获取更多细节。

内存限制

同样,亦可给 Docker 设置 内存限制:

1
docker run -it -m 300M ubuntu:14.04 /bin/bash

能力 (Capabilities)

Linux 的 Capability 可以通过使用 ​cap-add​ 和 ​cap-drop​ 设置。请参阅 https://docs.docker.com/engine/reference/r... 获取更多细节。这有助于提高安全性。

如需要挂载基于 FUSE 的文件系统,你需要结合 ​--cap-add​ 和 ​--device 使用:

1
docker run --rm -it --cap-add SYS_ADMIN --device /dev/fuse sshfs

授予对某个设备的访问权限:

1
docker run -it --device=/dev/ttyUSB0 debian bash

授予对所有设备的访问权限:

1
docker run -it --privileged -v /dev/bus/usb:/dev/bus/usb debian bash

有关容器特权的更多信息请参阅 本文。

信息

  • docker ps 查看运行中的所有容器。
  • docker logs​ 从容器中读取日志。(你也可以使用自定义日志驱动,不过在 1.10 中,它只支持 ​json-file​ 和 ​journald)。
  • docker inspect 查看某个容器的所有信息(包括 IP 地址)。
  • docker events 从容器中获取事件 (events)。
  • docker port 查看容器的公开端口。
  • docker top 查看容器中活动进程。
  • docker stats 查看容器的资源使用量统计信息。
  • docker diff 查看容器文件系统中存在改动的文件。

docker ps -a 将显示所有容器,包括运行中和已停止的。

docker stats --all 同样将显示所有容器,默认仅显示运行中的容器。

导入 / 导出

  • docker cp 在容器和本地文件系统之间复制文件或目录。
  • docker export 将容器的文件系统打包为归档文件流 (tarball archive stream) 并输出至标准输出 (STDOUT)。

执行命令

例如,进入正在运行的 ​foo​ 容器,并连接 (attach) 到一个新的 Shell 进程:​docker exec -it foo /bin/bash。

镜像 (Images)

镜像是 Docker 容器的模板。

生命周期

  • docker images 查看所有镜像。
  • docker import 从归档文件创建镜像。
  • docker build 从 Dockerfile 创建镜像。
  • docker commit 为容器创建镜像,如果容器正在运行则会临时暂停。
  • docker rmi 删除镜像。
  • docker load 从标准输入 (STDIN) 加载归档包 (tar archive) 作为镜像,包括镜像本身和标签 (tags, 0.7 起)。
  • docker save 将镜像打包为归档包,并输出至标准输出 (STDOUT),包括所有的父层、标签和版本 (parent layers, tags, versions, 0.7 起)。

其它信息

清理

虽然你可以用 ​docker rmi​ 命令来删除指定的镜像,不过有个名为 docker-gc 的工具,它可以以一种安全的方式,清理掉那些不再被任何容器使用的镜像。Docker 1.13 起,使用 ​docker image prune​ 亦可删除未使用的镜像。参见 清理。

加载 / 保存镜像

从文件中加载镜像:

1
docker load < my_image.tar.gz

保存既有镜像:

1
docker save my_image:my_tag | gzip > my_image.tar.gz

导入 / 导出容器

从文件中导入容器镜像:

1
cat my_container.tar.gz | docker import - my_image:my_tag

导出既有容器:

1
docker export my_container | gzip > my_container.tar.gz

加载已保存的镜像 与 导入已导出为镜像的容器 的不同

通过 ​load​ 命令来加载镜像,会创建一个新的镜像,并继承原镜像的所有历史。
通过 ​import​ 将容器作为镜像导入,也会创建一个新的镜像,但并不包含原镜像的历史,因此会比使用 ​load 方式生成的镜像更小。

网络 (Networks)

Docker 具备 网络 功能。我并不是很了解它,所以这是一个扩展本文的好地方。文档 使用网络 指出,这是一种无需暴露端口即可实现 Docker 容器间通信的好方法。

生命周期

其它信息

建立连接

你可以 为容器指定 IP 地址:

1
2
3
4
5
6
7
8
# 使用你自己的子网和网关创建一个桥接网络
docker network create --subnet 203.0.113.0/24 --gateway 203.0.113.254 iptastic

# 基于以上创建的网络,运行一个 Nginx 容器并指定 IP
$ docker run --rm -it --net iptastic --ip 203.0.113.2 nginx

# 在其他地方使用 CURL 访问这个 IP(假设该 IP 为公网)
$ curl 203.0.113.2

仓管中心和仓库 (Registry & Repository)

仓库 (repository) 是 被托管 (hosted) 的已命名镜像 (tagged images) 的集合,这组镜像用于构建容器文件系统。

仓管中心 (registry) 则是 托管服务 (host) – 用于存储仓库并提供 HTTP API,以便 管理仓库的上传和下载。

Docker 官方托管着自己的 仓管中心,包含着数量众多的仓库。不过话虽如此,这个仓管中心 并没有很好地验证镜像,所以如果你担心安全问题的话,请尽量避免使用它。

本地仓管中心

你可以使用 docker distribution 项目搭建本地的仓管中心,详情参阅 本地发布 (local deploy) 的介绍。

科学上网后,也可以看看 Google+ Group。

Dockerfile

当你执行 ​docker build​ 时,Docker 将会根据 配置文件 启动 Docker 容器。远优于使用 ​docker commit。

以下是一些编写 Dockerfile 的常用编辑器,并链接到适配的语法高亮模块︰

指令

  • .dockerignore
  • FROM 为其他指令设置基础镜像 (Base Image)。
  • MAINTAINER (deprecated - use LABEL instead) 为生成的镜像设置作者字段。
  • RUN 在当前镜像的基础上生成一个新层并执行命令。
  • CMD 设置容器默认执行命令。
  • EXPOSE 告知 Docker 容器在运行时所要监听的网络端口。注意:并没有实际上将端口设置为可访问。
  • ENV 设置环境变量。
  • ADD 将文件、目录或远程文件复制到容器中。缓存无效。请尽量用 ​COPY​ 代替 ​ADD。
  • COPY 将文件或文件夹复制到容器中。注意:将使用 ROOT 用户复制文件,故无论 USER / WORKDIR 指令如何配置,你都需要手动修改其所有者(​chown​),​ADD 也是一样。
  • ENTRYPOINT 将容器设为可执行的。
  • VOLUME 在容器内部创建挂载点 (mount point) 指向外部挂载的卷标或其他容器。
  • USER 设置随后执行 RUN / CMD / ENTRYPOINT 命令的用户名。
  • WORKDIR 设置工作目录 (working directory)。
  • ARG 定义编译时 (build-time) 变量。
  • ONBUILD 添加触发指令,当该镜像被作为其他镜像的基础镜像时该指令会被触发。
  • STOPSIGNAL 设置停止容器时,向容器内发送的系统调用信号 (system call signal)。
  • LABEL 将键值对元数据 (key/value metadata) 应用到镜像、容器或是守护进程。

教程

例子

层 (Layers)

Docker 的版本化文件系统是基于层的。就像 Git 的提交或文件变更系统 一样。

链接 (links) 通过 TCP/IP 端口 实现 Docker 容器之间的通讯。Atlassian 展示了可用的例子。你还可以 通过主机名 (hostname) 链接。

在某种意义上来说,该特性已经被 自定义网络 所替代。

注意:如果你希望容器之间只通过链接进行通讯,在启动 Docker 守护进程时,请使用 ​-icc=false 来禁用内部进程通讯。

假设你有一个名为 CONTAINER 的容器(通过 ​docker run --name CONTAINER 指定)并且在 Dockerfile 中,暴露了一个端口:

1
EXPOSE 1337

然后,我们创建另外一个名为 LINKED 的容器:

1
docker run -d --link CONTAINER:ALIAS --name LINKED user/wordpress

然后 CONTAINER 暴露的端口和别名将会以如下的环境变量出现在 LINKED 中:

1
2
$ALIAS_PORT_1337_TCP_PORT
$ALIAS_PORT_1337_TCP_ADDR

那么你便可以通过这种方式来连接它了。

使用 ​docker rm --link 即可删除链接。

通常,Docker 容器(亦可理解为「服务」)之间的链接,是「服务发现」的一个子集。如果你打算在生产中大规模使用 Docker,这将是一个很大的问题。请参阅 The Docker Ecosystem: Service Discovery and Distributed Configuration Stores 获取更多信息。

卷标 (Volumes)

Docker 的卷标 (volumes) 是 独立的文件系统。它们并非必须连接到特定的容器上。

生命周期

信息

卷标在不能使用链接(只有 TCP/IP)的情况下非常有用。例如,如果你有两个 Docker 实例需要通讯并在文件系统上留下记录。

你可以一次性将其挂载到多个 docker 容器上,通过 ​docker run --volumes-from。

因为卷标是独立的文件系统,它们通常被用于存储各容器之间的瞬时状态。也就是说,你可以配置一个无状态临时容器,关掉之后,当你有第二个这种临时容器实例的时候,你可以从上一次保存的状态继续执行。

查看 卷标进阶 来获取更多细节。Container42 非常有用。

你可以 将宿主 MacOS 的文件夹映射为 Docker 卷标:

1
docker run -v /Users/wsargent/myapp/src:/src

你也可以用远程 NFS 卷标,如果你觉得你 有足够勇气。

还可以考虑运行一个纯数据容器,像 这里 所说的那样,提供可移植数据。

记得,文件也可以被挂载为卷标。

暴露端口 (Exposing ports)

通过宿主容器暴露输入端口相当 繁琐但有效的。

例如使用 ​-p 将容器端口映射到宿主端口上(只使用本地主机 (localhost) 接口):

1
docker run -p 127.0.0.1:$HOSTPORT:$CONTAINERPORT --name CONTAINER -t someimage

你可以使用 EXPOSE 告知 Docker,该容器在运行时监听指定的端口:

1
EXPOSE <CONTAINERPORT>

但是注意 EXPOSE 并不会直接暴露端口,你需要用参数 ​-p 。比如说你要在 localhost 上暴露容器的端口:

1
iptables -t nat -A DOCKER -p tcp --dport <LOCALHOSTPORT> -j DNAT --to-destination <CONTAINERIP>:<PORT>

如果你是在 Virtualbox 中运行 Docker,那么你需要配置端口转发 (forward the port)。使用 forwarded_port 在 Vagrantfile 上配置暴露的端口范围,这样你就可以动态地映射了:

1
2
3
4
5
6
7
8
9
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
...

(49000..49900).each do |port|
  config.vm.network :forwarded_port, :host => port, :guest => port
end

...
end

如果你忘记了将什么端口映射到宿主机上的话,可使用 ​docker port 查看:

1
docker port CONTAINER $CONTAINERPORT

最佳实践

这里有一些最佳实践,以及争论焦点:

安全 (Security)

这节准备讨论一些关于 Docker 安全性的问题。Docker 官方文档 安全 页面讲述了更多细节。

首先第一件事:Docker 是有 root 权限的。如果你在 ​docker​ 组,那么你就有 root 权限。如果你将 Docker 的 Unix Socket 暴露给容器,意味着你赋予了容器 宿主机 root 权限。

Docker 不应当作为唯一的防御措施。你应当使其更加安全可靠。

为了更好地理解容器暴露了什么,可参阅由 Aaron Grattafiori 编写的 Understanding and Hardening Linux Containers。这是一个完整全面且包含大量链接和脚注的容器问题指南,介绍了许多有用的内容。即使你已经加固过容器,以下的安全提示依然十分有帮助,但并不能代替理解的过程。

安全提示

为了最大的安全性,你应当考虑在虚拟机上运行 Docker。这是直接从 Docker 安全团队拿来的资料 – slides / notes。之后,可使用 AppArmor、seccomp、SELinux、grsec 等来 限制容器的权限。更多细节,请查阅 Docker 1.10 security features。

Docker 镜像 ID 属于 敏感信息 所以它不应该向外界公开。请将它们当作密码来对待。

阅读由 Thomas Sjögren 编写的 Docker Security Cheat Sheet:关于加固容器的不错的建议。

查看 Docker 安全测试脚本,下载 最佳实践白皮书。

你应当远离使用非稳定版本 grsecurity /pax 的内核,比如 Alpine Linux。如果在产品中用了 grsecurity,那么你应该考虑使用有 商业支持 的 稳定版本,就像你对待 RedHat 那样。虽然要 $200 每月,但对于你的运维预算来说不值一提。

从 Docker 1.11 开始,你可以轻松的限制在容器中可用的进程数,以防止 fork 炸弹。 这要求 Linux 内核 >= 4.3,并且要在内核配置中打开 CGROUP_PIDS=y。

1
docker run --pids-limit=64

同时,你也可以限制进程再获取新权限。该功能是 Linux 内核从 3.5 版本开始就拥有的。你可以从 这篇博客 中阅读到更多关于这方面的内容。

1
docker run --security-opt=no-new-privileges

以下内容摘选自 Container Solutions 的 Docker Security Cheat Sheet(PDF 版本,难以使用,故复制至此):

关闭内部进程通讯:

1
docker -d --icc=false --iptables

设置容器为只读:

1
docker run --read-only

通过 hashsum 来验证卷标:

1
docker pull debian@sha256:a25306f3850e1bd44541976aa7b5fd0a29be

设置卷标为只读:

1
docker run -v $(pwd)/secrets:/secrets:ro debian

在 Dockerfile 中定义用户并以该用户运行,避免在容器中以 ROOT 身份操作:

1
2
RUN groupadd -r user && useradd -r -g user user
USER user

用户命名空间 (User Namespaces)

还可以通过使用 用户命名空间 – 自 1.10 版本起已内置,但默认并未启用。

要在 Ubuntu 15.10 中启用用户命名空间 (remap the userns),请 跟着这篇博客的例子 来做。

安全相关视频

安全路线图

Docker 的路线图提到关于 seccomp 的支持。
一个名为 bane 的 AppArmor 策略生成器正在实现 安全配置文件。

小贴士

链接:

清理

最新的 数据管理命令 已在 Docker 1.13 实现:

  • docker system prune
  • docker volume prune
  • docker network prune
  • docker container prune
  • docker image prune

df 命令

docker system df 将显示当前 Docker 各部分占用的磁盘空间。

Heredoc 声明 Docker 容器

1
2
3
4
docker build -t htop - << EOF
FROM alpine
RUN apk --no-cache add htop
EOF

最近一次的容器 ID

1
2
3
alias dl='docker ps -l -q'
docker run ubuntu echo hello world
docker commit $(dl) helloworld

带命令的提交(需要 Dockerfile)

1
docker commit -run='{"Cmd":["postgres", "-too -many -opts"]}' $(dl) postgres

获取 IP 地址

1
docker inspect $(dl) | grep -wm1 IPAddress | cut -d '"' -f 4

或使用 jq:

1
docker inspect $(dl) | jq -r '.[0].NetworkSettings.IPAddress'

或使用 go 模板:

1
docker inspect -f '{{ .NetworkSettings.IPAddress }}' <container_name>

或在通过 Dockerfile 构建镜像时,通过构建参数 (build argument) 传入:

1
2
3
4
5
6
DOCKER_HOST_IP=`ifconfig | grep -E "([0-9]{1,3}\.){3}[0-9]{1,3}" | grep -v 127.0.0.1 | awk '{ print $2 }' | cut -f2 -d: | head -n1`
echo DOCKER_HOST_IP = $DOCKER_HOST_IP
docker build \
 --build-arg ARTIFACTORY_ADDRESS=$DOCKER_HOST_IP
 -t sometag \
some-directory/

获取端口映射

1
docker inspect -f '{{range $p, $conf := .NetworkSettings.Ports}} {{$p}} -> {{(index $conf 0).HostPort}} {{end}}' <containername>

通过正则匹配容器

1
for i in $(docker ps -a | grep "REGEXP_PATTERN" | cut -f1 -d" "); do echo $i; done`

获取环境变量配置

1
docker run --rm ubuntu env

强行终止运行中的容器

1
docker kill $(docker ps -q)

删除所有容器(强行删除!无论容器运行或停止)

1
docker rm -f $(docker ps -qa)

删除旧容器

1
docker ps -a | grep 'weeks ago' | awk '{print $1}' | xargs docker rm

删除已停止的容器

1
docker rm -v `docker ps -a -q -f status=exited`

停止并删除容器

1
docker stop $(docker ps -aq) && docker rm -v $(docker ps -aq)

删除无用 (dangling) 的镜像

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

删除所有镜像

1
docker rmi $(docker images -q)

删除无用 (dangling) 的卷标

Docker 1.9 版本起:

1
docker volume rm $(docker volume ls -q -f dangling=true)

1.9.0 中,参数 ​dangling=false​ 居然 没 用 - 它会被忽略然后列出所有的卷标。

查看镜像依赖

1
docker images -viz | dot -Tpng -o docker.png

Docker 容器瘦身

  • 在某层 (RUN layer) 清理 APT

这应当和其他 apt 命令在同一层中完成。
否则,前面的层将会保持原有信息,而你的镜像则依旧臃肿。

1
2
3
RUN {apt commands} \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
  • 压缩镜像

    1
    2
    ID=$(docker run -d image-name /bin/bash)
    docker export $ID | docker import – flat-image-name
  • 备份

    1
    2
    3
    ID=$(docker run -d image-name /bin/bash)
    (docker export $ID | gzip -c > image.tgz)
    gzip -dc image.tgz | docker import - flat-image-name

监视运行中容器的系统资源利用率

检查某个容器的 CPU、内存以及网络 I/O 使用情况,你可以:

1
docker stats <container>

按 ID 列出所有容器:

1
docker stats $(docker ps -q)

按名称列出所有容器:

1
docker stats $(docker ps --format '{{.Names}}')

按指定镜像名称列出所有容器:

1
docker ps -a -f ancestor=ubuntu

删除所有未标签命名 (untagged) 的容器:

1
docker rmi $(docker images | grep “^” | awk '{split($0,a," "); print a[3]}')

通过正则匹配删除指定容器:

1
docker ps -a | grep wildfly | awk '{print $1}' | xargs docker rm -f

删除所有已退出 (exited) 的容器:

1
docker rm -f $(docker ps -a | grep Exit | awk '{ print $1 }')

将文件挂载为卷标

文件也可以被挂载为卷标。例如你可以仅仅注入单个配置文件:

1
2
3
4
5
6
7
8
# 从容器复制文件
docker run --rm httpd cat /usr/local/apache2/conf/httpd.conf > httpd.conf

# 编辑文件
vim httpd.conf

# 挂载修改后的配置启动容器
docker run --rm -ti -v "$PWD/httpd.conf:/usr/local/apache2/conf/httpd.conf:ro" -p "80:80" httpd

参考链接

赞赏


使用 privoxy 转发 socks 到 http

安装及配置

安装

1
2
$ sudo apt-get update
$ sudo apt-get install privoxy

配置,在文件​/etc/privoxy/config 末追加内容

1
2
3
4
# 把本地 HTTP 流量转发到本地 1080 SOCKS5 代理
forward-socks5t / 127.0.0.1:1080 .
# 可选,默认监听本地连接
listen-address 127.0.0.1:8118

重新加载配置

1
2
$ sudo /etc/init.d/privoxy start
$ sudo /etc/init.d/privoxy reload   # 不重启服务的情况下重新加载配置

privoxy 默认开机自启动

参考:

赞赏


Linux 技巧:让进程在后台运行

使用 screen

新建窗口

1
$ screen -S test // 新建一个名字为 test 的窗口,并进入到该窗口

跳出窗口

快捷键 ​Ctrl a +d 实现会话分离,重新进入 ssh 主界面

恢复会话窗口

1
2
3
4
5
$ screen -list // 列出窗口列表
There are screens on:
1388.test (2019年04月07日 16时27分13秒) (Detached)
1 Sockets in /var/run/screen/S-jinva.
$ screen -r 1388 // 重新进入 1388 进程,恢复 test 会话窗口

杀死当前会话窗口

快捷键 ​Ctrl a +k​
或者在当前会话窗口输入 ​exit 按回车退出

1
exit

清除死去的窗口

窗口被杀死后,再用 screen -ls 或者 screen -list 可以看到该窗口后面的(???dead)字样,说明窗口死了,但是仍在占用空间。这时需要清除窗口

1
$ screen -wipe // 自动清除死去的窗口

参考:https://linuxize.com/post/how-to-use-linux-screen/

赞赏


使用 RDM 连接 Homestead 中的 Redis

安装RDM

https://redisdesktop.com/download

修改 redis.conf

1
$ sudo vim /etc/redis/redis.conf
  1. 将bind 127.0.0.1 ::1 改成 bind 0.0.0.0 ::1
  2. 将 requirepass 前面的 # 号去掉,后面加上一个密码,例如:
1
requirepass yourpassword

修改 Homestead.yaml
添加端口转发

1
2
3
ports:
    - send: 63790
    to: 6379

重启虚拟机

1
$ vagrant reload --provision

赞赏


Linux 设置关机重启提示

由于运行 reboot 命令系统默认不需要输入密码,为了防止意外发生,必须设置关机提示。

编辑 .zshrc

1
$ vi ~/.zshrc

注意,如果使用 bash 的话,请编辑 .bashrc 文件。

追加内容

1
2
3
# Shutdown and restart confirmation
alias shutdown="echo 'Are you sure?' If so, run /sbin/shutdown"
alias reboot="echo 'Are you sure?' If so, run /sbin/reboot"

更新文件

1
$ source .zshrc

到此,每当运行 reboot 或者 shutdown 命令的时候,都会出现提示。
这只是其中一种笨方法,但是很简单。

赞赏


如何在Linux中禁用网络摄像头和麦克风

禁用摄像头

首先,使用如下命令找到网络摄像头驱动:

1
$ sudo lsmod | grep uvcvideo

示例输出:

1
2
3
4
5
6
7
uvcvideo 114688 1
videobuf2_vmalloc 16384 1 uvcvideo
videobuf2_v4l2 28672 1 uvcvideo
videobuf2_common 53248 2 uvcvideo,videobuf2_v4l2
videodev 208896 4 uvcvideo,videobuf2_common,videobuf2_v4l2
media 45056 2 uvcvideo,videodev
usbcore 286720 9 uvcvideo,usbhid,usb_storage,ehci_hcd,ath3k,btusb,uas,ums_realtek,ehci_pci

这里,uvcvideo 是我的网络摄像头驱动。

现在,让我们禁用网络摄像头。

为此,请编辑以下文件(如果文件不存在,只需创建它):

1
$ sudo vi /etc/modprobe.d/blacklist.conf

添加以下行:

1
2
##Disable webcam.
blacklist uvcvideo

##Disable webcam 这行不是必需的。为了便于理解,我添加了它。

保存并退出文件。重启系统以使更改生效。

要验证网络摄像头是否真的被禁用,可打开 vlc player 的视频捕捉功能,看设备当中有没有发现摄像头硬件信息。

相反,如果要启用它,只需将 blacklist.confi 中添加的内容注释或删除掉即可。

禁用麦克风

使用如下命令查看麦克风驱动信息:

1
cat /proc/asound/modules

示例输出:

1
2
0 snd_hda_intel
1 snd_usb_audio

其中 ​snd_hda_intel 为麦克风驱动名称。

编辑以下文件:

1
$ sudo vi /etc/modprobe.d/blacklist.conf

添加以下行:

1
2
# Disable mic
blacklist snd_hda_intel

保存并推出文件。重启系统以便更改生效。

要验证是否真的被禁用,只需打开声音设置,查看声音输入源是否存在麦克风信息。

相反,如果要启用它,只需将 blacklist.confi 中添加的内容注释或删除掉即可。

赞赏


0%