0%

问题背景

如果给你一个包含 5000 万个元素的数组,然后会有频繁的区间修改操作,比如让第 1 个数到第 1000万 个数每个数都加上 1,而且这种操作是频繁的。

此时,很容易想到的办法就是从 1 遍历到 1000万,每个加上 1,但如果这种操作很频繁的话,效率就可能会非常低下。

差分数组原理

假设现在有一个数组,\(nums=\{0, 2, 5, 4, 9, 7, 10, 0\}\)

\(index\) 0 1 2 3 4 5 6 7
\(d[i]\) 0 2 5 4 9 7 10 0

那么差分数组是什么呢?

其实差分数组本质上也是一个数组,我们暂且定义差分数组为 \(d\),差分数组 \(f\) 的大小和原来 \(d\) 数组大小一样,而且 \(f[i] = d[i] - d[i - 1]\),(\(i\neq0\))。

它的含义是什么呢?就是原来数组 \(i\) 位置上的元素和 \(i-1\) 位置上的元素作差,得到的值就是 \(d[i]\) 的值。

所以,上面数组对应的差分数组值如下图所示:

\(index\) 0 1 2 3 4 5 6 7
\(d[i]\) 0 2 5 4 9 7 10 0
\(f[i]\) 0 2 3 -1 5 -2 3 -10

那么构造这个数组有什么意义呢?

现在我们有这么一个区间操作,即在区间 \([1,4]\) 上,所有的数值都加上 3.

\(index\) 0 1 2 3 4 5 6 7
\(d[i]\) 0 2+3 5+3 4+3 9+3 7 10 0
\(f[i]\) 0 2+3 3 -1 5 -2-3 3 -10

由上面的表格可以知道,这个操作在差分数组上,只影响到了差分数组区间其实位置和结束位置。

我们只需要修改一下差分数组的起始和结束位置,就可以记录这次的区间修改操作了。这样就可以把修改区间的时间复杂度 \(O(n)\) 降为 \(O(1)\)

现在,我们如何根据差分数组 \(f\) 来推测 \(d\) 中某一个位置的值呢?

只需要求差分数组的前缀和即可。

差分数组定义

对于已知有 \(n\) 个元素的离线数列 \(d\),我们可以建立记录它每项与前一项差值的差分数组 \(f\),显然有:

\[ f[i]=\left\{ \begin{aligned} &d[i], &(i = 0) \\ &d[i] - d[i - 1], &(1 \leq i \lt n) \end{aligned} \right. \]

差分数组简单性质

  1. 计算数列各项的值:数组第 \(i\) 项的值是可以用差分数组的前 \(i\) 项的和计算的,即 前缀和
  2. 计算数列每一项的前缀和:第 \(i\) 项的前缀和即为数列 \(f[i]\)\(i\) 项的和。对差分数列求两次前缀和?

差分数组用途

  1. 快速处理区间加减操作:

假如现在对数列中区间 \([L, R]\) 上的数加上 \(x\),我们通过性质 1 知道,第一个受影响的差分数组中的元素为 \(f[L]\),即令 \(f[L]+=x\),那么后面数列元素在计算过程中都会加上 \(x\); 最后一个受影响的差分数组中的元素为 \(f[R]\),所以令 \(f[R+1]-=x\),即可保证不会影响到 \(R\) 以后数列元素的计算。这样我们不必对区间内每一个数进行处理,只需要处理两个差分后的数即可;

  1. 询问区间和问题:

由性质 2 我们可以计算出数列各项的前缀和数组 \(sum\) 各项的值;那么显然,区间 \([L, R]\) 的和即为 \(ans = sum[R] - sum[L - 1]\)

差分数组应用

leetcode 1109. 航班预订统计

对于预定记录 \(booking = [l, r, inc]\),我们需要让 \(d[l - 1]\) 增加 \(inc\)\(d[r]\) 减少 \(inc\)。特别地,当 \(r\)\(n\) 时,我们无需修改 \(d[r]\), 因为这个位置溢出了下标范围。如果求前缀和时考虑该位置,那么该位置对应的前缀和值必定为 0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public int[] corpFlightBookings(int[][] bookings, int n) {
int[] nums = new int[n];
for (int[] booking : bookings) {
nums[booking[0] - 1] += booking[2];
if (booking[1] < n) {
nums[booking[1]] -= booking[2];
}
}
for (int i = 1; i < n; i++) {
nums[i] += nums[i - 1];
}
return nums;
}
}

