ggaaooppeenngg

为什么计算机科学是无限的但生命是有限的

Tensor 的用户 API 可以参考这里,这里做一下简单介绍。Tensor 是各种维度的向量和矩阵的统称,分 Tensor 和 SparseTensor。和 Tensor 不同,SparseTensor 存的是值以及值对应的 index,而 Tensor 存的是完整的矩阵。

举个例子。

1
2
3
4
5
6
7
8
import tensorflow as tf
a = tf.constant([1, 1])
b = tf.constant([2, 2])
c = tf.add(a, b)
sess = tf.InteractiveSession()
print("a[0]=%s, a[1]=%s" % (a[0].eval(), a[1].eval()))
print("c = %s" % c.eval())
sess.close()

输出对应如下。

1
2
a[0]=1, a[1]=1
c = [3 3]

Tensor 只有 eval 以后才能获得结果,是懒计算的。

Tensor 的实现

Tensor (tensorflow/tensorflow/core/framework/tensor.h) 依赖 TensorShape(tensorflow/tensorflow/tensorflow/core/framework/tensor_shape.h) 和 TensorBuffer (tensorflow/tensorflow/core/framework/tensor.h) 两个成员。

TensorShape 主要负责记录张量的形状。

TensorBuffer 主要负责管理 Tensor 的内存,TensorBuffer 继承自 RefCounted (tensorflow/tensorflow/core/lib/core/refcount.h),具有引用计数的功能,用于对内存进行管理。

1
2
3
4
5
// Interface to access the raw ref-counted data buffer.
class TensorBuffer : public core::RefCounted {
public:
~TensorBuffer() override {}
...

他们的对应的关系如下。

Tensor 从下往上看,其实就是一个带”形状“的内存,和 NumPy 的数组是差不多的。

OpKernel

对于一个线性回归来说,是最简单也最好理解的模型,方便分析底层的代码实现。

$$ Y=XW+b $$

损失函数用平方差定义的,优化器是提督下降,这样一个模型可以用一下的 Python 代码实现,这个代码片段是截取的,如果要完整运行这个例子可以在这里复现。

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
import tensorflow as tf
sess = tf.InteractiveSession()
x = tf.placeholder(tf.float32,[None, x_train.shape[1]])
y = tf.placeholder(tf.float32,[None, 1])
w = tf.Variable(tf.zeros([x_train.shape[1],1]))
b = tf.Variable(tf.zeros([1])) # placeholder 不用加 None
pred = tf.add(tf.matmul(x, w), b)

init = tf.global_variables_initializer()
cost = tf.reduce_mean(tf.square(y - pred))
optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001).minimize(cost)
epochs = 3000

init.run()

for epoch in range(0, epochs):
optimizer.run(feed_dict={x:x_train, y:y_train})
c = cost.eval(feed_dict = {x:x_train,y:y_train})
if epoch%100 == 0:
print_percentage(int(c*100))

print('\nEpoch: {0}, Error: {1}'.format(epoch+1, c))

b_value = b.eval()
w_value = w.eval()

# Predicted Labels
y_pred = pred.eval(feed_dict={x: x_test})

# Mean Squared Error
mse = tf.reduce_mean(tf.square(y_pred - y_test))
print("MSE", mse.eval())
sess.close()

对应的训练结果。

1
2
Epoch: 3000, Error: 0.25882452726364136
MSE 0.30620116674806463

可以看到,线性回归的模型主要依赖的两个 Operation 分别是 tf.addtf.matmul,其他的复杂模型也是类似的逻辑,对应的 OpKernel 分别是 AddOpMatMulOp,这里可以看一下具体的实现。

如果有源代码,可以连着源代码用 bazel 编译,可以参照这里自己编写一个 Op。

MatMulOp 的实现在 /tensorflow/core/kernels/matmul_op.cc 下面,定义在tensorflow/tensorflow/core/ops/math_ops.cc 下面。AddOp/tensorflow/core/kernels/matmul_op.cc,实现在 /tensorflow/core/kernels/cwise_op_add_1.cc 下面,依赖 /tensorflow/tensorflow/core/kernels/cwise_ops_common.hcommon 的定义。

Add 用的是 Eigenadd /tensorflow/tensorflow/core/kernels/cwise_ops.h,依赖third_party/Eigen/src/Core/functors/BinaryFunctors.h

举个例子,看一下 MatMulOpMatMulOp 的构造函数里面有一个 OpKernelConstruction 可以初始化 OpKernel,通过 OpKernel 可以获得这个 Op 的参数比如transpose_a 等等。

1
2
3
4
5
6
7
8
9
10
11
12
template <typename Device, typename T, bool USE_CUBLAS>
class MatMulOp : public OpKernel {
public:
explicit MatMulOp(OpKernelConstruction* ctx)
: OpKernel(ctx), algorithms_set_already_(false) { // 在执行构造函数之前,执行两个成员的构造函数
OP_REQUIRES_OK(ctx, ctx->GetAttr("transpose_a", &transpose_a_));
OP_REQUIRES_OK(ctx, ctx->GetAttr("transpose_b", &transpose_b_));

LaunchMatMul<Device, T, USE_CUBLAS>::GetBlasGemmAlgorithm(
ctx, &algorithms_, &algorithms_set_already_);
use_autotune_ = MatmulAutotuneEnable();
}

每个 OpKernel 都要实现一个 Compute 函数,可以看到这个 Compute 函数首先检查了两个 Tensor 是否是矩阵,然后检查两个矩阵的形状是否符合矩阵相乘的条件,然后根据形状分配 TensorShape 并且根据 TensorShape 分配新的 Tensor (其实顺便分配的 TensorBuffer 的内存空间)。然后通过 LaunchMatMul 真正执行相乘操作,因为这个计算过程,可能是用了 GPU,所以模版是带 Device 的(GPUDevice/CPUDevice)。

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
void Compute(OpKernelContext* ctx) override {
const Tensor& a = ctx->input(0);
const Tensor& b = ctx->input(1);

// Check that the dimensions of the two matrices are valid.
OP_REQUIRES(ctx, TensorShapeUtils::IsMatrix(a.shape()),
errors::InvalidArgument("In[0] is not a matrix"));
OP_REQUIRES(ctx, TensorShapeUtils::IsMatrix(b.shape()),
errors::InvalidArgument("In[1] is not a matrix"));
Eigen::array<Eigen::IndexPair<Eigen::DenseIndex>, 1> dim_pair;
dim_pair[0].first = transpose_a_ ? 0 : 1;
dim_pair[0].second = transpose_b_ ? 1 : 0;

OP_REQUIRES(
ctx, a.dim_size(dim_pair[0].first) == b.dim_size(dim_pair[0].second),
errors::InvalidArgument(
"Matrix size-incompatible: In[0]: ", a.shape().DebugString(),
", In[1]: ", b.shape().DebugString()));
int a_dim_remaining = 1 - dim_pair[0].first;
int b_dim_remaining = 1 - dim_pair[0].second;
TensorShape out_shape(
{a.dim_size(a_dim_remaining), b.dim_size(b_dim_remaining)});
Tensor* out = nullptr;
OP_REQUIRES_OK(ctx, ctx->allocate_output(0, out_shape, &out));

if (out->NumElements() == 0) {
// If a has shape [0, x] or b has shape [x, 0], the output shape
// is a 0-element matrix, so there is nothing to do.
return;
}

if (a.NumElements() == 0 || b.NumElements() == 0) {
// If a has shape [x, 0] and b has shape [0, y], the
// output shape is [x, y] where x and y are non-zero, so we fill
// the output with zeros.
functor::SetZeroFunctor<Device, T> f;
f(ctx->eigen_device<Device>(), out->flat<T>());
return;
}

LaunchMatMul<Device, T, USE_CUBLAS>::launch(
ctx, a, b, dim_pair, &algorithms_, use_autotune_, out);
}

LaunchMatMul 继承自 LaunchMatMulBase,在 LaunchMatMulBase 当中调用了 functor::MatMulFunctor,这个 functor 主要就会执行乘法操作,在这之前会检查一下是否其中一个元素是 vector,这样可以直接优化算出来,而不用 Eigen 库来算,这样更快,这个目前看到的是 CPU 的路径。

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
template <typename Device, typename T>
struct LaunchMatMulBase {
#if GOOGLE_CUDA
typedef se::blas::AlgorithmType AlgorithmType;
#else
typedef int64 AlgorithmType;
#endif // GOOGLE_CUDA

static void launch(
OpKernelContext* ctx, const Tensor& a, const Tensor& b,
const Eigen::array<Eigen::IndexPair<Eigen::DenseIndex>, 1>& dim_pair,
std::vector<AlgorithmType>* algorithms, bool use_aututone, Tensor* out) {
#ifndef TENSORFLOW_USE_SYCL
// An explicit vector-matrix multiply is much better optimized than an
// implicit one and this is a bottleneck during non-batched inference.
bool was_vector = ExplicitVectorMatrixOptimization<T>(a, b, dim_pair, out);
if (!was_vector) {
#endif // TENSORFLOW_USE_SYCL
functor::MatMulFunctor<Device, T>()(ctx->eigen_device<Device>(),
out->matrix<T>(), a.matrix<T>(),
b.matrix<T>(), dim_pair);
#ifndef TENSORFLOW_USE_SYCL
}
#endif // TENSORFLOW_USE_SYCL
}

static void GetBlasGemmAlgorithm(OpKernelConstruction* ctx,
std::vector<int64>* algorithms,
bool* algorithm_set_flag) {}
};

MatMulFunctor 在设备 d 上计算矩阵相乘的结果,其中调用的是 MatMul<CPUDevice>

1
2
3
4
5
6
template <typename Device, typename In0, typename In1, typename Out,
typename DimPair>
void MatMul(const Device& d, Out out, In0 in0, In1 in1,
const DimPair& dim_pair) {
out.device(d) = in0.contract(in1, dim_pair);
}

这里的 contract 调用的是 TensorContractionOp (third_party/unsupported/Eigen/CXX11/src/Tensor/TensorContraction.h),跟之前说的一样,这个 Op 是计算图的一部分,要通过 eval 来做计算,计算结果是 eval 驱动的。TensorContractionOp 的构造函数就是,这负责构建左表达式和右表达式。

1
2
3
EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE TensorContractionOp(
const LhsXprType& lhs, const RhsXprType& rhs, const Indices& dims)
: m_lhs_xpr(lhs), m_rhs_xpr(rhs), m_indices(dims) {}

真正的计算过程在 TensorContractionEvaluatorBase 里面,真正执行计算过程,计算细节就省略了主要是矩阵相乘。

CUDA

如果条件编译 GOOGLE_CUDA 的话,会使用 GPU 的代码,对应会调用到 steam executor,这个以后具体分析。

总结

Tensorflow 基于图模型的,并且是懒计算的,通过扩展可以自己用 C++ 实现新的 Op,并且也可以观察默认自带的 OpKernel 是如何实现的,对于理解 Tensorflow 的工作流程会有很大的帮助。Tensorflow 本身依赖了 Eigen,CUDA 等线性代数库或者 GPU 计算库,要看懂代码还是要多学一点线代的知识,比如 Contraction 这个概念我也是第一次晓得。

参考文献

  1. 深入理解 Tensorflow 架构设计与原理实现

Raft 的作者其实对 Paxos 研究得很深,毕竟 Paxos 基本上就是共识算法的代名词,建议在了解 Raft 之前先看看 Paxos,因为 Paxos 里面有一段从单点开始完整的推断共识算法的正确性和必要性的过程,方便建立一种对共识算法上直觉的认同。Raft 论文最好看作者的博士论文,那个比较完整。

leader election

Raft 有 Term 的概念,把时间每分成了一个个的任期,每个任期开始是选举过程。Raft server 有三种角色,Candidate 就是选举中的状态,Leader 是当选的状态,Follower 是服从并选择接受 Leader 的 Log 的状态。

其实可以把选主看作一个 basic Paxos,大家对 leader=? 达成一致,触发的条件是超时和初始化的时候,term 就是类似于 Paxos 的 propose number,和 basic Paxos 不一样的是每个 term 只能投一个人,所以每个 term 不会有冲突,只有一个人会当选,但是会有平票的问题,大家用同一个 term 选举的情况下就会发生(比如说大家都投自己,谁都不接受谁),所以为了减少冲突,把重试的时间随机化,快速超时的人能拿到高 term,并且其他慢速超时的 candidate 会服从这个先拿到下一个 term 的 leader。成为 leader 的标志是收到了大部分人的服从(3/5 或者 2/3)。

log replication

log 不一致的就以上几种情况,可能少了 entry,可能多了没 commited 的 entry,可能又少了 entry 还多了没 commited 的 entry,log 不一致的情况就这几种。index term 能唯一确定一个 entry,如果一个 entry 确定,之前的也确定了。基于上面的性质,如果 entry commited 之前的就是都 commited 了。这个通过 append 的 prev index prev term,来保证这个特性。解决方法是在 AppendRequest 里面带上新的 entry 前的 index 和 term(在 leader 那里存的前一个),如果这个 entry 不在 follower 里面的话,就拒绝追加新 entry,然后 leader 就要用前一个 index 尝试,直到开始出现 match 的点,有点像 git tree 里面开始产生分支的那个 base,这个过程一直减少自己保存的 follower 的 nextIndex,一直到获得和自己 match 的最后一个 index 开始追加复制自己的 log。也就是说这样会强迫所有的 follower 复制主节点的 log,这里有一个按 term 回溯 base 的方法,但是这个优化作者认为有点多此一举,毕竟错误发生的情况比较少。这里会有一个问题(啥问题),后面会通过选举的时候加上限制解决。

这个问题就是,新的 leader 可能没有之前 commited 的 log,然后修改了其他 follower 的 log,导致每个 state machine 执行的命令是不一致的,所以要加一个约束,在投票的时候,新的 leader 必须包含之前 commited 的 log,这个保证就需要通过定义 entry 的 update-to-date,在投票的时候拒绝比自己老的 leader 当选。

safety

这个问题被称作 safety,为了解决这个问题 update-to-date 怎么定义呢?比如下面这个例子,如果 S3 挂了,其实 index 5 已经 commited 了,但是没有办法确定 index 5 的这个 log 是否 commited 了。所以要选个最可能包含所有 log 的做 leader。

先比 term,再比 log 数量,按顺序比,有点像字符串比较,这样可以确保 leader 有其他 follower 的 entry。没有大多数人的又新又全就无法成为 leader。

commit from previous entry

如果一个 leader 在 commit 之前 crash 了,并且新的 leader 没有已经在大多数节点上的 entry 可能会覆盖掉本该 commit 的 entry。

比如这图,如果是 (d1) 的情况,3 就会把 2 盖掉,但是实际上 2 本该 commited 了,但是在 commit 之前 S5 可能在 term 5 成为了主(这个没有违背上面的 upda-to-date 因为 3 确实比 2 要新,并且可以从 S2, S3, S4 那里获得选票)。

光看数量,没办法替之前 term 的 entry commit,因为 d1 的情况,可能就把 2 盖掉了。只有 term 4 的时候 4 commit 了(当前的 term 可以 commit)才能替之前 term 的 entry commit(实际上间接 commit 了,这个可以证明),这样 term 3 就不会成为主,就不可能盖掉 2。

总结一下,在 Raft 中 leader 不会 commit 之前 term 的 entry,只 commit 当前 term 的 entry。当前 term 的 entry commit 了,之前 term 的 entry 就间接 commit 了。

持久化数据

当前的 term 和 vote 。

membership change

raft conf change 有几个主要阶段,其中 C(old)+c(new) 的情况下,需要服从两个 conf 的大多数,并且,每次只能一个一个的增加 server 和 减少 server。

参考文献

Paxos 是什么

Paxos is a mechanism for achieving consensus on a single value over unreliable communication channels.

Paxos 就是一个在不稳定的网络环境下建立的对一个值的共识。我们说的 Multi-Paxos 是指对多个值的共识,不过我们先一步一步来。

在 Paxos 中是没有 leader 这个概念的,所以相对来说会比较慢,因为谁都可以处理请求并且把自己的 peer 盖掉导致冲突,达成一致的过程会更长。

Paxos 只抵抗机器崩溃,网络异常,不抵抗恶意行为的节点,或者说使用不同协议的参与者,或者说参与者不能撒谎,所以经典的 Paxos 不抵抗拜占庭问题(也有针对拜占庭问题的 Paxos 带了 verify 的过程)。

Paxos 论文里面描述的算法,其实不是很清晰,一般人看了都会有疑问,所以后来很多人对这个算法做了补充和解释,包括作者本人。

基本需求

第一个是安全性,就是只有一个值会被选择,并且节点对这个值不会主动知道,而是在值被选择以后被动学习知道这个值被选择的(这个原文有点难懂我换成了自己的话解释了一下)。
第二个是活性,也就是值最终会被选择,并且所有节点会最终学习到这个值被选择了。

解决方案

首先单点可以排除,因为单点虽然是最容易解决一致性问题的,但是如果单点挂了,整个就不可用了,所以显然不能依靠单点。

那如果每个节点就接受自己接受到的第一个值,也会有平票问题(split votes)。redblue 对等的,那到底谁被选择了呢,所以来了就接受的策略并不可行。

Paxos 是 quorum based 的,表示的一致性协议是少数服从多数的,在大多数节点都接受了这个值以后,这个值就被选择了。为了让节点可以接受多个值,多个值之间需要区分,所以就有了提交号,这个号码是单增的,拒绝掉小号的提交,并且当一个值已经被选择,那么之后的提交都要提交这个值,这样做的目的是让提交者知道这个值被学习了,是大家认可的一个值。有了少数服从多数原则,就会碰到冲突的问题。

这种情况就是下面这样,从时间上来讲,red 已经被选择了,如果 S3 能够拒绝 red 的提交,那么 S3,S4,S5就可以拿着 red 重试,并且知道 red 已经被大多数人接受了,而知道冲突的 S3 就会决绝这次请求,这个点上就处理了两个值(接受了一个拒绝了一个)。

序列号可以帮助我们区分优先关系,这样因为网络问题延迟的请求就可以被处理。下面这种情况,red 虽然先提交,但是并不是先被大多数接受的(存在网络延迟),这个时候blue已经被选择了(被大多数人接受),我们需要提交 red 的提交意识到自己已经太“老”了,而触发这就是接受了 blueS3

所以你会发现,矛盾的点其实就是这个 S3,也就是少数服从多数原则,能保证任意的大多数都是有交集的。交集中的点会发现矛盾和之前接受的值有矛盾选择拒绝。

问题:三节点的容忍度是 1,四节点的容忍度是多少?

答案:也是 1,因为要形成发现矛盾的交集对于 4 来说,要达到 3/4,才能构成大多数,这就是为什么集群选单数的原因,因为双数从算法的角度来说没什么帮助。

接下来看具体的算法。

其实,从朴素角度来说,经典 Paxos 看起来就是一个两阶段提交的过程,首先是准备阶段,选择一个提交号 n,提交 prepare(n),接受者需要返回自己接受的值,和已经接受的提交号。当从大多数收到回复以后就可以做判断了,如果有返回接受值,选择提交号最大的值进行下一阶段(这个行为对应的是发现有值可能被接受了,尝试服从或者学习这个接受),不然就可以用自己的值进行下一阶段。

下一阶段就是 accept(value,n),如果接受者发现自己目前收到的n,没有比accpet给的n大,就接受这个值,并且更新自己的n,否则就拒绝(这里就保证提交者能够发现自己变老了或者被拒绝了)。
如果接受者发现提交号大于自己当前的最大提交号,就接受这个值,不然就拒绝。当提交者从大多数人那里接受到返回以后发现有拒绝的情况,就进行重试拿一个新的n开始,否则这个值就被接受了。

Basic Paxos value 就是设置一次,不存在再设次一次的情况。

总结起来就是如图:

那这样的一个二阶段提交,看看能不能解决前面的问题,主要有三种可能。
注意,这里的例子是原文里的例子,一个变量的取值是X或者Y,然后3.1 表示 S1提交的提交号为 3 的提交,这是我们定义的 message ID。

第一种值被选择了,后者意识到了X,放弃 YX 提交,也就意味着提交者学习到了这个值已经被选择了。

第二种情况,值没有被选择,但是交集的部分看到了一个被选择的值,也会选择放弃YX提交,虽然X暂时没有被选择(被大多数人接受),但是可以保证两个提交都成功。

第三种情况,值没有被选择,同时交集的部分也没有发现被选择的值,如果已经做了 promies 这个时候就会拒绝老的提交。比如下面在 S3 accept(X) 的时候就会放弃提交并且进行重试(S1 S2 也会一起重试), 并且在重试的时候覆盖掉原先接受的x)。

看起来所有的问题都解决了,但是活性问题无法保证。这个情况发生在提交之间相互阻塞的情况,S3 S4 S5 拿着更高的提交号导致 S1 S2 S3 的 accept 被拒绝重新进行提交,又把 S3 S4 S5 给拒绝了。

解决这个问题的办法就是把重试时间进行一些随机化,减少这种巧合发生,或者把重试的时间指数增长等等。

Multi-Paxos

到此 classic Paxos 算是告一段落。那 Paxos 有什么问题呢。首先是活性的问题,这个在后面可以通过选主的方式解决。其次是学习是通过 propose 获知值的,不然无法知道一个值是否被接受了,要走一遍整套的 Paxos 协议。

Multi-Paxos 新增的问题是如何选择 log entry,并且用选主的方式减少冲突,以及减少 prepare 的请求。

以下图为例。深色框表示这个 log 已经被选择(怎么确定选择后面会提到)。寻找 log entry 的方式就是寻找第一个没有被选择的 log,尝试执行 basic Paxos,如果有值被选择会尝试用这个 log 提交帮助这个 log 被选择,这个过程和 basic paxos 是一致的,但是在此之后就要继续寻找下一个 log 尝试进行我们的尝试。

下图中(只看 S1 和 S2 这两行),第一次会找到最后一个没有没学习的 log,也就是 index=3 的 log,但是发现了 s1cmp,就会服从这个 cmp,然后到 index=4 发现了 s2sub,也选择服从,并且学习到了 index=4 的 log 应该是 sub,最后到了 index=5 才进行插入。

这样的情况下,Paxos 可以接受并发的请求,而 Raft 却规定了对 log 只能 append,不能 3 4 5 都能同时处理,简化了实现。反正只要保证了 log 的顺序一致,状态机的最终状态都是一致的。

接下来的是对于性能的一些提升,一个是选主避免大面积冲突,另一个是优化二阶段提交的次数。解决方法是选出一个主,保证一次只有一个提交者。另外对于 prepare 是对整个 log 进行 prepare,而不是单个 log entry,这样大多数情况下,大部分 log 都可以一次性就被选择。

可以通过 lease based 选主。 lamport 提出的方式比较简单,节点之间维持T间隔的心跳,2T 之内没有收到更高编号的主的心跳就成为主,非主则转发请求给主,这样还是不会避免同时出现两主的情况,两主会有冲突,但 Paxos 就算有两主还是能正常运行,毕竟有主只是优化方式。

接下来我们看看优化提交的过程。回忆一下,prepare 的作用,其中一点是帮助我们发现冲突,知道有值可能被选择了,另外一点是拒绝老的提交,让他们发现自己变“老”了。

我们可以改变提交号的意义,让他代表整个 log,也就是所有 log entry 都用一个提交号,这样接受者可以通过返回 noMoreAccepted 让提交者意识到在当前 log entry 之后的 log 都没有 accpeted 是可以被“锁”住的,然后如果大多数节点返回 noMoreAccepted,就可以跳过之后的 prepare 直到 accept 被拒绝。这样后续的 accept 操作就可以不用 prepare 在一趟之内解决,所以二阶段提交的第一阶段的 promise 落在了整个 log 上。这个情况下 发起者的 accept 一直顺序进行就可以,问题发生在有其他主(之前说到的会临时有多主的情况),又提交了 prepare,然后这个提交号更高,把后一段锁住了,accept 就会发现冲突学习新的值。

补充 1 持续发送 accept 请求,让所有的节点都同步这个 log。
补充 2 让 proposer 带上 firstUnchosenIndex 让 acceptor 知道大多数可以确认被选择的 log。