写本文目的:网上讲解01背包问题的文章有很多,但看着这些文章感觉少了点什么,后来想想,好像都在讲解问题本身,然后给出答案,然后我们看这些文章的时候,好像都懂了,但是我们好像还是不知道什么是动态规划。因此本文就是要讲一讲01背包里面包含的动态规划思想。

什么是01背包问题

背包问题描述:

给定 n 件物品,物品的重量为 weights[i],物品的价值为 values[i]。现挑选物品放入背包中,假定背包能承受的最大重量为 W,问应该如何选择装入背包中的物品,使得装入背包中物品的总价值最大?

01背包问题算是动态规划里面的基础问题了,网上已经有很多讲解01背包问题的文章了,而且讲得也很好。比如:

动态规划:关于01背包问题,你该了解这些! 动态规划:关于01背包问题,你该了解这些!(滚动数组) 图解 | 你管这破玩意叫动态规划

还有就是《算法图解》里面的 9.1 背包问题,虽然标题是背包问题,但讲解的依然是01背包,算法图解里面的讲解算是很形象了。

如果完全不了解01背包问题的可以看上面几篇文章,讲得都挺好的,这里没有必要再讲一遍了。

01背包为什么叫01背包

想象一下,我们现在有一个背包,然后对于摆在我们面前的 n 件物品,我们都有两个选择,选或者不选,没有其他的选择了。

我们就可以使用 01 来表示这两种状态,0 表示不选择,1 表示选择。假设我们有 3 件物品,那么 [0, 1, 0] 就表示我们只选择了第 2 件物品。

也就是说,最终下来所有可能的不同组合情况共有 2^n 种(n 位,每位有两种可能,0 或 1)。

如果通过遍历所有可能的组合来求解 01背包 问题,那么时间复杂度将会是指数级别的。

什么是动态规划(维基百科)

这里照搬一下维基百科关于动态规划的一些描述。