但是还有问题,例如下面这张图,对于所有小于 firstUnchosenIndex 的 i 来说,如果 accpetedProposal[i] 的提交号和 request 的 proposal 一样的话,就可以确认 log[i] 是被选择了的,并且标记 acceptedProposal[i] = 无限。但是 2.5 确实来自之前的 leader 的,貌似无法被标记为选择了。这需要我们进一步修改这个协议。

这个时候需要在 accept 返回的时候带上自己的 firtUnchosenIndex,如果 proposer 的比这个大,可以把 acceptor 直接补齐。用 success 命令,让 acceptor 直接更改index 的这个值, 并且继续返回 firtUnchosenIndex,让 proposer 弥补。

最后一部分是配置改变,degree of replication,也就是要保证同时只有一个 conf 生效,可以用 a 个之前的 log 做 conf,这样在使用这个 conf 之前,conf 已经发生了改变。但是同时限制住了并发度(只能为a)。直觉上讲就是约定一个 index 在某个时间点一次性切换。

参考文献

最新的 kubeadm 的设计文档在这里,如果把里面设计大概看一遍就能够理解里面的流程了,可以说设计的还是很缜密的,并且大大简化了 k8s 的运维工作。

kubeadm 安全设施

主要解释 kubeadm init 和 kubeadm join 的过程和实现

kubeadm init

  1. 首先进行 preflight-checks ,检查系统是否满足初始化的状态。
  2. 创建自签名的 CA,并且生成和签发各个 component 的私钥和证书 (/etc/kubernetes/pki),如果文件已存在就不会再生成了,比如要给 apiserver 添加域名可以重新签发一个证书,然后重启就好了。
  3. 写入各个服务的配置文件,以及一个 admin.conf (/etc/kubernetes/)
  4. 配置 kubelet 的动态配置加载 (disable by default)
  5. 配置静态 pod (/etc/kubernetes/manifests)
  6. 给 master 添加 taint 和 label,让其他 pod 默认不会运行在 master 上
  7. 生成用于让其他 kubelet 加入的 token
  8. 配置用 token 加入的可以自动确认 CSR(也就是用 CA 自动签 kubelet 的证书)
  9. 设置 kube-dns
  10. 检查 self-hosting,如果设置了,就把 static pod 转成 daemonset

是否使用外部 CA 的条件是,目录下有 CA 证书,但是没有 CA 私钥。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if res, _ := certsphase.UsingExternalCA(i.cfg); !res {

// PHASE 1: Generate certificates
if err := certsphase.CreatePKIAssets(i.cfg); err != nil {
return err
}

// PHASE 2: Generate kubeconfig files for the admin and the kubelet
if err := kubeconfigphase.CreateInitKubeConfigFiles(kubeConfigDir, i.cfg); err != nil {
return err
}

} else {
fmt.Println("[externalca] The file 'ca.key' was not found, yet all other certificates are present. Using external CA mode - certificates or kubeconfig will not be generated.")
}

具体的生成 PKI 相关的配置的过程

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
// CreatePKIAssets will create and write to disk all PKI assets necessary to establish the control plane.
// If the PKI assets already exists in the target folder, they are used only if evaluated equal; otherwise an error is returned.
func CreatePKIAssets(cfg *kubeadmapi.MasterConfiguration) error {

certActions := []func(cfg *kubeadmapi.MasterConfiguration) error{
CreateCACertAndKeyFiles,
CreateAPIServerCertAndKeyFiles,
CreateAPIServerKubeletClientCertAndKeyFiles,
CreateEtcdCACertAndKeyFiles,
CreateEtcdServerCertAndKeyFiles,
CreateEtcdPeerCertAndKeyFiles,
CreateEtcdHealthcheckClientCertAndKeyFiles,
CreateAPIServerEtcdClientCertAndKeyFiles,
CreateServiceAccountKeyAndPublicKeyFiles,
CreateFrontProxyCACertAndKeyFiles,
CreateFrontProxyClientCertAndKeyFiles,
}

for _, action := range certActions {
err := action(cfg)
if err != nil {
return err
}
}

fmt.Printf("[certificates] Valid certificates and keys now exist in %q\n", cfg.CertificatesDir)

return nil
}

列表中的函数都是用来生成所有证书和私钥的,主要依靠 k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/pkiutil 来完成。私钥很容易生成,没什么需要特别配置的,主要看 CA 的证书里面配了啥,因为这个跟 k8s 的鉴权有关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// NewSelfSignedCACert creates a CA certificate
func NewSelfSignedCACert(cfg Config, key *rsa.PrivateKey) (*x509.Certificate, error) {
now := time.Now()
tmpl := x509.Certificate{
SerialNumber: new(big.Int).SetInt64(0),
Subject: pkix.Name{
CommonName: cfg.CommonName,
Organization: cfg.Organization,
},
NotBefore: now.UTC(),
NotAfter: now.Add(duration365d * 10).UTC(),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
}

certDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &tmpl, &tmpl, key.Public(), key)
if err != nil {
return nil, err
}
return x509.ParseCertificate(certDERBytes)
}

openssl x509 -in /etc/kubernetes/pki/ca.crt -text -noout 可以查看 ca 证书里面的内容,和我们看到的配置是一致的。

然后我们再看一下 apiserver 的私钥和证书是怎么签的,首先把生成的 CA 证书和私钥加载进来,然后生成私钥并且签出自己的证书。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func CreateAPIServerCertAndKeyFiles(cfg *kubeadmapi.MasterConfiguration) error {

caCert, caKey, err := loadCertificateAuthority(cfg.CertificatesDir, kubeadmconstants.CACertAndKeyBaseName)
if err != nil {
return err
}

apiCert, apiKey, err := NewAPIServerCertAndKey(cfg, caCert, caKey)
if err != nil {
return err
}

return writeCertificateFilesIfNotExist(
cfg.CertificatesDir,
kubeadmconstants.APIServerCertAndKeyBaseName,
caCert,
apiCert,
apiKey,
)
}

这里比较重要,因为 SAN 这个是用来匹配域名的,如果这里没写好,HTTPS 是拒绝访问的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// NewAPIServerCertAndKey generate certificate for apiserver, signed by the given CA.
func NewAPIServerCertAndKey(cfg *kubeadmapi.MasterConfiguration, caCert *x509.Certificate, caKey *rsa.PrivateKey) (*x509.Certificate, *rsa.PrivateKey, error) {

altNames, err := pkiutil.GetAPIServerAltNames(cfg)
if err != nil {
return nil, nil, fmt.Errorf("failure while composing altnames for API server: %v", err)
}

config := certutil.Config{
CommonName: kubeadmconstants.APIServerCertCommonName,
AltNames: *altNames,
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
apiCert, apiKey, err := pkiutil.NewCertAndKey(caCert, caKey, config)
if err != nil {
return nil, nil, fmt.Errorf("failure while creating API server key and certificate: %v", err)
}

return apiCert, apiKey, nil
}

apiserver 的证书也是对的,马赛克的部分是我自己配的一些 IP 和 域名,和 kubeadm 配的一些 kube-dns 用的 overlay 网络上的 master 域名,其他 master 上的 components 也是类似的,在配高可用的时候要把每个 master 节点上的域名和 IP 都配上。kubelet 的证书也要配对,就在 join 里面介绍了,这个会用来鉴权 node 的身份。

其他部分的代码和设计文档的描述是一致的,所以没什么好看的,感觉一个好的设计文档是非常重要的,代码只是设计的实现,如果设计本身就可读性很强,阅读代码只是辅助理解一些细节和找 BUG 用的而已。

kubeadm join

  1. 首先用 token 鉴权的方式获取 apiserver 的 CA 证书,并且通过 SHA256 验证。
  2. 加载动态配置,如果 master 上有的话。
  3. TLS 初始化,首先用 token 鉴权的方式把自己的 CSR 发给 apiserver,然后签发自己的证书,这个证书要用来检查 node 的身份的。
  4. 配置 kubelet 和 server 开始建立连接。

新版本的 kubelet 有些变动,支持把一些 featuregate 配置写到文件里面,比如这个阻止 fork bomb 的配置,新版本只能用配置文件写了,不能通过参数配置。

kubeadm 的详细过程如下:

首先会把 flags 传入 NodeConfiguration 中,开始 AddJoinConfigFlags(cmd.PersistentFlags(),如果没有 nodeNamecfg 默认用 host 的 name 并且小写化通过 GetHostname获得,和在宿主机上执行 hostname 是一致的。在初始化之前先 尝试启动 TryStartKubelet 过程,然后把 token 和 server 写入(证书 data 是怎么生成的?),先配置kubelet-bootstrap-config

整体流程如下,token 验证是基于 JWT 的,所以 token 的格式是 "^([a-z0-9]{6})\\.([a-z0-9]{16})$",分两部分,tokenID.tokenSecret

1
2
3
4
5
6
7
8
discovery.For
->GetValidatedClusterInfoObject 这个是获取 CA 证书的过程
->token.RetrieveValidatedClusterInfo 这一步是 JWT 验证
->tokenutil.ParseToken 得到 tokenID 和 tokenSecret
->pubKeyPins.Allow 加载用于检验 CA 证书的 HASH 值
->buildInsecureBootstrapKubeConfig (用 token-bootstrap-client 身份)获取信息
-> 从kube-public 获取 configmap (和 kubectl describe configmap cluster-info -n kube-public 中的信息是一致的)这个configmap 里面包含 JWS 签名和 master 的 CA 证书,获取对应 tokenID 的 jws token,验证 token 成功,并且验证 CA 的证书 hash,如果通过说明这个 master 是可信的,然后拿到 CA 证书以后开始构建自己的证书,建立 secure config。
->

kubeconfigutil.WriteToDisk 会把 bootstrap-kubelet.conf 写入到配置目录中,kubeadm 的任务就完成了,之前的 kubeadm 会代替 kubelet 生成 kubelet.conf(其中用的是公钥鉴权),现在移走了,kubelet 启动的时候会尝试使用这个配置文件建立 HTTPS 的鉴权配置文件。

可以看一下 kubelet 用这个配置给 master 发 CSR 以后得到了什么,可以看到是生成了自己的证书的。

并且这个证书里面的 SAN 也是用来进行 Node 鉴权的身份确认的信息。用 openssl x509 -in /var/lib/kubelet/pki/kubelet-client.crt -text -noout查看信息。

红线部分就是 Node 鉴权的信息,基于这个确认 node 的身份。另外一个是 Role Based Access Control,那个主要是配给 pod,限制 pod 行为用的,类似于 linux 的 chmod

kubeadm 高可用配置

如果没有安全配置,其实高可用挺好配置的,现在加入了安全配置,虽然麻烦,但是还是能理解 k8s 的良苦用心的,理解了整个鉴权的过程以后就可以做,现在支持配置文件初始化其实更好配置了。可以基于这个文档配置,算了我还是不总结了,看懂上面的,照着配置就好了。主要是自己生成 CA,他们用的 cfsslcfssljson,这个工具比 openssl 好用一点,比较容易配置。生成 ca 证书和私钥以后,就可以构建 HTTPS 的 etcd。

先创建 ca 的配置文件 ca-config.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
{
"signing": {
"default": {
"expiry": "43800h"
},
"profiles": {
"server": {
"expiry": "43800h",
"usages": [
"signing",
"key encipherment",
"server auth",
"client auth"
]
},
"client": {
"expiry": "43800h",
"usages": [
"signing",
"key encipherment",
"client auth"
]
},
"peer": {
"expiry": "43800h",
"usages": [
"signing",
"key encipherment",
"server auth",
"client auth"
]
}
}
}
}

然后生成用于自签名的 csr 的配置文件 ca-csr.json,用于签发自签名的 CA 证书。

1
2
3
4
5
6
7
{
"CN": "etcd",
"key": {
"algo": "rsa",
"size": 2048
}
}

结果就是目录下面生成了,ca-key.pemca.pem,这个命令不太按规则,对应的叫 ca.keyca.crtpem 是密钥保存的格式。

接下来用 client.json 获得自己的私钥和通过 ca 签的证书。

1
2
3
4
5
6
7
{
"CN": "client",
"key": {
"algo": "ecdsa",
"size": 256
}
}

执行 cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=client client.json | cfssljson -bare client,生成了 client-key.pem,client.pem,分别是私钥和证书,把文件 ca.pem,ca-key.pem, client.pem,client-key.pem,ca-config.json, 拷贝到每台 master 机器上面,一般放到 /etc/kubernetes/pki/ 下面。

然后

1
2
3
4
5
6
7
cfssl print-defaults csr > config.json
sed -i '0,/CN/{s/example\.net/'"$PEER_NAME"'/}' config.json
sed -i 's/www\.example\.net/'"$PRIVATE_IP"'/' config.json
sed -i 's/example\.net/'"$PEER_NAME"'/' config.json

cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=server config.json | cfssljson -bare server
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=peer config.json | cfssljson -bare peer

签出出两对密钥和证书,把每台的机器的 域名 和 IP 替换掉示例的 example 配置,就是这些地方可以改成自己的域名和地址,这个会被用来 check。

可以放到 kubelet 的 static pod 里面,启动 etcd,两个证书分别是用作 server 验证和 client 验证的,server 让别人访问的时候相信你,peer 是你访问别人的时候让别人相信你。

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
61
62
63
64
65
66
cat >/etc/kubernetes/manifests/etcd.yaml <<EOF
apiVersion: v1
kind: Pod
metadata:
labels:
component: etcd
tier: control-plane
name: <podname>
namespace: kube-system
spec:
containers:
- command:
- etcd --name ${PEER_NAME} \
- --data-dir /var/lib/etcd \
- --listen-client-urls https://${PRIVATE_IP}:2379 \
- --advertise-client-urls https://${PRIVATE_IP}:2379 \
- --listen-peer-urls https://${PRIVATE_IP}:2380 \
- --initial-advertise-peer-urls https://${PRIVATE_IP}:2380 \
- --cert-file=/certs/server.pem \
- --key-file=/certs/server-key.pem \
- --client-cert-auth \
- --trusted-ca-file=/certs/ca.pem \
- --peer-cert-file=/certs/peer.pem \
- --peer-key-file=/certs/peer-key.pem \
- --peer-client-cert-auth \
- --peer-trusted-ca-file=/certs/ca.pem \
- --initial-cluster etcd0=https://<etcd0-ip-address>:2380,etcd1=https://<etcd1-ip-address>:2380,etcd2=https://<etcd2-ip-address>:2380 \
- --initial-cluster-token my-etcd-token \
- --initial-cluster-state new
image: k8s.gcr.io/etcd-amd64:3.1.10
livenessProbe:
httpGet:
path: /health
port: 2379
scheme: HTTP
initialDelaySeconds: 15
timeoutSeconds: 15
name: etcd
env:
- name: PUBLIC_IP
valueFrom:
fieldRef:
fieldPath: status.hostIP
- name: PRIVATE_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: PEER_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
volumeMounts:
- mountPath: /var/lib/etcd
name: etcd
- mountPath: /certs
name: certs
hostNetwork: true
volumes:
- hostPath:
path: /var/lib/etcd
type: DirectoryOrCreate
name: etcd
- hostPath:
path: /etc/kubernetes/pki/etcd
name: certs
EOF

然后每个节点的 master init 的配置文件按照下面这个配置,client.pem 是用来让 etcd 相信 apiserver 的。private-ipload-balancer-ip 都是要写到证书的 SAN 里面的,不然用这些 ip 是访问不了 apiserver 的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: kubeadm.k8s.io/v1alpha1
kind: MasterConfiguration
api:
advertiseAddress: <private-ip>
etcd:
endpoints:
- https://<etcd0-ip-address>:2379
- https://<etcd1-ip-address>:2379
- https://<etcd2-ip-address>:2379
caFile: /etc/kubernetes/pki/etcd/ca.pem
certFile: /etc/kubernetes/pki/etcd/client.pem
keyFile: /etc/kubernetes/pki/etcd/client-key.pem
networking:
podSubnet: <podCIDR>
apiServerCertSANs:
- <load-balancer-ip>
apiServerExtraArgs:
apiserver-count: "3"
EOF

至于怎么做 loadbalance 可以用七层的也可以用四层的,七层把证书配到负载均衡服务上,四层的就不用,自己在裸机器上做可以用 vip + nginx 做一个四层的,也可以把证书放到 nginx 上做七层的,但是在云环境下,都不怎么支持自己配置 vip,需要用云厂商的 lb 服务,这个就看具体提供商的服务怎么配了。

首先公钥念“gongyue”,而不是公钥,拼音打字打多了就发现得这么读。

HTTPS 是基于 SSL/TLS 的安全 HTTP 协议,其实 HTTPS 主要还是看安全套接字层,在这之上还是一个 HTTP 协议,这里主要总结一下我们现在主流用到的加密体系,比如我们经常看到的 .pem.crt.key.csr 还有 CA 啊之类的是啥东西,并且这些东西都如何工作和应用的。

在密码学里面,有几个角色,类似于中国的甲乙丙丁,一个是 Alice 和 Bob,这是正常通信的两个人,还有一个是 Eve,是信道上具备窃听能力的人,另外一个是 Mallory,这个人可以妨碍网络流量,主动攻击。

对称加密

其实最简单的加密算法就是对称加密

Alice 和 Bob 有一个共有的密码,也就是只有两个人拥有,相互传输的密文只能通过这个密钥解开。

首先加密需要基于密钥,凯撒密码可不可以,可以是可以,但是被人知道算法以后,就可以被所有人破解,并且优秀的加密算法应该被人验证,如果不是公开的算法就没有办法验证,当然一些保守的加密方法会不公开,这样其实也很难解密,但是互联网的加密协议很显然是用在千家万户的,所以一定要是经得起推敲的加密算法。

分组密码

分组密码的作用是,一般会用 128 位一个分组,这样加密的好处是即使一个小的变化也会导致输出大量的变化,这样攻击者很难通过出现频率分析加密方式(比如 HTTP 开头都是一样的,所以使用顺序加密,很容易用 HTTP 的通用开头做输入得出加密方法)。但是分组密码的小影响会导致大改变的特性导致攻击者没办法这么做。

(如果攻击者把有的流量都记录下来,等有一天通过方法获得密钥就能解开这些数据了,可能是未来算力提高,或者通过法律手段,斯诺登的加密信箱就是 FBI 让加密邮箱公司强制提供的)

哈希函数

哈希函数其实很熟悉了,解释一下 MAC。

MAC

MAC 是 message authetication code,是密钥的哈希函数,因为普通哈希函数,如果 Mallory 可以直接用假的数据用哈希算出结果发给 Bob,缺少身份验证,MAC 就是带密钥的哈希函数,HMAC 其实就是把密钥和消息组合在一起的协议。

非对称加密

非对称加密又叫做公钥加密,对称加密固然好,但是对称密钥在团体中使用的话,大家都要共享,密钥给来给去的很容易出问题。对称加密就没有这个问题,可以方便传播,对称密钥分私钥和公钥,一个用于加密一个用于解密,私钥加密的数据只能用公钥解密。公钥的出现使得密钥可以大范围传播。

数字签名

数字签名主要验证消息的真实性,对消息进行验证。主要是对消息进行哈希,然后用私钥加密,追加到文档中做身份验证,这样用公钥解的开的话就能证明消息的发送者。

TLS 的具体协议

PKI 公钥基础设施

PKI 主要是用来保证公钥的可信,通过中间的权威机构(CA)签发的公钥也就是证书才能被认可是合法的证书。

证书

证书包含了版本、序列号、签名算法、颁发者、有效期、使用者、公钥。证书也有证书链,比如 root CA 可以签发 中间 CA 的证书。根证书一般是跟着操作系统一起造就装好了的,大公司的操作系统和浏览器都有自己的根证书库,自带就在电脑上。

应用

RSA 是目前最广泛应用的密钥算法,破解 1024 位的 RSA 密钥的成本大约是 1000 万人民币,现在一般用 2048 位的 RSA,基本无解。

其实公钥体系已经很完全了,但是大部分出问题都情况是私钥泄漏,管理自己的私钥非常重要。

1. 不要用 CA 生成的私钥,尽量自己生成。
2. 不要用刚开机的机器生成随机数,这台机器获得的外界熵不够多。
3. 定期更换私钥
4. 不要随意传播私钥
5. 安全存储私钥,这个主要是中间 CA 可能要用,普通服务直接换私钥就可以。

下面就用 OpenSSL 进行一些实验。

首先我们生成一个 RSA 密钥,前面提到了最好用 2048 位的,用 AES-128 算法来加密保存,会让你输入密码,这个密钥基于这个密码保存,这个文件的格式叫 PEM 所以如果看到 PEM 格式的文件就知道是私钥了。

1
2
3
4
5
6
7
$openssl genrsa -aes128 -out test.key 2048
Generating RSA private key, 2048 bit long modulus
...+++
.............................+++
e is 65537 (0x10001)
Enter pass phrase for test.key:
Verifying - Enter pass phrase for test.key:

可以看这个文件,看起来一通乱七八糟的东西,但是根据开头的信息,我们有办法用密码把私钥解析出来。

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
$cat test.key
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,52551A2438582E22358335433B7BAEE0

GiVITGQMbkGxzBYestzFNX4KMUgP84A2p49mOBozQWkAol+zU5llumFXrj/bzATq
OiH01+UN7sqKXD+lNwXsvL2bhhWFGe4h80CbCaVhPYnAtNtxU4HNTbAnCFZPmTHg
wC5JCHlJvt6TGIOeOeyTj9qnCeIKd1XJGYypG8syzzaKuNbSwlKN8DrUKGJP/w/8
gCPKT2AA2l53ysxBI6i2jHAxQSs1Y+K1jFrZjgObT3QDN4eqT1Io/waSDAiB8tkl
rW35ZYO0Toe3iJgTOp325v5dvC6mCOvL0QAQWDz7y239l8fAdyDJUO2tSzjXfIii
FM12NaHfw5m+GcT6brqlwbAOL6BSMX8Q+Dj/fdeDoPOF+pKnG4AGW3LPEY6LJgq0
pkExHJ5cl/nEL2Q2H3yekvPjW40XDZfjyQSivwEJGhsmAq3+tfis/7h5f5Rg/2yh
Vb8kkPQTu0SofS1VFkSQN6iJe5A3imjMkacKDoKtafORR1lWu0noaOqr757orcgI
hK847ijdFdwjstycrGBdN1INr0Yx/FnyaPYNR2XZhWD4uQwZ1tMVge5duw8jgHrH
ZwCeBAgfpSynbMy/1GTru1sO2T6qpZj9pfao59jVcN1fka+YKI5oEdxFFc97wcgk
XVX9lVrcPrI0bMyuhje5Vi04HbOxpd4GmtnNPKTwtmMbDsSVVIX+hlFltSBl+0in
PsKlUeBGjAmUapT3x1v7OP5K7K8YvszHMehbctDS2E9bZstCwhnsMog3b+Jxhw40
gXsOc6Vb3kJljkPXu6k7qGGkqzVqUuUMSaWlE87s5Cm4ZyS8c5IPQvmQk4S/1AJP
v5sD3TObRlJAIw0MEItPY4daQBMyIXPr+UXUAKfMoFK8bXG9aNpACKm/pQ1pVMj1
eE0+lzQ1UZUVM0GotBZGce+TtbrU/I/dbhGLA2KKyoohsCSH2yV+wGIMrmmbUiqH
a46FYwJLtFdDZ9m3ZVj1KCMza9/B2ylIetCX98C3/fVd81L3rxpSmbpWvzWYhw+Y
205t8p26WUloAQrkP+kqw0HDsiDeEU2QilvNwtA5qlfd8666rJEJpBg50jM8Sb0R
JYsG8Mc2a9CNpXt5pVi2kHdoRRkiSeGDh9xTnOlvnCc77p4MMlvcC3bqHwGe85ON
0q5xVJMyLwzAXR82X0FunRiiDNBO3Pg6k+UBALmLNrgieet57Jdva90OnkEA0Ihg
sYHHnfsMMJ6EnW4mNov8tdVi7mExK8K+RRe1vu+zAqzx/UuKkjekW0NdzQjHv4Hs
mX/9XzVE2cLG9GLWVojyTS5XUg16NBn7qM3HRQmQoAgXHlfCJUx33xMjTL3glbNH
65jpvG1S7l6U45BGMI3d2eQ0XfiSUHOHh/zHOIGRl7JcHyoK082IzivPbHkrlolv
o1E+4sgpFc09rQYgcW2cb6Nr3H3aYOlC86iPn4Ecxj8M/wznH/JNjjfoKKhmFWcU
EtmrreXjR8QlUCXCHZCA3DOi/MujcPU0qGZkaz+Ttii4NACZoHpedL+XQ9EcXci7
xkFO4NxsezDunpvXNHgkzES+as5lNxSa4wC4tUbC9h4zhgbeiOGBSTL8Aq0MxwX3
-----END RSA PRIVATE KEY-----

解析私钥

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
$openssl rsa -text -in test.key
Enter pass phrase for test.key:
Private-Key: (2048 bit)
modulus:
00:a0:18:a6:70:ce:65:64:ea:f1:43:30:35:ff:cf:
b4:5a:8e:20:52:04:84:bf:bf:3a:b4:a7:65:94:a5:
df:b5:14:09:61:ba:79:8a:43:d1:fe:cc:d9:96:d3:
81:5b:82:d7:63:e1:e9:6f:30:5e:b0:3f:fe:65:c4:
e1:d5:b2:e3:ce:ba:fc:7b:5e:b2:83:f0:9f:3f:c2:
15:39:b4:fa:e7:3c:ff:42:96:e7:a6:7a:29:e1:ba:
0b:c7:99:aa:ac:07:2a:2b:74:b3:f8:10:8d:0f:91:
44:a4:fa:48:c1:aa:88:0e:86:ff:c1:da:59:c8:dd:
32:d5:5b:15:f4:80:3e:2f:d7:d3:92:09:63:54:d0:
01:46:78:cd:5c:5d:f1:1c:ad:a7:ab:84:5c:86:e1:
25:69:6d:6c:6c:df:90:5f:af:ca:6f:43:17:50:05:
b0:77:3d:92:e9:e7:4c:66:c3:58:08:96:60:7a:16:
02:d3:6f:56:cb:df:41:69:eb:83:f3:28:b7:82:0a:
c2:c6:b4:a3:6e:f1:2d:7f:ec:ea:87:7b:94:4b:8b:
b8:e1:72:0d:00:c1:8d:9f:cc:03:32:de:74:6e:26:
29:0b:4f:f4:41:93:1c:9c:ae:22:41:81:71:b6:9c:
8c:17:15:63:d5:86:ce:74:b2:99:fb:7f:ff:37:c8:
03:7b
publicExponent: 65537 (0x10001)
privateExponent:
1a:ae:40:fe:c7:c6:ea:1c:a5:7c:97:0a:48:c9:aa:
ba:f4:b8:ba:32:7a:95:22:1f:7c:7f:f1:53:e6:98:
f3:aa:95:2d:ae:50:17:14:da:68:66:67:54:d5:86:
d7:63:64:d6:06:8e:4a:b3:7a:f4:50:95:eb:0b:f6:
bf:10:83:1a:ae:da:e9:0c:8d:1f:a3:f8:46:3d:e8:
1f:a7:e3:b0:a9:df:b8:8f:41:a7:e2:f0:1b:e8:4f:
92:42:2f:c9:5f:a0:4d:81:b3:84:81:ed:a0:4c:8b:
6e:1b:30:08:e6:8c:aa:2f:21:6c:83:21:37:72:75:
c8:4c:d7:c9:d9:9d:83:87:67:05:4d:6d:28:72:71:
63:3b:b6:82:f8:42:0f:94:af:f2:b1:d8:c5:d3:3f:
50:bf:13:61:b2:0b:de:6b:34:42:cd:29:27:04:c9:
ff:49:14:75:d0:d5:e5:5c:4b:29:a1:95:c3:c5:e5:
34:46:9e:81:d4:9d:c3:c4:06:c9:96:90:39:90:fb:
db:06:77:fa:46:73:38:60:0e:e3:40:7b:d0:5d:a0:
97:0d:6e:0b:39:d6:99:63:a6:ee:67:b7:94:35:e2:
63:cf:02:a1:eb:0a:f0:50:99:6f:30:ae:6b:ef:1e:
14:a0:1a:f4:8e:ed:cd:81:bf:3d:2b:9d:b5:9e:b8:
21
prime1:
00:cc:ff:e2:f4:39:5d:33:de:96:15:e6:7c:d2:e6:
a3:56:a9:6a:09:0c:e9:26:94:36:41:92:b9:db:c9:
09:20:28:9d:bc:c6:76:60:88:93:97:81:16:86:da:
4d:65:0e:87:ec:ef:15:6d:c9:06:f7:99:12:eb:4a:
a6:7e:49:9d:1a:68:ca:35:57:5c:4b:2f:32:2e:e4:
76:87:a5:02:94:27:1a:1f:38:28:58:77:68:2d:5d:
fa:c2:fd:c4:09:80:e4:eb:14:84:cc:73:06:96:4b:
08:8e:da:1c:55:38:8d:8c:7f:19:01:fa:54:b3:62:
d4:cb:4c:df:01:e0:02:d8:c3
prime2:
00:c7:ec:f1:2b:83:26:d1:35:e3:55:00:3e:a9:7d:
2e:f0:68:4d:27:77:3f:d5:1c:99:ef:a0:98:3c:fd:
fd:d7:8f:51:f5:82:e7:8f:37:34:a7:1a:1f:c4:83:
44:ab:11:62:54:7a:5e:5c:a4:7f:d6:dd:f8:45:3c:
b6:bc:1e:b5:56:df:60:65:66:aa:43:82:f5:7a:7c:
72:3b:3d:fe:33:d4:27:b2:c5:9a:07:36:b4:ca:bc:
1d:a7:7a:5f:9c:1a:75:2b:2c:57:97:5a:b8:a9:de:
0e:8a:8c:84:ff:51:e9:12:e9:d4:8b:bf:de:5f:98:
52:9c:08:55:42:e1:70:be:e9
exponent1:
15:76:dd:7e:90:db:0f:69:48:f1:b6:16:6f:c6:b2:
67:8a:89:8d:b5:0a:5c:7d:bc:48:95:62:5c:7e:ea:
33:b1:cd:02:4d:0d:6c:02:20:e2:06:24:23:ae:8b:
d7:fe:f3:80:7d:70:12:f4:af:84:11:45:07:d9:e3:
20:e9:f8:47:21:9d:ba:84:11:27:d6:23:3d:01:b2:
df:75:09:96:15:9a:08:96:ca:b2:a8:9e:01:d2:0b:
45:8b:68:91:4e:2b:a9:e9:96:16:0a:1d:30:73:5e:
cc:06:4e:5d:25:f4:bc:37:3a:99:18:6a:f1:f5:71:
2e:70:38:11:6c:31:20:1d
exponent2:
00:a6:f0:8f:21:4a:4e:6b:7b:97:ec:2e:5c:24:a2:
c7:43:2f:94:dd:53:92:15:9d:e0:5c:5b:b9:43:94:
c3:15:f0:32:fb:d2:e7:10:8b:84:87:d4:24:9a:af:
11:f3:d6:7c:49:16:35:1d:1e:af:30:f8:00:8b:af:
fa:d6:72:bd:f1:60:6c:d9:bf:34:85:53:21:2f:ba:
22:98:9d:57:5a:67:d9:0e:4a:3a:27:b3:e2:9b:37:
21:7b:eb:8f:52:86:35:38:6b:ba:68:43:f4:d6:c2:
f9:59:6f:a4:ce:9d:d3:05:5c:03:82:fe:1f:ed:aa:
ff:b0:12:b5:3f:37:88:31:a1
coefficient:
09:97:9e:dc:20:fe:c5:e2:34:47:d8:64:de:bb:ad:
70:65:4d:08:49:c8:cf:28:40:f6:87:43:09:c9:63:
bc:d8:cd:11:53:78:ba:ad:1a:f0:8b:e7:fa:1c:5f:
c9:9d:5f:ae:e1:2a:7f:87:7a:7f:1a:e3:c8:b5:8d:
eb:b2:af:18:c6:1e:07:43:f0:e7:be:4e:bc:c6:1b:
77:b8:43:36:58:3a:b5:8a:2c:f7:76:37:c7:97:4c:
8c:fd:47:71:09:f8:76:fe:8d:0f:e1:3a:30:56:5c:
2b:70:60:9d:fa:53:74:8a:db:b9:04:78:ce:1c:1d:
28:ca:78:81:53:07:de:5e
writing RSA key
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAoBimcM5lZOrxQzA1/8+0Wo4gUgSEv786tKdllKXftRQJYbp5
ikPR/szZltOBW4LXY+HpbzBesD/+ZcTh1bLjzrr8e16yg/CfP8IVObT65zz/Qpbn
pnop4boLx5mqrAcqK3Sz+BCND5FEpPpIwaqIDob/wdpZyN0y1VsV9IA+L9fTkglj
VNABRnjNXF3xHK2nq4RchuElaW1sbN+QX6/Kb0MXUAWwdz2S6edMZsNYCJZgehYC
029Wy99BaeuD8yi3ggrCxrSjbvEtf+zqh3uUS4u44XINAMGNn8wDMt50biYpC0/0
QZMcnK4iQYFxtpyMFxVj1YbOdLKZ+3//N8gDewIDAQABAoIBABquQP7HxuocpXyX
CkjJqrr0uLoyepUiH3x/8VPmmPOqlS2uUBcU2mhmZ1TVhtdjZNYGjkqzevRQlesL
9r8Qgxqu2ukMjR+j+EY96B+n47Cp37iPQafi8BvoT5JCL8lfoE2Bs4SB7aBMi24b
MAjmjKovIWyDITdydchM18nZnYOHZwVNbShycWM7toL4Qg+Ur/Kx2MXTP1C/E2Gy
C95rNELNKScEyf9JFHXQ1eVcSymhlcPF5TRGnoHUncPEBsmWkDmQ+9sGd/pGczhg
DuNAe9BdoJcNbgs51pljpu5nt5Q14mPPAqHrCvBQmW8wrmvvHhSgGvSO7c2Bvz0r
nbWeuCECgYEAzP/i9DldM96WFeZ80uajVqlqCQzpJpQ2QZK528kJICidvMZ2YIiT
l4EWhtpNZQ6H7O8VbckG95kS60qmfkmdGmjKNVdcSy8yLuR2h6UClCcaHzgoWHdo
LV36wv3ECYDk6xSEzHMGlksIjtocVTiNjH8ZAfpUs2LUy0zfAeAC2MMCgYEAx+zx
K4Mm0TXjVQA+qX0u8GhNJ3c/1RyZ76CYPP39149R9YLnjzc0pxofxINEqxFiVHpe
XKR/1t34RTy2vB61Vt9gZWaqQ4L1enxyOz3+M9QnssWaBza0yrwdp3pfnBp1KyxX
l1q4qd4OioyE/1HpEunUi7/eX5hSnAhVQuFwvukCgYAVdt1+kNsPaUjxthZvxrJn
iomNtQpcfbxIlWJcfuozsc0CTQ1sAiDiBiQjrovX/vOAfXAS9K+EEUUH2eMg6fhH
IZ26hBEn1iM9AbLfdQmWFZoIlsqyqJ4B0gtFi2iRTiup6ZYWCh0wc17MBk5dJfS8
NzqZGGrx9XEucDgRbDEgHQKBgQCm8I8hSk5re5fsLlwkosdDL5TdU5IVneBcW7lD
lMMV8DL70ucQi4SH1CSarxHz1nxJFjUdHq8w+ACLr/rWcr3xYGzZvzSFUyEvuiKY
nVdaZ9kOSjons+KbNyF7649ShjU4a7poQ/TWwvlZb6TOndMFXAOC/h/tqv+wErU/
N4gxoQKBgAmXntwg/sXiNEfYZN67rXBlTQhJyM8oQPaHQwnJY7zYzRFTeLqtGvCL
5/ocX8mdX67hKn+Hen8a48i1jeuyrxjGHgdD8Oe+TrzGG3e4QzZYOrWKLPd2N8eX
TIz9R3EJ+Hb+jQ/hOjBWXCtwYJ36U3SK27kEeM4cHSjKeIFTB95e
-----END RSA PRIVATE KEY-----

这个私钥是我自己生成的,没用来干什么,所以直接展示了,但是生产环境的私钥要妥善保管,现在我们生成共钥,输入密码,读取密钥,然后 -pubout 表示生成公钥。

1
2
3
4
5
6
7
8
9
10
11
12
13
$openssl rsa -in test.key -pubout -out test-public.key
Enter pass phrase for test.key:
writing RSA key
$cat test-public.key
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoBimcM5lZOrxQzA1/8+0
Wo4gUgSEv786tKdllKXftRQJYbp5ikPR/szZltOBW4LXY+HpbzBesD/+ZcTh1bLj
zrr8e16yg/CfP8IVObT65zz/Qpbnpnop4boLx5mqrAcqK3Sz+BCND5FEpPpIwaqI
Dob/wdpZyN0y1VsV9IA+L9fTkgljVNABRnjNXF3xHK2nq4RchuElaW1sbN+QX6/K
b0MXUAWwdz2S6edMZsNYCJZgehYC029Wy99BaeuD8yi3ggrCxrSjbvEtf+zqh3uU
S4u44XINAMGNn8wDMt50biYpC0/0QZMcnK4iQYFxtpyMFxVj1YbOdLKZ+3//N8gD
ewIDAQAB
-----END PUBLIC KEY-----

创建证书需要发起 CSR(certificate signing request),到 CA 那里,这个 csr 包含了申请者的信息和申请者的公钥。下面就是创建 csr 的命令,其中比较重要的是要配置好 Common Name,这个会拿来和访问的 host 进行匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$openssl req -new -key test.key -out test.csr
Enter pass phrase for test.key:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) []:CN
State or Province Name (full name) []:SH
Locality Name (eg, city) []:SH
Organization Name (eg, company) []:.
Organizational Unit Name (eg, section) []:.
Common Name (eg, fully qualified host name) []:www.example.com
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:1234

可以查看里面的信息是否正确,req 表示处理 csr 文件,-text 一般是用于展示文件内容。

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
$openssl req -text -in test.csr -noout
Certificate Request:
Data:
Version: 0 (0x0)
Subject: C=CN, ST=SH, L=SH, CN=www.example.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:a0:18:a6:70:ce:65:64:ea:f1:43:30:35:ff:cf:
b4:5a:8e:20:52:04:84:bf:bf:3a:b4:a7:65:94:a5:
df:b5:14:09:61:ba:79:8a:43:d1:fe:cc:d9:96:d3:
81:5b:82:d7:63:e1:e9:6f:30:5e:b0:3f:fe:65:c4:
e1:d5:b2:e3:ce:ba:fc:7b:5e:b2:83:f0:9f:3f:c2:
15:39:b4:fa:e7:3c:ff:42:96:e7:a6:7a:29:e1:ba:
0b:c7:99:aa:ac:07:2a:2b:74:b3:f8:10:8d:0f:91:
44:a4:fa:48:c1:aa:88:0e:86:ff:c1:da:59:c8:dd:
32:d5:5b:15:f4:80:3e:2f:d7:d3:92:09:63:54:d0:
01:46:78:cd:5c:5d:f1:1c:ad:a7:ab:84:5c:86:e1:
25:69:6d:6c:6c:df:90:5f:af:ca:6f:43:17:50:05:
b0:77:3d:92:e9:e7:4c:66:c3:58:08:96:60:7a:16:
02:d3:6f:56:cb:df:41:69:eb:83:f3:28:b7:82:0a:
c2:c6:b4:a3:6e:f1:2d:7f:ec:ea:87:7b:94:4b:8b:
b8:e1:72:0d:00:c1:8d:9f:cc:03:32:de:74:6e:26:
29:0b:4f:f4:41:93:1c:9c:ae:22:41:81:71:b6:9c:
8c:17:15:63:d5:86:ce:74:b2:99:fb:7f:ff:37:c8:
03:7b
Exponent: 65537 (0x10001)
Attributes:
challengePassword :unable to print attribute
Signature Algorithm: sha256WithRSAEncryption
33:83:fa:d3:a1:7d:1b:5c:cc:cb:b1:19:99:79:e4:b8:29:fc:
0e:ac:e6:40:f5:13:f0:d7:f7:2b:67:d4:32:39:78:3f:0b:f0:
5e:2c:f4:5c:c1:14:f0:f7:82:5d:1e:c5:bf:00:3e:87:d2:b5:
ed:a7:46:75:70:da:db:53:f1:19:37:15:63:09:63:a8:4d:74:
19:ed:c5:3a:50:7b:db:5a:68:f0:88:37:54:23:0d:bb:4d:c3:
b6:1a:3f:1d:93:24:17:f3:c5:66:c8:9c:43:67:e8:3b:cc:48:
20:8e:9e:da:a6:a0:48:90:6d:b1:bc:ff:0d:39:62:7b:8c:5c:
cb:ec:ce:e1:de:0c:f3:5b:51:3e:5c:ab:ad:6f:f5:96:9c:e5:
12:9e:1b:a7:27:90:fe:d3:9f:f9:c2:9d:7e:b5:62:ac:f9:45:
33:6a:a7:b5:c2:ab:b7:18:a8:a6:91:15:26:27:a4:c9:84:26:
88:85:3e:68:99:8c:f4:c6:32:8d:61:71:83:cb:86:96:92:2e:
c7:bc:76:e0:59:82:e8:fe:47:39:da:f0:57:72:f7:59:c4:ba:
7a:51:23:13:bc:8c:75:07:d7:2d:cf:2b:69:07:20:80:27:6d:
6d:ae:cb:27:5d:ef:0c:92:99:a4:02:45:5b:58:ac:e9:71:1e:
ee:5f:54:78

可以看到里面的信息,还有签名算法,以及公钥等等。

我们可以用自己的私钥给自己签名,比如 x509 是证书的格式。

1
2
3
4
5
$openssl x509 -req -days 365 -in test.csr -signkey test.key -out test.crt
Signature ok
subject=/C=CN/ST=SH/L=SH/CN=www.example.com
Getting Private key
Enter pass phrase for test.key:

也可以把两步结合起来,直接创建自签名的证书,openssl req -new -x509 -days 365 -key test.key -out test.crt,如果不想要交互式的可以直接

1
2
openssl req -new -x509 -days 365 -key test.key -out test.crt \
-subj "/C=CN/L=BJ/O=HaiDian/CN=www.example.com"

CN 只能写一个,虽然可以写泛域名,但是要支持多个域名可以通过扩展字段 SAN(Subject Alternative Name)来解决。

我的一点观点

我个人觉得 nvidia 的 CUDA 太封闭了,不是很能明白这样封闭的产品怎么能够长久生存,仅仅是因为家大业大么,如果有家公司推出了八成性能的 GPU,但是整套开发的生态非常友好,是不是会像 Android 取代诺基亚一样,还是 nvidia 就是苹果,就算封闭环境也能保证强劲体验,这我也不好说了。

安装

CUDA+cudnn 装起来挺麻烦的,反正如果有错误的话,可以检查 CUDA samples 里面的 deviceQuery 是否成功,如果不成功可以用 strace 看一下少了什么东西,再想办法安装上去。检查 cudnn samples 是否成功也是一样的,里面有一个 mnistDNN 的例子。

Hello World!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

__global__ void helloFromGPU (void)
{
printf("Hello world!\n");
}

int main(void)
{
printf("Hello World! from CPU\n");

helloFromGPU <<<1, 10>>>();
cudaDeviceReset();
return 0;
}

以上就是一段 GPU 的 Hello world,执行下面的代码可以看到我们在 GPU 上并行执行了十个 “Hello world!” 的打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ nvcc -arch sm_20 hello.cu -o hello
$ ./hello
Hello World! from CPU
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!

CUDA

CUDA 全称 Compute Unified Device Architecure,用于定义 GPU 的架构标准。GPU 的工作方式主要依赖于多核心的并行计算,CUDA 提供了方便的模型进行这种模式的编程,下面就会简单介绍一下 CUDA 的架构以及基于 GPU 的编程。

CUDA 一次启动的线程称为网格(grid),网格中包含块,每个块包含新程,是一个二维的模式,这张图片就说明得很清晰,首先是由 Grid 组成,然后每个 Block 有 shared memory,同 block 的线程可以访问 shared memory,不同的 Block 的线程只能访问全局的内存,这种结构也方便设计和实现并行算法。

cuda-arch

CUDA 的编译器 nvcc 是 gcc 的一个扩展,支持编写运行在 GPU 上的函数,其中的,<<x, y>> 扩展就是用来指定 block 和 thread 的数量的。

比如典型的俩个向量相加的例子:

1
2
3
4
void sumArrayOnHost(float *A, float *B, float *C, const int N) {
for (int i = 0; i < N; i++)
C[i] = A[i] + B[i]
}

但是 GPU 的核函数怎么写呢,是这样的。

1
2
3
4
__global__ void sumArrayOnGPU(float *A, float *N, float C) {
int i = threadIdx.x
C[i] = A[i] + B[i]
}

然后,如果 N 是 32 的话,可以如下调用,__global__ 表示这个函数可以在 host 调用也可以在 GPU 上调用,用个 32 个线程的 block 算这个向量和,N 隐性包含在了定义中。

1
sumArrayOnGPU<<1, 32>>(float *A, float *B, float *C)

线程组织

基于 CPU 和 GPU 的异构计算平台可以优势互补,CPU 负责处理逻辑复杂的串行程序,而 GPU 重点处理数据密集型的并行计算程序,从而发挥最大功效。

线程组织主要依据网格(现在网络这个词其实比较容易混淆,可以是神经网络可以是计算机网络,这里指的是线程的组织形式)模型。主要分 grid, block, thread,组织的方式就靠索引来决定,可以通过 block 索引和 thread 索引进行线程的定位。CUDA 提供了块内线程同步的方法,但是没有提供块间同步的原语。

线程束分化

线程束 = warp

CPU 有很强的分支预测的能力,会预加载指令,如果预测正确,执行代价就很小,但是 GPU 在这方面是很弱的,因为线程束当中如果执行分支出现不同,那出现分支的线程就会被禁止执行,这也是用 GPU 做并行编程的时候需要参考的一个很重要的因素,保证并行的线程出现线程束分化的情况尽量少。

下面就是线程束分化的例子。

并行归约问题

并行归约是指如何对并行问题进行归约,比如说相邻配对和交错配对。

设备管理

管理 GPU 主要是通过 CUDA API 或者 nvidia-smi 命令来获取。

SM(流式多处理器)

上面讲的是抽象上的分层,但是实际的物理层面承载 GPU 的是 SM。最早的 GPU 架构叫 Fermi,然后是 Kepler,然后才是 Tesla。一般说的 cm_20, sm_20 就指这种计算能力和架构,新款的 GPU 计算能力要更强一点。

内存模型

CUDA 的内存模型和 CPU 是类似的也是多级的结构。线程有局部内存,块之间有全局内存。GPU 和 CPU 都是主要采用 DRAM 来做主存,CPU 的一级缓存会用 SRAM 做。CPU 的多级缓存对用户来说不是很需要考虑,尽量屏蔽了其中的细节,但是 GPU 相对来说会把这种分级结构暴露给用户,这对编程来说也是一种新的挑战。

后面

用 GPU 计算一些计算密集型的程序,速度是真快,比 CPU 块很多,所以这也是为什么大量深度学习的应用都要通过 GPU 加速的原因。

本文通过裸写神经网络的方法,帮助理解神经网络的工作方式,直接在 klab 上查看就可以。

kube-dns 是 kubernetes 基于 DNS 的服务发现模块,主要由三个容器组成,分别是 dnsmasq, kube-dns, sidecar,整体的结构如图。

sidecar

sidecar 是一个监控健康模块,同时向外暴露metrics 记录,但是为啥叫三蹦子不知道。

接受的探测参数是

--probe=<label>,<server>,<dnsname>[,<interval_seconds>][,<type>]

例子如下

--probe=dnsmasq,127.0.0.1:53,kubernetes.default.svc.cluster.local,5,A

等于是每隔 5s 向127.0.0.1:53 进行 DNS 查询 kubernetes.default.svc.cluster.local 的 A 记录

对应的结构体是

dnsmasq

dnsmasq-nanny 是 dnsmasq 的保姆进程,dnsmasq 是一个简易的 DNS server。

dnsmasq-nanny “–” 后面是 dnsmasq 的参数,比如下面这个参数表示的是把 server=/cluster.local/127.0.0.1#10053 当作 dnsmasq 的配置,10053 是 kube-dns 的地址,也就是把 cluster.local 的域名拦截转到 kube-dns 进行解析,剩下的通过正常的域名解析流程。

--server=/cluster.local/127.0.0.1#10053

dnsmasq 简单来说扮演的是集群当中的一个传统 dns server 并且把集群内部的 dns 查询拦截到 kube-dns 当中通过中心化的方法进行 dns 查询,集群的 dns 查询主要依靠 kube-dns。

kube-dns

kube-dns 主要基于 skydns 来实现。

k8s.io/dns/pkg/dns/dns.goKubeDNS.Start 下面有 endpoints 和 services 的 controllers,会把 service 注册到 kube-dns 的 cache 当中 (k8s.io/dns/pkg/dns/treecache),这里有 k8s 域名命名规范

主要的实现方式是 skydns 接受一个后端实现。

KubeDNS.Records KubeDNS.ReverseRecord 基于 TreeCache 实现 DNS 记录存储的后端,从而使得 skydns 提供 DNS 服务。

总结

整体来说 kube-dns 还是一个比较简单的模块,基于 kube-apiserver 的一个控制器,提供中心化的 DNS 查询。

kube-controller-manager 可以认为是一个守护进程用于监视 apiserver 暴露的集群状态,并且不断地尝试把当前状态向集群的目标状态迁移。为了避免频繁查询 apiserver,apiserver 提供了 watch 接口用于监视资源的增加删除和更新,client-go 对此作了抽象,封装一层 informer 来表示本地 apiserver 状态的 cache。这个视频 有一个 Google 工程师讲解的 client-go 的详细内容,这篇 七牛前同事的文章介绍了 informer 的整体结构,写得也很好。

client 当中的 controller

处理事件的 controller 由几部分构成,首先是 Config 当中的可配置部分,下图是 controller 的关系,controller 实现了 Controller 接口。

controller 从 Queue 当中通过 Pop 获取对象交给 Process 回调处理,DeltaFIFO 和 FIFO 是类似的,只是 DeltaFIFO 可以处理删除事件,一般都用 DeltaFIFO。ListerWatcher 就是用客户端构造出来的,针对对应资源的 List Watch 方法的集合,List 用于获取最开始的对象获取,Watch 用于监控之后的变化,所有最开始的时候现有的对象会通过 List 传给 Add 回调,同步了当前状态以后再不断接受新的变化,但是 Watch 本身是有超时机制的,不能永久监听,所以再超时之后还会通过 List 方法,先同步一次再进行删除操作。Resync Period 表示把 cache 中的对象重新入队给回调函数处理,这种情况一般是可能你可能漏掉了更细操作,或者是之前的一些失败了。大部分情况用不到这个选项,可以非常相信 etcd 的功能。

调用路径

1
2
3
4
5
cache.NewListWatchFromClient -> listWatcher
cache.NewIndexer -> store
cache.NewLister(store) -> lister
cache.NewReflector
go refector.Run()

还有一个关键结构是 reflector,reflector 会把对象转化成对应的需要的对象并且 Add 到 Queue 当中去。

Informer 本身的框架是异步的,所以为了做并发控制就引入了 workqueue 的组件,workqueue 有 rate limit 的功能,并且能够合并更新操作。

注意不要修改传入的对象,因为他们要和 cache 一致,如果要写对象的话,需要使用 api.Scheme.Copy 这个函数,进行深度拷贝,所有的 k8s Object 都要支持深拷贝的方法。

kube-controller 当中的插件式 controller

k8s.io/kubernetes/cmd/kube-controller-manager/app/controllermanager.go 中的NewControllerInitializers 函数有大部分 controller 的列表,bootstrapsignertokencleaner 是默认关闭的。

1
2
3
4
var ControllersDisabledByDefault = sets.NewString(
"bootstrapsigner",
"tokencleaner",
)

需要特殊初始化的是 serviceaccount-tok,另外的是 NewControllerInitializers 当中的 controller 了。所有的 controller 初始化函数都要满足如下接口。

1
2
3
4
// InitFunc is used to launch a particular controller.  It may run additional "should I activate checks".
// Any error returned will cause the controller process to `Fatal`
// The bool indicates whether the controller was enabled.
type InitFunc func(ctx ControllerContext) (bool, error)

node controller

接下来看一个具体的 controller,startNodeController

startNodeController 首先解析 ClusterCIDRServiceCIDR 两个子网范围,下面是 NodeController 初始化需要的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
nodeController, err := nodecontroller.NewNodeController(
ctx.InformerFactory.Core().V1().Pods(),
ctx.InformerFactory.Core().V1().Nodes(),
ctx.InformerFactory.Extensions().V1beta1().DaemonSets(),
ctx.Cloud,
ctx.ClientBuilder.ClientOrDie("node-controller"),
ctx.Options.PodEvictionTimeout.Duration,
ctx.Options.NodeEvictionRate,
ctx.Options.SecondaryNodeEvictionRate,
ctx.Options.LargeClusterSizeThreshold,
ctx.Options.UnhealthyZoneThreshold,
ctx.Options.NodeMonitorGracePeriod.Duration,
ctx.Options.NodeStartupGracePeriod.Duration,
ctx.Options.NodeMonitorPeriod.Duration,
clusterCIDR,
serviceCIDR,
int(ctx.Options.NodeCIDRMaskSize),
ctx.Options.AllocateNodeCIDRs,
ipam.CIDRAllocatorType(ctx.Options.CIDRAllocatorType),
ctx.Options.EnableTaintManager,
utilfeature.DefaultFeatureGate.Enabled(features.TaintBasedEvictions),
utilfeature.DefaultFeatureGate.Enabled(features.TaintNodesByCondition),
)

InformerFactory 是用来构造具体 resource 的 informer 的工厂类型,构造了 pods, nodes, daemonsets 的 informer, 说明 node controller 需要 watch 这几种 resource 的变化。

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
// Run starts an asynchronous loop that monitors the status of cluster nodes.
func (nc *Controller) Run(stopCh <-chan struct{}) {
defer utilruntime.HandleCrash()

glog.Infof("Starting node controller")
defer glog.Infof("Shutting down node controller")

if !controller.WaitForCacheSync("node", stopCh, nc.nodeInformerSynced, nc.podInformerSynced, nc.daemonSetInformerSynced) {
return
}

// Incorporate the results of node status pushed from kubelet to master.
go wait.Until(func() {
if err := nc.monitorNodeStatus(); err != nil {
glog.Errorf("Error monitoring node status: %v", err)
}
}, nc.nodeMonitorPeriod, wait.NeverStop)

if nc.runTaintManager {
go nc.taintManager.Run(wait.NeverStop)
}

if nc.useTaintBasedEvictions {
// Handling taint based evictions. Because we don't want a dedicated logic in TaintManager for NC-originated
// taints and we normally don't rate limit evictions caused by taints, we need to rate limit adding taints.
go wait.Until(nc.doNoExecuteTaintingPass, scheduler.NodeEvictionPeriod, wait.NeverStop)
} else {
// Managing eviction of nodes:
// When we delete pods off a node, if the node was not empty at the time we then
// queue an eviction watcher. If we hit an error, retry deletion.
go wait.Until(nc.doEvictionPass, scheduler.NodeEvictionPeriod, wait.NeverStop)
}

<-stopCh
}

现在看一下运行的时候是如何做的,首先要调用 controller.WaitForCacheSync 等待 node,pod,daemonSet 的 inofrmer 同步,这是因为在 kube-controller-manager 当中使用的子 controller 使用的 informer 都是共享型的,也就是多个 controller 之间共享一个 informer 的 cache,所以在开始的时候需要保证所有的 sharedInformerFactory 创建的 informers 之间的 cache 先等待一次一致。controller 的读基本上是从 cache 读的,只要写才会打到 etcd 里面,然后等待 cache 的更新回调。

nodeController 主要分成两部分,一部分是 monitorNodeStatus , 它首先从 informer 的 cache 当中 list 新添加的节点和删除的节点,和 newZoneRepresentations。node 是分 zone 的,这在单可用区的 cluster 当中是个空字符串,但是如果 labels 中有 failure-domain.beta.kubernetes.io/zonefailure-domain.beta.kubernetes.io/region 就会构成不同的可用区的划分, 这个适用于仅仅在一家云厂商分不同可用区的时候可以用到。这篇文档 描述了多 zone 的 cluster 的内容,没有 zone 有相应的可用状态,如果某个 zone 变成不可用需要把 pod 从这个 zone 当中剔除,所以 pod 的 failover 是以 zone 为单位的。

处理 node 比较啰嗦,tryUpdateNodeStatus 尝试获取当前的 conditions 更新并且获取 conditions。处理 node 过程主要是标记 node 为不可用的 node,或者把不可用的状态恢复过来。

接下来的就是处理 pod eviction 的部分,另外,这篇文档解释了一些 kubelet 支持的资源耗尽的情况下 kubelet 的剔除策略。

taintManager.Run 处理 taint 的逻辑,这篇文档 解释了 taint 和 toleration 的关系,以及基于 taint 的 eviction 策略。首先看 node 的更新,然后把上面不能 tolerate 的 pod 传给 handlePodUpdate,然后 pod 有更新也会 handlePodUpdate,在 pod 更新的时间中会让 node 抢占一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for {
select {
case <-stopCh:
break
case nodeUpdate := <-tc.nodeUpdateChannel:
tc.handleNodeUpdate(nodeUpdate)
case podUpdate := <-tc.podUpdateChannel:
// If we found a Pod update we need to empty Node queue first.
priority:
for {
select {
case nodeUpdate := <-tc.nodeUpdateChannel:
tc.handleNodeUpdate(nodeUpdate)
default:
break priority
}
}
// After Node queue is emptied we process podUpdate.
tc.handlePodUpdate(podUpdate)
}
}

最后是 eviction 的部分,基于 eviction 的会把 pod 直接删除,基于 taint 只是打上标记,然后通过上面的 tiantManager 剔除。和 pod eviction 不同,taint eviction 是通过限制加 taint 的速率控制 raltelimit 的。

podGCController

pod GC Controller 只 watch pod 一种资源,比较简单。

k8s.io/kubernetes/pkg/controller/podgc/gc_controller.go 下。

gcc.gcOrphaned 删除 node 不存在的 pod,gcc.gcUnscheduledTerminating 删除正在终止,但是没有调度的 pod,gcc.gcTerminated 删除已经被终止的 pod。

其他 controller

其他 controller 也是类似的,从 apiserver 获取状态,并且向对应的状态迁移,这也是为什么 kubernetes 的命令和资源都是宣告式的原因。

kubernetes 概览

以下是 k8s 的整体架构,在 master 节点上主要是 kube-apiserver(整合了 kube-aggregator),还有 kube-scheduler,以及 kube-controller-manager,包括后端存储 etcd。

其中 kube-apiserver 是一个比较关键的部分,而且前期写得坑很多,导致这一部分虽然看起来是一个 API server 其实代码很复杂,特别冗余,而且目前对 kube-apiserver 还要做拆分,能够支持插入第三方的 apiserver,也就是又一个 aggregated apiserver 的 feature,也是和 kube-apiserver 和里面包的一层 genericserver 揉合在一起了,感觉一个大的系统 API server 越写越挫是一个通病,还好现在 k8s 迷途知返正在调整。

kube-apiserver

Kube-apiserver 可以是认为在 generic server 上封装的一层官方默认的 apiserver,有第三方需要的情况下,自己也可以在 generic server 上封装一层加入到集成模式中,这里主要介绍 kube-apiserver 的结构。

restful API

kube-apiserver 是一个 restful 服务,请求直接通过 HTTP 请求发送,例如创建一个 ubuntu 的 pod,用以下的 pod.yaml 文件。

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
name: ubuntu1
labels:
name: ubuntu1
spec:
containers:
- name: ubuntu1
image: ubuntu
command: ["sleep", "1d"]

执行命令 kubectl create -f ./pod.yaml -v=8,可以看到对应的 POST 请求如下。

1
2
3
4
5
6
7
8
Request Body: {"apiVersion":"v1","kind":"Pod","metadata":{"labels":{"name":"ubuntu1"},"name":"ubuntu1","namespace":"default"},"spec":{"containers":[{"command":["sleep","1d"],"image":"ubuntu","name":"ubuntu1"}],"schedulerName":"default-scheduler"}}
curl -k -v -XPOST -H "Content-Type: application/json" -H "Accept: application/json" -H "User-Agent: kubectl/v1.7.5 (linux/amd64) kubernetes/17d7182" https://localhost:6443/api/v1/namespaces/default/pods
POST https://localhost:6443/api/v1/namespaces/default/pods 201 Created in 6 milliseconds
Response Headers:
Content-Type: application/json
Content-Length: 1208
Date: Wed, 18 Oct 2017 15:04:17 GMT
Response Body: {"kind":"Pod","apiVersion":"v1","metadata":{"name":"ubuntu1","namespace":"default","selfLink":"/api/v1/namespaces/default/pods/ubuntu1","uid":"9c9af581-b415-11e7-8033-024d1ba659e8","resourceVersion":"486154","creationTimestamp":"2017-10-18T15:04:17Z","labels":{"name":"ubuntu1"}},"spec":{"volumes":[{"name":"default-token-p0980","secret":{"secretName":"default-token-p0980","defaultMode":420}}],"containers":[{"name":"ubuntu1","image":"ubuntu","command":["sleep","1d"],"resources":{},"volumeMounts":[{"name":"default-token-p0980","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount"}],"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"Always"}],"restartPolicy":"Always","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","serviceAccountName":"default","serviceAccount":"default","securityContext":{},"schedulerName":"default-scheduler","tolerations":[{"key":"node.kubernetes.io/not-ready","operator":"Exists","effect":"NoExecute","tolerationSeconds":300},{"key":"node.alpha.kubernetes.io/unreachable","operator":"Exists","effect":"NoExecute","tolerationSeconds":300}]},"status":{"phase":"Pending","qosClass":"BestEffort"}}

从 url path 里面可以看到几个划分,path 的分类大概有下面这几种。

路径上整体分成 group, version, resource, 作为核心 API group 的 core(包括 pod, node 之类的 resource),不带 group,直接接在 /api/ 后面,其他的 api group 则接在 /apis 后面。以 pod 为例,pod 对应的数据类型如下,这个数据结构和 POST 请求中的结构的参数是一致的。

如果是 job 的话则是在,pkg/apis/batch/v2alpha1/types.go,和 API 路径是对应的。例子当中 kubectl 加上 level 大于 8 的 log 就会打印请求和相应的 body,可以看到 request body 和上面的数据结构是一致的。这个请求会发送到 apiserver 进行处理并且返回存储之后的 pod。

重要结构体

Config

父结构,主要的配置内容,其中有一个结构 RESTOptionsGetter genericregistry.RESTOptionsGetter 是和 API 初始化相关的,这个接口的实现是在 k8s.io/apiserver/pkg/server/options/etcd.go 中的 storageFactoryRestOptionsFactory 实现的,对应的实现函数是

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
func (f *storageFactoryRestOptionsFactory) GetRESTOptions(resource schema.GroupResource) (generic.RESTOptions, error) {
storageConfig, err := f.StorageFactory.NewConfig(resource)
if err != nil {
return generic.RESTOptions{}, fmt.Errorf("unable to find storage destination for %v, due to %v", resource, err.Error())
}

ret := generic.RESTOptions{
StorageConfig: storageConfig,
Decorator: generic.UndecoratedStorage,
DeleteCollectionWorkers: f.Options.DeleteCollectionWorkers,
EnableGarbageCollection: f.Options.EnableGarbageCollection,
ResourcePrefix: f.StorageFactory.ResourcePrefix(resource),
}
if f.Options.EnableWatchCache {
sizes, err := ParseWatchCacheSizes(f.Options.WatchCacheSizes)
if err != nil {
return generic.RESTOptions{}, err
}
cacheSize, ok := sizes[resource]
if !ok {
cacheSize = f.Options.DefaultWatchCacheSize
}
ret.Decorator = genericregistry.StorageWithCacher(cacheSize)
}

return ret, nil
}

APIGroupInfo

APIGroupInfo 主要定义了一个 API 组的相关信息,观察一下 APIGroupInfo 是如何初始化的。

k8s.io/pkg/master/master.go 当中,每个 Resource 都要提供自己的 Provider,比如说 storagerest 就在 k8s.io/kubernetes/pkg/registry/storage/rest/storage_storage.go 定义了 NewRESTStorage 方法。而默认的 resource 的 legacy provider 单独处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
if c.ExtraConfig.APIResourceConfigSource.AnyResourcesForVersionEnabled(apiv1.SchemeGroupVersion) {
legacyRESTStorageProvider := corerest.LegacyRESTStorageProvider{
StorageFactory: c.ExtraConfig.StorageFactory,
ProxyTransport: c.ExtraConfig.ProxyTransport,
KubeletClientConfig: c.ExtraConfig.KubeletClientConfig,
EventTTL: c.ExtraConfig.EventTTL,
ServiceIPRange: c.ExtraConfig.ServiceIPRange,
ServiceNodePortRange: c.ExtraConfig.ServiceNodePortRange,
LoopbackClientConfig: c.GenericConfig.LoopbackClientConfig,
}
m.InstallLegacyAPI(&c, c.GenericConfig.RESTOptionsGetter, legacyRESTStorageProvider)
}

然后通过调用 k8s.io/kubernetes/pkg/registry/core/rest.LegacyRESTStorageProviderNewLegacyRESTStorage 来初始化基础对象的 apigroup info,比如初始化 podStorage,serviceStorage 和 nodeStorage 等等。legacy ApiGrouInfo 的 Scheme, ParamaterCodec, NegotiatedSerializer 都是用 "k8s.io/kubernetes/pkg/api" 包下的全局变量初始化的。

1
2
3
Scheme:                      api.Scheme,
ParameterCodec: api.ParameterCodec,
NegotiatedSerializer: api.Codecs,

然后合并成一个 restStorage 存入 apiGroupInfo 中。

1
2
3
4
5
6
7
8
9
10
11
restStorageMap := map[string]rest.Storage{
"pods": podStorage.Pod,
"pods/attach": podStorage.Attach,
"pods/status": podStorage.Status,
"pods/log": podStorage.Log,
"pods/exec": podStorage.Exec,
"pods/portforward": podStorage.PortForward,
"pods/proxy": podStorage.Proxy,
"pods/binding": podStorage.Binding,
"bindings": podStorage.Binding,
...

举个例子 podStorage 就是用的 genericregistry.Store,这是一个通用的 etc 辅助结构,把 etcd 抽象成存储结构。

1
2
3
4
5
// REST implements a RESTStorage for pods
type REST struct {
*genericregistry.Store
proxyTransport http.RoundTripper
}

serialization

pkg/api.Codecs 是全局默认的 codec 来自下面这段代码。

1
2
3
4
func NewCodecFactory(scheme *runtime.Scheme) CodecFactory {
serializers := newSerializersForScheme(scheme, json.DefaultMetaFactory)
return newCodecFactory(scheme, serializers)
}

默认具体定义了这几种 serilizer。

1
2
3
4
5
func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory) []serializerType {
jsonSerializer := json.NewSerializer(mf, scheme, scheme, false)
jsonPrettySerializer := json.NewSerializer(mf, scheme, scheme, true)
yamlSerializer := json.NewYAMLSerializer(mf, scheme, scheme)
...

而且标准库的 json 有很严重的性能问题,换用了 json-iter 但是有很多标准库不兼容的问题,性能提升了大概 20% 但是没办法和进主线,我尝试在上面工作的了一段时间,改了两个问题还是有错,由于时间关系,暂时放弃了这个工作,相关的 issue 在这里

filters

首先通过 ./staging/src/k8s.io/apiserver/pkg/server/config.go 下的 DefaultBuildHandlerChain 构建 filters。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
handler := genericapifilters.WithAuthorization(apiHandler, c.RequestContextMapper, c.Authorizer, c.Serializer)
handler = genericfilters.WithMaxInFlightLimit(handler, c.MaxRequestsInFlight, c.MaxMutatingRequestsInFlight, c.RequestContextMapper, c.LongRunningFunc)
handler = genericapifilters.WithImpersonation(handler, c.RequestContextMapper, c.Authorizer, c.Serializer)
if utilfeature.DefaultFeatureGate.Enabled(features.AdvancedAuditing) {
handler = genericapifilters.WithAudit(handler, c.RequestContextMapper, c.AuditBackend, c.AuditPolicyChecker, c.LongRunningFunc)
} else {
handler = genericapifilters.WithLegacyAudit(handler, c.RequestContextMapper, c.LegacyAuditWriter)
}
failedHandler := genericapifilters.Unauthorized(c.RequestContextMapper, c.Serializer, c.SupportsBasicAuth)
if utilfeature.DefaultFeatureGate.Enabled(features.AdvancedAuditing) {
failedHandler = genericapifilters.WithFailedAuthenticationAudit(failedHandler, c.RequestContextMapper, c.AuditBackend, c.AuditPolicyChecker)
}
handler = genericapifilters.WithAuthentication(handler, c.RequestContextMapper, c.Authenticator, failedHandler)
handler = genericfilters.WithCORS(handler, c.CorsAllowedOriginList, nil, nil, nil, "true")
handler = genericfilters.WithTimeoutForNonLongRunningRequests(handler, c.RequestContextMapper, c.LongRunningFunc, c.RequestTimeout)
handler = genericapifilters.WithRequestInfo(handler, c.RequestInfoResolver, c.RequestContextMapper)
handler = apirequest.WithRequestContext(handler, c.RequestContextMapper)
handler = genericfilters.WithPanicRecovery(handler)
return handler
}
panic recover

genericfilters.WithPanicRecovery 在 handler 的最外层对出现的 panic 恢复,并且打印每次请求的 log,所以你想观察 API 请求的情况可以 grep wrap.go 就能看到。

request context

apirequest.WithRequestContext 给 request 绑定一个 Context

RequestInfo

跟路 url 提取后续请求需要的 group, version, namespace, verb, resource 等信息。

WithTimeoutForNonLongRunningRequests

限制 API 调用时间,超时处理提前终止 write。

WithCORS

允许跨域访问。

authentication

k8s.io/apiserver/pkg/endpoints/filters/authentication.go 下。WithAuthentication 插入鉴权信息,例如证书鉴权,token 鉴权等,并且从鉴权信息当中获取 user 信息(可能是 service account 也可能是外部用户)user 身份是由 里面的几种方式确认的

authorization

检查是否有权限进行对应资源的操作。一种是 RBAC 一种是 Node。具体这两种方式可以看这个介绍,RBAC 主要是针对服务的,而 Node 模式主要是针对 kubelet 的。

impersonation

让用户伪装成其他用户,比如 admin 可以用普通用户的身份创建资源。

路由

通过 genericapiserver 的 InstallLegacyAPIGroup 就注册到路由当中。具体的做法就是根据 version, resource, sub resource, verb 等信息构造路由,然后用 go-restful 注册处理函数。比如说 GET

1
2
3
4
5
6
7
route := ws.GET(action.Path).To(handler).
Doc(doc).
Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
Operation("read"+namespaced+kind+strings.Title(subresource)+operationSuffix).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
Returns(http.StatusOK, "OK", producedObject).
Writes(producedObject)

handler 里面做的内容就是序列化,然后根据具体的要求(GET DELETE 等)到 etcd 中操作,当然本身还有一层缓存,这取决于 API 的 options 是希望更新还是直接读缓存(缓存会比 etcd 旧一些),比如对于 kubelet 会不断查询 node 信息,但是 kubelet 本身并不需要最新的信息,这个时候就会从缓存中读取。

性能调优

开启代理 kubectl proxy,就可以通过 localhost 直接访问 kube-apiserver HTTP 服务。然后执行 go tool pprof http://localhost:8001/debug/pprof/profile 可以获得 profile 结果,下图红色的部分就是调用耗时最多的部分。

除此之外,kube-apiserver 本身也暴露了很多 prometheus 的 metrics 但是往上现在没有现成的模板,只能根据自己的需求来在 prometheus 当作做 query。可以在 k8s.io/apiserver/pkg/endpoints/metrics/metrics.go 里面看到。

之前也说过,超时间调用时会打 log 的,在代码中保存了一些 trace 日志,可以通过 grep Trace来过滤。Trace[%d] 这样开头, %d 是一个 id 可以看到具体的 trace 信息。