动态规划(英语:Dynamic programming,简称DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

动态规划在查找有很多重叠子问题的情况的最优解时有效。它将问题重新组合成子问题。为了避免多次解决这些子问题,它们的结果都逐渐被计算并被保存,从简单的问题直到整个问题都被解决。因此,动态规划保存递归时的结果,因而不会在解决同样的问题时花费时间。

动态规划只能应用于有最优子结构的问题。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。简单地说,问题能够分解成子问题来解决。

动态规划的适用情况:

  1. 最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
  2. 无后效性。即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。
  3. 子问题重叠性质。子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率,降低了时间复杂度。

递归算法

在讲解01背包跟动态规划的关系之前,很有必要先看看递归算法是如何执行的,了解了递归算法的执行过程之后,后面我们就能知道动态规划的优势了。

说到递归,我们很容易想到斐波那契数列,因为斐波那契数列很容易地通过递归算法实现,如下,就几行代码:

1
2
3
4
5
6
public int f(int n) {
if (n == 0 || n == 1)
return n;

return f(n - 1) + f(n - 2);
}

但是虽然它的代码很简单,但是执行的过程并没有那么简单。

我们可以将递归的执行过程想象成一个树状的结构,递归的执行过程如下图,因为 f(0)f(1) 是已知的,所以叶子结点到 f(0) 或者 f(1) 的时候就结束了。

fib.png

简单来讲,就是在求解的过程中,由大到小,依次求解,求解完左边之后,再求解右边,又进行一次由大到小的求解。在到达边界的时候,依次返回。

然后我们可以很容易发现,其实在递归的过程中,f(2)f(3) 都被重复计算了,而且,我们的递归层数越多,被重复计算的子问题就越多。

递归有个问题是,子问题会被多次重复求解,这样会导致有很多时间浪费在做一些重复计算上。

递归与动态规划

对于斐波那契数列,有没有方法可以做到 f(2)f(3) 只求解一次呢?

有的,先求解子问题即可,从斐波那契数列的定义,我们可以知道 f(n) + f(n + 1) = f(n + 2),那么,如果我们要求 f(5),我们可以从 f(0) 求解到 f(5)

1
2
3
4
f(2) = f(0) + f(1),f(0) 和 f(1) 不需要求解,可以直接得出
f(3) = f(1) + f(2),上一步我们已经求解了 f(2) 了,我们加上 f(1) 就得到 f(3) 了,这一步利用了上一步的计算结果
f(4) = f(2) + f(3),上一步我们已经求解了 f(3) 了,而且我们的 f(2) 在第一步已经求解了,我们这里直接利用了前两次计算的结果
f(5) = f(3) + f(4),同理,这里利用了前两次计算的结果

代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public int f(int n) {
if (n <= 1) return n;

int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;

for (int i = 2; i <= n; i++){
dp[i] = dp[i - 1] + dp[i - 2];
}

return dp[n];
}

所以这里的关键是什么呢?

  1. 先求解子问题,记录下子问题的答案。
  2. 逐步求解更大的问题,这个更大的问题可以直接利用前面的计算结果。

这跟动态规划有什么关系?就从动态规划的适用情况说吧:

  1. 最优子结构:我们从小到大求解 f(n) 的过程中,每一个 f(i) 可以看作最优子结构,当然这里实际上并没有优劣一说,我们每一个 f(i) 都是唯一的(对于 01背包 这种就有最优子结构的说法了)。
  2. 无后效性:我们在求解 f(i) 的时候,并不关心 f(i - 1)f(i - 2) 是怎么得到的,我们只需要知道前面求解的结果,并不关系其过程。
  3. 子问题重叠:我们在递归求解 f(5) 的时候,发现 f(2)f(3) 都被重复计算了,f(2)f(3) 就是重叠子问题。

所以我们可以看到,动态规划的关键是,先求解子问题,得到子问题的解,然后逐步求解更大的问题,对于更大的问题,直接利用子问题的结果(子问题的结果都是最优结果),不需要再次计算。逐步求解,直到我们得到最终问题的答案。

01背包的决策过程

假设我们有一个容量为 4 的背包,有三件物品,物品0重量为 1、价值为 15,物品1重量为 3、价值为 20,物品2重量为 4、价值为 30。

然后使用 dp[i] 表示不同的背包容量下所能获得的最大价值。我们的背包容量为 4,所以只需要计算到 dp[4] 即可。

  1. 对于第一件物品,dp[1]dp[2]dp[3]dp[4] 都是 15,因为只有容量大于等于 1 的时候我们才能装得下这件物品,然后可以获得其价值。

  2. 对于第二件物品,dp[1]dp[2] 依然是 15,因为只有容量为 3 的背包才能放得下第二件物品。

  1. 对于 dp[3],容量为 3 的时候,有两种情况
  • 不放入这件物品,那么所能获得的价值依然是 15。
  • 放入这件物品。但是放入之后,就要占据背包全部容量了,之前的所有物品都放不下了,但是我们可以获得更大的价值 20。因此 dp[3] = 20

因此在背包容量为 3,有前两件物品选择的情况下,我们所能获得的最大价值是 20。

  1. 对于 dp[4],容量为 4 的时候,我们可以选择不放入这件物品,那么所能获得的价值依然是 15。
  • 不放入这件物品,那么所能获得的价值依然是 15。
  • 放入这件物品。放入之后,我们的背包还剩容量 1,在之前的物品中,容量 1 所能获得的最大价值是 15。

因此在背包容量为 4,有前两件物品选择的情况下,我们所能获得的最大价值是 20 + 15 = 35,20 是当前物品的价值,15 是剩余背包容量所能获得的最大价值。

  1. 对于第三件物品的分析过程类似在对第二件物品做决策的过程,不再赘述。

我们在决策的过程中,需要根据之前物品选择之后,不同背包容量的所能获得的最大价值来进行决定是否选择当前物品。这样就能保证我们每次选择都是最优的结果。

01背包问题里面的动态规划

我们可以从动态规划适用情况来看01背包问题:

  1. 最优子结构

0~i 件物品选择之后,不同背包容量下所能获得的最大价值就是一个最优子结构。我们并不需要知道前面的 0~i 件物品具体是怎样选择的,我们只需要知道选择的最优结果即可。

之所以需要计算不同容量,是因为如果我们当前剩余背包容量放不下当前物品的时候,就需要求解放入当前物品后,剩余的背包容量的最大价值,然后加上当前物品的价值,就是放入当前物品所能获得的最大价值。

  1. 无后效性

对于 0~i 件物品具体怎么选择,我们在决定要不要放入当前物品的时候,并不关心,我们只需要知道有某一种方案可以使得获得当前最大价值就行了,同时我们后面不管怎么选择都影响不到之前选择得到的最优结果。

  1. 重叠子问题

我们在选择一件物品的时候,假设要放入当前物品,那就需要知道剩余背包容量的在之前的物品选择中所能获得的最大价值。而之前的物品选择中,不同容量下所能获得的最大价值就是子问题。

01背包里面的重叠子问题

下面的讲解跟回溯相关,想详细了解回溯的朋友可以看看这一篇文章:回溯算法理论基础!

如果直接从动态规划的角度来思考01背包问题里面的重叠子问题可能会比较困难。

但是还有一种算法可以求解01背包问题的,那就是回溯(本质是递归),但是上面也说了,时间复杂度太高了,指数级别的。但是从回溯的角度来看,就很好理解01背包里面的重叠子问题。

现在,假设我们有一个容量为 W 的背包,有 3 件物品 A、B、C,需要从这 3 件物品里面选取不同的物品加入背包,使得加入背包的价值最大。我们使用一个长度为 3 的数组来记录每一种不同的组合,数组的每一个元素使用 0 代替没有选中的状态,使用 1 来代替选中的状态。

用回溯算法的处理过程如下,下图的每一个节点里面,我们都计算从 0~W 的容量下,当前的组合能获得的最大价值。

01.png

从上图看来,我们不管是否选择了 A,都要再针对 B 和 C 做出同样的组合判断,但是我们要知道,对于不同的背包容量,B 和 C 组合能得到的最大价值都是固定的,跟 A 并没有关系。

所以如果使用回溯算法,对于选不选 A,后面对于 B 和 C 的组合我们都做了重复的计算了。

上图上色的部分就是重复计算的部分,当然还有其他的重复计算,这些重复计算的地方就是01背包里面的重叠子问题了。

所以01背包里面的重叠子问题就是,i 个物品在不同背包容量下的最大价值。

动态规划是怎么处理重叠子问题的

我们再回忆一下上面提到的斐波那契数列的优化写法,我们是通过计算更小的斐波那契数列,逐步计算到我们需要的那个数 n,在这个计算过程中我们可以直接使用前面的计算结果。

因此,对于01背包问题,我们其实也可以先求解重叠的子问题,然后将这些子问题的解保存下来,在解决更大的问题的时候直接使用前面计算的结果。

具体来说,可以怎么做呢?

就是每次做判断的时候,我们都去计算将这个物品加入背包或者不加入背包的情况下,0~W 不同的背包容量下所能获得的最大价值。

然后我们在对下一个物品进行选择的时候,可以用背包容量减去当前物品重量,得到一个一个剩余的背包容量,我们看剩余背包容量在前面物品的选择中最大价值是多少,加上当前物品的价值,就是加入当前物品之后所能获得的最大价值。

我们需要注意的是,加入这件物品之后,得到的最大价值未必比不加入这件物品的价值更大,这个时候就需要对比一下两种情况,哪一种情况能获得的价值最大,就是当前选择过的物品不同组合下所能获得的最大价值了。

在01背包里面,我们可以直接利用 0~i-1 件物品的选择结果,来判断是否将第 i 件物品加入到背包。

总结

01背包里面的动态规划思想(假设我们已经选择了 0~i-1 件物品,W 是背包容量):

  1. 最优子结构:0~i-1 件物品在不同的背包容量下能获得的最大价值是一个最优子结构。
  2. 无后效性:我们在决定放不放第 i 件物品的时候,前面的 0~i-1 件物品是如何组合得到那个最大价值完全没影响。
  3. 重叠子问题:在 0~i-1 件物品的不同组合中,0~W 所能获得的最大价值。

arch

实例

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
➜  ~ ps aux | grep laravels | grep -v grep
root 29239 0.0 0.1 989924 42660 ? Sl 11:07 0:03 laravels: master process
root 29250 0.0 0.0 402248 23060 ? S 11:07 0:00 laravels: manager process
root 29259 0.0 0.5 635160 188684 ? S 11:07 0:06 laravels: worker process 0
root 29260 0.0 0.5 635148 192564 ? S 11:07 0:13 laravels: worker process 1
root 29261 0.0 0.5 633112 189896 ? S 11:07 0:10 laravels: worker process 2
root 29262 0.0 0.5 641240 194080 ? S 11:07 0:19 laravels: worker process 3
root 29263 0.0 0.5 620484 171616 ? S 11:07 0:11 laravels: worker process 4
root 29264 0.0 0.4 588592 146464 ? S 11:07 0:05 laravels: worker process 5
root 29265 0.0 0.4 512808 139264 ? S 11:07 0:03 laravels: worker process 6
root 29266 0.0 0.4 592672 145852 ? S 11:07 0:03 laravels: worker process 7
root 29267 0.0 0.3 551456 102340 ? S 11:07 0:04 laravels: worker process 8
root 29268 0.0 0.5 633128 186340 ? S 11:07 0:05 laravels: worker process 9
root 29269 0.0 0.2 537124 90532 ? S 11:07 0:04 laravels: worker process 10
root 29270 0.0 0.4 590644 147988 ? S 11:07 0:02 laravels: worker process 11
root 29271 0.0 0.2 526872 79992 ? S 11:07 0:05 laravels: worker process 12
root 29272 0.0 0.6 645412 197672 ? S 11:07 0:11 laravels: worker process 13
root 29273 0.0 1.3 904576 452132 ? S 11:07 0:15 laravels: worker process 14
root 29274 0.0 0.3 545384 101540 ? S 11:07 0:04 laravels: worker process 15

➜ ~ pstree -pa 29239
php,29239 // master 进程
├─php,29250 // manager 进程
│ ├─php,29259 // worker 0
│ ├─php,29260 // worker 1
│ ├─php,29261 // worker 2
│ ├─php,29262 // worker 3
│ ├─php,29263 // worker 4
│ ├─php,29264 // worker 5
│ ├─php,29265 // worker 6
│ ├─php,29266 // worker 7
│ ├─php,29267 // worker 8
│ ├─php,29268 // worker 9
│ ├─php,29269 // worker 10
│ ├─php,29270 // worker 11
│ ├─php,29271 // worker 12
│ ├─php,29272 // worker 13
│ ├─php,29273 // worker 14
│ └─php,29274 // worker 15
├─{php},29251 // {php} 开头的都是 Master 里创建的线程,这里有 8 个
├─{php},29252
├─{php},29253
├─{php},29254
├─{php},29255
├─{php},29256
├─{php},29257
└─{php},29258

我们可以看到,Master 进程是多线程的,但是 Worker 是单线程的。

Master 进程

是一个多线程的程序。其中有一组很重要的线程,称之为 Reactor 线程。它就是真正处理 TCP 连接,收发数据的线程。

多线程使用 pthread 线程库。

Manager 进程

Manager 进程由 Master 进程 fork。

管理 Worker/Task Worker 进程,Worker/Task Worker 进程都是由 Manager 进程 fork 并管理的。

  • 子进程结束运行时,manager 进程负责回收此子进程,避免称为僵尸进程。并创建新的子进程。
  • 服务器关闭时(swoole server),manager 进程将发信号给所有子进程,通知子进程关闭服务。
  • 服务器 reload 时(swoole server),manager 进程会逐个关闭/重启子进程。

为什么不是 Master 进程呢,主要原因是 Master 进程是多线程的,不能安全的执行 fork 操作。

Reactor 线程

主线程(Master 进程)在 accept 新的连接后,会将这个连接分配给一个固定的 Reactor 线程,并由这个线程负责监听此 socket。在 socket 可读时读取数据,并进行协议解析,将请求投递到 Worker 进程。

  • 负责维护 TCP 连接、处理网络 IO、处理协议、收发数据。
  • 完全是异步非阻塞的模式
  • 除 Start/Shutdown 事件回调外,不执行任何 PHP 代码
  • 将 TCP 客户端发来的数据缓冲、拼接、拆分成完整的一个请求数据包
  • Reactor 以多线程的方式运行

分配的计算方式是 fd % serv->reactor_num

Worker 进程

类似于 php-fpm 进程。

  • 接受由 Reactor 线程投递的请求数据包,并执行 PHP 回调函数处理数据。
  • 生成响应数据并发给 Reactor 线程,由 Reactor 线程发送给 TCP 客户端
  • 可以是异步模式,也可以是同步模式
  • Worker 以多进程的方式进行

关系

可以理解为 Reactor 就是 nginx,Worker 就是 php-fpm。Reactor 线程异步并行地处理网络请求,然后再转发给 Worker 进程中去处理(在回调函数中处理)。Reactor 和 Worker 间通过 UnixSocket 进行通信。

一个更通俗的比喻,假设 Server 就是一个工厂,那 Reactor 就是销售,接受客户订单。而 Worker 就是工人,当销售接到订单后,Worker 去工作生产出客户要的东西。而 Task Worker 可以理解为行政人员,可以帮助 Worker 干些杂事,让 Worker 专心工作。

Reactor

它要求主线程(I/O 处理单元)只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作线程/进程(逻辑单元)。除此之外,主线程不做任何其他工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。

swoole 事件图

events

一个请求经历的步骤:

1、服务器主线程等待客户端连接 2、Reactor 线程处理连接 socket,读取 socket 上的请求数据(receive),将请求封装好后投递给 Worker 进程 3、Worker 进程就是逻辑单元,处理业务数据 4、Worker 进程结果返回给 Reactor 线程 5、Reactor 线程将结果写回 socket(Send)

所以,跟客户端交互的其实是 Reactor 线程,Reactor 线程作为沟通客户端跟后端的中介。

资源管理介绍

在 kubernetes 中,所有的内容都抽象为资源,用户需要通过操作资源来管理 kubernetes。

kubernetes 的本质上就是一个集群系统,用户可以在集群中部署各种服务,所谓的部署服务,其实就是在 kubernetes 集群 中运行一个个的容器,并将指定的程序跑在容器中。

kubernetes 的最小管理单元是 Pod 而不是容器,所以只能将容器放在 Pod 中,而 kubernetes 一般也不会直接管理 Pod, 而是通过 Pod 控制器 来管理 Pod 的。

Pod 可以提供服务之后,就要考虑如何访问 Pod 中的服务,kubernetes 提供了 Service 资源实现这个功能。

当然,如果 Pod 中程序的数据需要持久化,kubernetes 还提供了各种存储系统。

k8s-resource

外部访问的入口是 service。

学习 kubernetes 的核心,就是学习如何对集群上的 PodPod 控制器Service存储等各种资源进行操作。

yaml 注意事项

  1. 书写 yaml 切记 : 后面要加一个空格
  2. 如果需要将多段 yaml 配置放在一个文件中,中间要使用 --- 分隔

资源管理方式

  • 命令式对象管理:直接使用命令去操作 kubernetes 资源
1
kubectl run nginx-pod --image=nginx:1.17.1 --port=80
  • 命令式对象配置:通过命令配置和配置文件去操作 kubernetes 资源
1
kubectl create/patch -f nginx-pod.yaml
  • 声明式对象配置:通过 apply 命令和配置文件去操作 kubernetes 资源
1
2
# 适用于 创建或更新 资源
kubectl apply -f nginx-pod.yaml
类型 操作对象 适用环境 优点 缺点
命令式对象管理 对象 测试 简单 只能操作活动对象,无法审计、跟踪
命令式对象配置 文件 开发 可以审计、跟踪 项目大时,配置文件多,操作麻烦
声明式对象配置 目录 开发 支持目录操作 意外情况下难以调试

命令式对象管理

kubectl 命令

kubectl 是 kubernetes 集群的命令行工具,通过它能够对集群本身进行管理,并能够在集群上进行容器化应用的安装部署。kubectl 命令的语法如下:

1
kubectl [command] [type] [name] [flags]

command:指定要对资源执行的操作,例如 creategetdelete

type:指定资源类型,比如 deploymentpodservice

name:指定资源的名称,名称大小写敏感

flags:指定额外的可选参数

1
2
3
4
5
6
7
8
# 查看所有 pod
kubectl get pod

# 查看某个 pod
kubectl get pod pod_name

# 查看某个 pod,以 yaml 格式展示结果
kubectl get pod pod_name -o yaml

资源类型

kubernetes 中所有的内容都抽象为资源,可以通过下面的命令进行查看:

1
kubectl api-resources

经常使用的资源有下面这些:

  • 集群级别资源
    • nodes(no):集群组成部分
    • namespaces(ns):隔离 Pod
  • Pod 资源
    • pods(po):装载容器
    • replicationcontrollers(rc):控制 pod 资源
    • replicasets(rs):控制 pod 资源
    • deployments(deploy)控制 pod 资源
    • daemonsets(ds):控制 pod 资源
    • jobs:控制 pod 资源
    • cronjobs(cj):控制 pod 资源
    • horizontalpodautoscalers(hpa):控制 pod 资源
    • statefulsets(sts):控制 pod 资源
  • 服务发现资源
    • services(svc):统一 pod 对外接口
    • ingress(ing):统一 pod 对外接口
  • 存储资源
    • volumeattachments:存储
    • persistentvolumes(pv):存储
    • persistentvolumeclaims(pvc):存储
  • 配置资源
    • configmaps(cm):配置
    • secrets:配置

操作

kubernetes 允许对资源进行多种操作,可以通过 --help 查看详细的操作命令

1
kubectl --help

经常使用的操作有下面这些:

  • 基本命令
    • create:创建一个资源
    • edit:编辑一个资源
    • get:获取一个资源
    • patch:更新一个资源
    • delete:删除一个资源
    • explain:展示资源文档
  • 运行和调试
    • run:在集群中运行一个指定的镜像
    • expose:暴露资源为 Service
    • describe:现实资源内部信息
    • logs:输出容器在 pod 中的日志
    • attach:进入运行中的容器
    • exec:执行容器中的一个命令
    • cp:在 Pod 外复制文件
    • rollout:管理资源的发布
    • scale:扩(缩)容 Pod 的数量
    • autoscale:自动调整 Pod 的数量
  • 高级命令
    • apply:通过文件对资源进行配置
    • label:更新资源上的标签
  • 其他命令
    • cluster-info:显示集群信息
    • version:显示当前 Server 和 Client 的版本

下面以一个 namespace 的创建和删除简单演示下命令的使用:

1
2
3
4
5
6
# 创建一个 namespace
[root@master ~]# kubectl create namespace dev
namespace/dev created

# 获取 namespace
[root@master ~]# kubectl get ns