vernlium.github.io's People
vernlium.github.io's Issues
test
pytorch_Resnet学习 | Vernlium
http://vernlium.github.io/2020/10/08/pytorch-Resnet%E5%AD%A6%E4%B9%A0/
本文介绍使用pytorch运行Resnet网络的推理,及分析resnet的实现源码。 pytorch Resnet网络学习
Tensorflow源码解析——算子注册
Tensorflow源码解析——算子注册
什么是op
op和kernel是TF框架中最重要的两个概念,如果一定要做一个类比的话,可以认为op相当于函数声明,kernel相当于函数实现。举个例子,对于矩阵相乘,可以声明一个op叫做MatMul,指明它的名称,输入,输出,参数,以及对参数的限制等。op只是告诉我们,这个操作的目的是什么,op内部有哪些可定制的东西,但不会提供具体实现。op在某种设备上的具体实现方法,是由kernel决定的。TF的计算图由节点构成,而每个节点对应了一个op,在构建计算图时,我们只知道不同节点对应的操作是什么,而不知道运行时这个操作是怎样实现的。也就是说,op是编译期概念,而kernel是运行期概念。
那为什么要把操作和它的实现分离呢?是为了实现TF代码的可移植性。我们可以把TF构建的计算图想象为Java的字节码,而计算图在执行的时候,需要考虑可用的设备资源,相当于我们在运行Java字节码的时候,需要考虑当前所在的操作系统,选择合适的字节码实现。因为TF的目标是在多设备上运行,但我们在编码的时候,是无法预先知道某一个操作具体是在哪种设备上运行的,因此,将op和它的实现分离,可以让我们在设计计算图的时候,更专注于它的结构,而不是具体实现。当我们构建完成一个计算图之后,在一个包含GPU的设备上,它可以利用对应操作在GPU上的kernel,充分利用GPU的高计算性能,在一个仅包含CPU的设备上,它也可以利用对应操作在CPU上的kenrel,完成计算功能。这就提高了TF代码在不同设备之间的可移植性。
注册方式
下面是tensorflow代码中注册Argmax
算子的代码:
REGISTER_OP("ArgMax")
.Input("input: T")
.Input("dimension: Tidx")
.Output("output: output_type")
.Attr("T: numbertype")
.Attr("Tidx: {int32, int64} = DT_INT32")
.Attr("output_type: {int32, int64} = DT_INT64")
.SetShapeFn(ArgOpShape);
通过REGISTER_OP
宏进行算子注册,注册的内容有:
- Input:算子的输入
- Output:算子的输出
- Attr:算子的属性,比如Argmax算子,有个属性是axis,在哪根轴上求最大值的下标
- ShapeFn:用于shape推断
下面分析这个算子是如何被注册进去的。
OpDef
OpDef的定义在tensorflow\core\framework\op_def.proto
中
message OpDef {
// Op names starting with an underscore are reserved for internal use.
// Names should be CamelCase and match the regexp "[A-Z][a-zA-Z0-9_]*".
string name = 1;
// For describing inputs and outputs.
message ArgDef {
// Name for the input/output. Should match the regexp "[a-z][a-z0-9_]*".
string name = 1;
// Human readable description.
string description = 2;
DataType type = 3;
string type_attr = 4; // if specified, attr must have type "type"
string number_attr = 5; // if specified, attr must have type "int"
// If specified, attr must have type "list(type)", and none of
// type, type_attr, and number_attr may be specified.
string type_list_attr = 6;
bool is_ref = 16;
};
OpDef中最核心的数据成员是操作名称、输入、输出、参数。
对于其中的几个难理解的点,作出说明:
ArgDef
中的3-6四个字段,是为了描述·输入或输出的类型。当输入或输出是一个张量时,type或type_attr被设置为这个张量的数据类型,当输入或输出是一个由相同数据类型的张量构成的序列时,number_attr被设置为int对应的标识,当输入或输出是一个由张量构成的列表时,type_list_attr被设置为list(type)对应的标识;AttrDef
中的has_minimum
字段,表明这个属性是否有最小值,如果数据类型是int,那么minimum就是允许的最小值,如果数据类型是列表,那么minimum就是列表的最短长度,is_aggregate这个字段,表明当前的操作是否是可聚集的。一个可聚集的操作是,能接受任意数量相同类型和形状的输入,并且保持输出与每个输入的类型和形状相同,这个字段对于操作的优化非常重要,如果一个操作是可聚集的,并且其输入来自多个不同的设备,那么我们就可以把聚集优化成一个树形的操作,先在设备内部对输入做聚集,最后在操作所在的设备集中,这样可以提高效率。这种优化对于分布式的机器学习模型训练非常有帮助,Spark ML中的TreeAggregate就实现了这样的优化。- is_stateful这个字段,表明当前的op是否带有状态的,什么操op会带有状态呢?比如Variable;
通过protoc工具用proto文件生成.h文件。命令为:
./protoc \
-I=/home/anan/tensorflow1.12/tensorflow-1.12.0/ \
--cpp_out=/home/anan/tensorflow1.12/tensorflow-1.12.0/tensorflow/core/framework/
/home/z00354782/tensorflow_1.12/tensorflow-
1.12.0/tensorflow/core/framework/op_def.proto
从中找到OpDef的定义:
class OpDef : public::google::protobuf::Message {
private:
::google::protobuf::RepeatedPtrField<::tensorflow::OpDef_ArgDef> input_arg_;
::google::protobuf::RepeatedPtrField<::tensorflow::OpDef_ArgDef> output_arg_;
::google::protobuf::RepeatedPtrField<::tensorflow::OpDef_ArgDef> attr_;
::google::protobuf::internal::ArenaStringPtr name_;
::google::protobuf::internal::ArenaStringPtr summary_;
::google::protobuf::internal::ArenaStringPtr description_;
bool is_commutative_;
bool is_aggregate_;
bool is_stateful_;
bool allows_uninitialized_input_;
}
为了方便进行OpDef的构建,TF还设计了OpDefBuilder
类,它的私有数据成员如下:
// Builder class passed to the REGISTER_OP() macro.
class OpDefBuilder {
public:
// ...
private:
OpRegistrationData op_reg_data_;
std::vector<string> attrs_;
std::vector<string> inputs_;
std::vector<string> outputs_;
std::vector<string> control_outputs_;
string doc_;
std::vector<string> errors_;
};
可以看到,除了errors_
字段外,其他内容几乎就是把OpDef的结构原封不动的搬了过来。
在op_def_builder.h
中还有一个新的结构,OpRegistrationData
,他的结构如下:
struct OpRegistrationData {
public:
OpRegistrationData() {}
OpRegistrationData(const OpDef& def) : op_def(def) {}
OpRegistrationData(const OpDef& def, const OpShapeInferenceFn& fn,
bool is_function = false)
: op_def(def), shape_inference_fn(fn), is_function_op(is_function) {}
OpDef op_def;
OpShapeInferenceFn shape_inference_fn;
bool is_function_op = false;
};
在这个结构中,除了屋面熟知的OpDef
之外,还包含一个OpShapeInferenceFn
结构,他的定义如下:
typedef std::function<Status(shape_inference::InferenceContext* c)>
OpShapeInferenceFn;
这个结构的定义中,涉及到了我们后面要讲到的形状推断的内容,这里我们只需要知道,OpShapeInferenceFn是一个帮助操作根据输入形状对输出形状进行推断的函数即可。
Op注册
上面的例子中使用REGISTER_OP
宏进行Op注册,看一下这个宏的定义:
#define REGISTER_OP(name) REGISTER_OP_UNIQ_HELPER(__COUNTER__, name)
#define REGISTER_OP_UNIQ_HELPER(ctr, name) REGISTER_OP_UNIQ(ctr, name)
#define REGISTER_OP_UNIQ(ctr, name) \
static ::tensorflow::register_op::OpDefBuilderReceiver register_op##ctr \
TF_ATTRIBUTE_UNUSED = \
::tensorflow::register_op::OpDefBuilderWrapper<SHOULD_REGISTER_OP( \
name)>(name)
注:
__COUNTER__
宏表示自动计数,最终的定义是register_op0
、register_op1
、register_op2
依次往后排。
static ::tensorflow::register_op::OpDefBuilderReceiver register_op0 = \
::tensorflow::register_op::OpDefBuilderWrapper<true>("Argmax") \
.Input("input: T")
.Input("dimension: Tidx")
.Output("output: output_type")
.Attr("T: numbertype")
.Attr("Tidx: {int32, int64} = DT_INT32")
.Attr("output_type: {int32, int64} = DT_INT64")
.SetShapeFn(ArgOpShape);
也就是说,生成一个OpDefBuilderWrapper
对象,并链式调用它的Input
、Output
、Attr
等方法。
OpDefBuilderWrapper
的定义为:
// Template specialization that forwards all calls to the contained builder.
template <>
class OpDefBuilderWrapper<true> {
public:
explicit OpDefBuilderWrapper(const char name[]) : builder_(name) {}
OpDefBuilderWrapper<true>& Attr(string spec) {
builder_.Attr(std::move(spec));
return *this;
}
OpDefBuilderWrapper<true>& Input(string spec) {
builder_.Input(std::move(spec));
return *this;
}
OpDefBuilderWrapper<true>& Output(string spec) {
builder_.Output(std::move(spec));
return *this;
}
OpDefBuilderWrapper<true>& SetIsCommutative() {
builder_.SetIsCommutative();
return *this;
}
OpDefBuilderWrapper<true>& SetIsAggregate() {
builder_.SetIsAggregate();
return *this;
}
OpDefBuilderWrapper<true>& SetIsStateful() {
builder_.SetIsStateful();
return *this;
}
OpDefBuilderWrapper<true>& SetAllowsUninitializedInput() {
builder_.SetAllowsUninitializedInput();
return *this;
}
OpDefBuilderWrapper<true>& Deprecated(int version, string explanation) {
builder_.Deprecated(version, std::move(explanation));
return *this;
}
OpDefBuilderWrapper<true>& Doc(string text) {
builder_.Doc(std::move(text));
return *this;
}
OpDefBuilderWrapper<true>& SetShapeFn(
Status (*fn)(shape_inference::InferenceContext*)) {
builder_.SetShapeFn(fn);
return *this;
}
const ::tensorflow::OpDefBuilder& builder() const { return builder_; }
private:
mutable ::tensorflow::OpDefBuilder builder_;
};
通过链式调用,把Input、Output、Attr等描述保存到OpDefBuiIder
的attrs_、inputs_、outputs_属性中。例如,Input的处理为:
OpDefBuilder& OpDefBuilder::Input(string spec) {
inputs_.push_back(std::move(spec));
return *this;
}
OpDefBuilderWrapper
是OpDefBuilder
的包装器,其成员包含一个OpDefBuilder
的对象,它的API都是设置型的,且都返回对象本身,提供 链式的方式进行属性设置。值得注意的是,这个类名后面跟着一个true,它的含义等会再看。
最终把OpDefBuilderWrapper
类型的对象用于构造OpDefBuilderReceiver
。
OpDefBuilderReceiver
定义为:
struct OpDefBuilderReceiver {
// To call OpRegistry::Global()->Register(...), used by the
// REGISTER_OP macro below.
// Note: These are implicitly converting constructors.
OpDefBuilderReceiver(
const OpDefBuilderWrapper<true>& wrapper); // NOLINT(runtime/explicit)
constexpr OpDefBuilderReceiver(const OpDefBuilderWrapper<false>&) {
} // NOLINT(runtime/explicit)
};
} // namespace register_op
OpDefBuilderReceiver
的构造函数的实现为:
OpDefBuilderReceiver::OpDefBuilderReceiver(
const OpDefBuilderWrapper<true>& wrapper) {
OpRegistry::Global()->Register(
[wrapper](OpRegistrationData* op_reg_data) -> Status {
return wrapper.builder().Finalize(op_reg_data);
});
}
相当于是OpDefBuilderWrapper
构造时,以OpDefBuilderWrapper
为参数,在构造函数中调用OpRegistry::Global()->Register(...)
。
也就是说,REGISTER_OP
绕了一圈,先用OpDefBuilderWrapper
对操作进行封装,然后把它作为参数传递给OpDefBuilderReceiver
的构造函数,而在这个构造函数中,完成了对算子的注册。
真正的注册过程就是OpRegistry
的Register
方法中完成的,下面具体看一下注册类的实现。
注册类
为了方便对操作进行统一管理,TF提出了OP注册器的概念。这个OP注册器的作用,是为各种OP提供一个统一的管理接囗。
操作注册类的继承结构如下:
其中,OpRegistryInterface
是一个接口类,它提供了注册类最基础的查找功能:
// Users that want to look up an OpDef by type name should take an
// OpRegistryInterface. Functions accepting a
// (const) OpRegistryInterface* may call LookUp() from multiple threads.
class OpRegistryInterface {
public:
virtual ~OpRegistryInterface();
// Returns an error status and sets *op_reg_data to nullptr if no OpDef is
// registered under that name, otherwise returns the registered OpDef.
// Caller must not delete the returned pointer.
virtual Status LookUp(const string& op_type_name,
const OpRegistrationData** op_reg_data) const = 0;
// Shorthand for calling LookUp to get the OpDef.
Status LookUpOpDef(const string& op_type_name, const OpDef** op_def) const;
};
OpRegistry
类继承了OpRegistryInterface
类。
// The standard implementation of OpRegistryInterface, along with a
// global singleton used for registering ops via the REGISTER
// macros below. Thread-safe.
//
// Example registration:
// OpRegistry::Global()->Register(
// [](OpRegistrationData* op_reg_data)->Status {
// // Populate *op_reg_data here.
// return Status::OK();
// });
class OpRegistry : public OpRegistryInterface {
public:
typedef std::function<Status(OpRegistrationData*)> OpRegistrationDataFactory;
OpRegistry();
~OpRegistry() override;
void Register(const OpRegistrationDataFactory& op_data_factory);
Status LookUp(const string& op_type_name,
const OpRegistrationData** op_reg_data) const override;
// Fills *ops with all registered OpDefs (except those with names
// starting with '_' if include_internal == false) sorted in
// ascending alphabetical order.
void Export(bool include_internal, OpList* ops) const;
// Returns ASCII-format OpList for all registered OpDefs (except
// those with names starting with '_' if include_internal == false).
string DebugString(bool include_internal) const;
// A singleton available at startup.
static OpRegistry* Global();
// Get all registered ops.
void GetRegisteredOps(std::vector<OpDef>* op_defs);
// Get all `OpRegistrationData`s.
void GetOpRegistrationData(std::vector<OpRegistrationData>* op_data);
// Watcher, a function object.
// The watcher, if set by SetWatcher(), is called every time an op is
// registered via the Register function. The watcher is passed the Status
// obtained from building and adding the OpDef to the registry, and the OpDef
// itself if it was successfully built. A watcher returns a Status which is in
// turn returned as the final registration status.
typedef std::function<Status(const Status&, const OpDef&)> Watcher;
// An OpRegistry object has only one watcher. This interface is not thread
// safe, as different clients are free to set the watcher any time.
// Clients are expected to atomically perform the following sequence of
// operations :
// SetWatcher(a_watcher);
// Register some ops;
// op_registry->ProcessRegistrations();
// SetWatcher(nullptr);
// Returns a non-OK status if a non-null watcher is over-written by another
// non-null watcher.
Status SetWatcher(const Watcher& watcher);
// Process the current list of deferred registrations. Note that calls to
// Export, LookUp and DebugString would also implicitly process the deferred
// registrations. Returns the status of the first failed op registration or
// Status::OK() otherwise.
Status ProcessRegistrations() const;
// Defer the registrations until a later call to a function that processes
// deferred registrations are made. Normally, registrations that happen after
// calls to Export, LookUp, ProcessRegistrations and DebugString are processed
// immediately. Call this to defer future registrations.
void DeferRegistrations();
// Clear the registrations that have been deferred.
void ClearDeferredRegistrations();
private:
// Ensures that all the functions in deferred_ get called, their OpDef's
// registered, and returns with deferred_ empty. Returns true the first
// time it is called. Prints a fatal log if any op registration fails.
bool MustCallDeferred() const EXCLUSIVE_LOCKS_REQUIRED(mu_);
// Calls the functions in deferred_ and registers their OpDef's
// It returns the Status of the first failed op registration or Status::OK()
// otherwise.
Status CallDeferred() const EXCLUSIVE_LOCKS_REQUIRED(mu_);
// Add 'def' to the registry with additional data 'data'. On failure, or if
// there is already an OpDef with that name registered, returns a non-okay
// status.
Status RegisterAlreadyLocked(const OpRegistrationDataFactory& op_data_factory)
const EXCLUSIVE_LOCKS_REQUIRED(mu_);
Status LookUpSlow(const string& op_type_name,
const OpRegistrationData** op_reg_data) const;
mutable mutex mu_;
// Functions in deferred_ may only be called with mu_ held.
mutable std::vector<OpRegistrationDataFactory> deferred_ GUARDED_BY(mu_);
// Values are owned.
mutable std::unordered_map<string, const OpRegistrationData*> registry_
GUARDED_BY(mu_);
mutable bool initialized_ GUARDED_BY(mu_);
// Registry watcher.
mutable Watcher watcher_ GUARDED_BY(mu_);
};
OpRegistry
类是单例模式,通过Global
获取单例对象,并且是线程安全的。
注册函数Register
的定义为:
void OpRegistry::Register(const OpRegistrationDataFactory& op_data_factory) {
mutex_lock lock(mu_);
if (initialized_) {
TF_QCHECK_OK(RegisterAlreadyLocked(op_data_factory));
} else {
deferred_.push_back(op_data_factory);
}
}
其中,OpRegistrationDataFactory
是一个function类型:
typedef std::function<Status(OpRegistrationData*)> OpRegistrationDataFactory;
也就是说,Register
注册时传入的是一个函数,最终在Register
中完成对函数的调用。
从代码看,只有RegisterAlreadyLocked(op_data_factory)
中可能产生对op_data_factory
的调用,所以可以从这儿入手看注册过程。姑且不论initialized_
字段的值。
// Add 'def' to the registry with additional data 'data'. On failure, or if
// there is already an OpDef with that name registered, returns a non-okay
// status.
Status OpRegistry::RegisterAlreadyLocked(
const OpRegistrationDataFactory& op_data_factory) const {
std::unique_ptr<OpRegistrationData> op_reg_data(new OpRegistrationData);
Status s = op_data_factory(op_reg_data.get());
if (s.ok()) {
s = ValidateOpDef(op_reg_data->op_def);
if (s.ok() &&
!gtl::InsertIfNotPresent(®istry_, op_reg_data->op_def.name(),
op_reg_data.get())) {
s = errors::AlreadyExists("Op with name ", op_reg_data->op_def.name());
}
}
Status watcher_status = s;
if (watcher_) {
watcher_status = watcher_(s, op_reg_data->op_def);
}
if (s.ok()) {
op_reg_data.release();
} else {
op_reg_data.reset();
}
return watcher_status;
}
函数的注释写的很清楚了,新增一个def到register中。失败或者算子name已经被注册,返回非okey结果。
这个函数中构造了一个OpRegistrationData
对象,并最终对op_data_factory
进行了调用。
OpRegistrationData
的定义如下,其中包含了一个OpDef
的变量。
struct OpRegistrationData {
public:
OpRegistrationData() {}
OpRegistrationData(const OpDef& def) : op_def(def) {}
OpRegistrationData(const OpDef& def, const OpShapeInferenceFn& fn,
bool is_function = false)
: op_def(def), shape_inference_fn(fn), is_function_op(is_function) {}
OpDef op_def;
OpShapeInferenceFn shape_inference_fn;
bool is_function_op = false;
};
对op_data_factory
的调用构造了一个OpRegistrationData
空对象,最终进入wrapper.builder().Finalize(op_reg_data)
中进行处理。
wrapper.builder()
返回的是OpDefBuilder
对象。函数Finalize
的实现为:
Status OpDefBuilder::Finalize(OpRegistrationData* op_reg_data) const {
std::vector<string> errors = errors_;
*op_reg_data = op_reg_data_;
OpDef* op_def = &op_reg_data->op_def;
for (StringPiece attr : attrs_) {
FinalizeAttr(attr, op_def, &errors);
}
for (StringPiece input : inputs_) {
FinalizeInputOrOutput(input, false, op_def, &errors);
}
for (StringPiece output : outputs_) {
FinalizeInputOrOutput(output, true, op_def, &errors);
}
for (StringPiece control_output : control_outputs_) {
FinalizeControlOutput(control_output, op_def, &errors);
}
FinalizeDoc(doc_, op_def, &errors);
if (errors.empty()) return Status::OK();
return errors::InvalidArgument(str_util::Join(errors, "\n"));
}
这里把最开始wrapper
中保存的inputs_
、outputs_
、attrs_
等信息依次取出,用于构建OpDef
对象。
得到的OpDef
对象首先经过ValidateOpDef(op_reg_data->op_def);
进行校验,然后插入到Register
的registry_
中。
gtl::InsertIfNotPresent(®istry_, op_reg_data->op_def.name(),
op_reg_data.get()))
到这里就完成了一个算子的注册过程。
下面这个代码值得注意:
if (initialized_) {
TF_QCHECK_OK(RegisterAlreadyLocked(op_data_factory));
} else {
deferred_.push_back(op_data_factory);
}
只有在initialized_
是true时,才进行注册,否则把op_data_factory
放到deferred_
这个vector中。
注意到Register
类有如下两个方法:
// Ensures that all the functions in deferred_ get called, their OpDef's
// registered, and returns with deferred_ empty. Returns true the first
// time it is called. Prints a fatal log if any op registration fails.
bool OpRegistry::MustCallDeferred() const {
if (initialized_) return false;
initialized_ = true;
for (size_t i = 0; i < deferred_.size(); ++i) {
TF_QCHECK_OK(RegisterAlreadyLocked(deferred_[i]));
}
deferred_.clear();
return true;
}
// Calls the functions in deferred_ and registers their OpDef's
// It returns the Status of the first failed op registration or Status::OK()
// otherwise.
Status OpRegistry::CallDeferred() const {
if (initialized_) return Status::OK();
initialized_ = true;
for (size_t i = 0; i < deferred_.size(); ++i) {
Status s = RegisterAlreadyLocked(deferred_[i]);
if (!s.ok()) {
return s;
}
}
deferred_.clear();
return Status::OK();
}
可以看出,在特定的调用中,把deferred_
中保存的算子注册函数全部取出,执行RegisterAlreadyLocked
真正的执行算子注册过程。
这里有几点值得关注:
- 注册函数
Register
的输入是一个函数引用,这个函数接收一个OpRegistrationData
指针作为输入; Watcher
是一个监视器,当每次注册一个算子的时候,在注册步骤的最后都要调用一下这个监视器,它可方便对注册的操作进行监控,所有的算子注册动作都逃不过它的眼,可以根据需求定制特殊Watcher;- registry_`是已注册的算子真正存放的位置,它的结构很简单,是一个key为算子名、value为算子数据的map;
initialized_
和deferred_
是与注册模式相关的两个数据成员,注册器在注册操作时,分为两种模式:- 即时注册模式和懒惰注册模式
- 注册模式通过
initialized_
字段区分,true为即时注册模式,false为懒惰注册模式; - 在懒惰注册模式中,被注册的算子先 被保存在
deferred_
向量中,在特定的函数调用时再将deferred_
中的算子注册到registryy_
,而即时注册模式下,待注册的算子不用经过deferred_
,直接注册到registry_
。
-懒惰注册模式的使用场景是,部分算子组合的注册是原子的,即要么全部注册,要么全部不注册,因为这些算子之间可能会有相互依赖关系。 - 构造函数将
initialized_
设置为false,进入懒惰注册模式,随后一旦调用了MustCallDeferred
或者CallDeferred
中的任意一个,都会将initialized_
设置为true,进入即时注册模式。想要重新返回懒惰注册模式也很简单,只需要调用DeferRegistrations
即可。
参考
https://www.cnblogs.com/jicanghai/p/9539513.html
注:文中代码基于
tensorflow1.12.0
版本。
TVM系列文章汇总 | Vernlium
c++ stl algorithms | Vernlium
http://vernlium.github.io/2020/02/23/c-stl-algorithms/
C++ STL algorithms 头文件
translate_learning
leetcode_动态规划题目总结 | Vernlium
之前对动态规划类型的题目一直很恐惧,拿到题目后没有思路。最近针对动态规划类型的题目做个专项练习,期望拿到这些题目的时候不慌,按照一定的套路分析出解题思路。 动态规划
Netty学习记录
docker私有镜像仓库搭建
title: docker镜像仓库的安装-k8s_2
date: 2017-08-14 21:13:24
tags:
简介
镜像仓库是存储镜像和分发镜像的系统。docker的使用离不开镜像仓库。当前最大的镜像仓库是dockerhub
,这里面包含了很多镜像,比如各大常用软件的镜像,如nginx/ubuntu/mysql/redis等,还有很多个人开发者制作的镜像,或者是完全新的镜像或者是在现有镜像基础上开发的特殊需求的镜像等。
有时候开发者可能需要进行测试或者涉及安全问题不想把镜像流传到外网,这时就需要自己搭建私有镜像仓库,只能在自己到内网中访问。
下面就来讲一下私有镜像仓库如何搭建。
安装
本安装指导使用到操作系统是Ubuntu 14.04。
下面我们安装一个域名为vernlium.com
的镜像仓库。
环境检查
我们到镜像仓库是以docker容器到方式启动到,所以需要安装docker。
apt-get install docker-engine
另外,安装过程中需要用到except软件,所以安装前需要检查此软件是否已安装,没安装到进行安装。
#检查是否安装except
#安装except
apt-get install except
证书生成
expect -c "
set timeout 300
spawn openssl req -newkey rsa:2048 -nodes -sha256 -keyout certs/domain.key -x509 -days 365 -out certs/domain.crt
expect {
\"Country Name *\" {send \"CN\r\";exp_coutinue}
\"State or Province Name*\" {send \"JS\r\";exp_coutinue}
\"Locality Name *\" {send \"NJ\r\";exp_coutinue}
\"Organization Name *\" {send \"HW\r\";exp_coutinue}
\"Organiztional Unit Name *\" {send \"DW\r\";exp_coutinue}
\"Common Name *\" {send \"vernlium.com:5005\r\";exp_coutinue}
\"Email Address *\" {send \"[email protected]\r\"}
}
expect eof;
"
执行完上述命令后,会在执行脚本的目录下certs下生成两个文件:domain.crt和domain.key,这个是证书文件和私钥文件。
这个证书在K8S安装时需要用到。
生成认证文件
镜像仓库容器启动
使用如下命令启动
docker run -d -ti \
-p 5005:5000 \
--restart=always \
--name registry \
--log-opt max-size=50m \
-v /etc/localtime:/etc/localtime:ro \
-v `pwd`/auth:/auth \
-v /var/lib/docker/registry:/var/lib/registry \
-e "REGISTRY_AUTH=htpasswd" \
-e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
-e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
-v `pwd`/certs:/certs \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
registry
命令中挂载来三个文件夹,其作用为:
- auth: 认证信息
- certs: 证书信息
- /var/lib/docker/registry: 存放镜像文件到目录,如果镜像比较多且单个镜像比较大到话,这个文件夹到磁盘空间需要比较大。
连接到镜像仓库
镜像仓库搭建完成后,如何让一个机器连接到此镜像仓库从此镜像仓库拉取镜像和向此镜像仓库推送镜像呢?
- /etc/hosts文件中添加镜像仓库域名对应的ip: vernlium.com 100.120.45.26
- 证书:新建目录:
/etc/docker/certs.d/vernlium.com\:5005/
,将镜像仓库安装过程中生成的docker.crt拷贝到此文件夹下; - 密码文件,在文件
/root/.docker/config.json
文件中添加如下内容:
{
“auth”:{
"vernlium.com:5005":{
"auth":"dGVzdDphbmFu"
}
}
其中:那一串字符是userName:passwd
的base64加密后密文,可使用浏览器进行加密。
Chrome在任意页面,按F12,切到Console页签,使用如下方式加密:
上述操作完成后,执行'docker login vernlium.com:5000',按照提示输入镜像仓库的用户名(一般会给出默认值,就是config.json文件中配的)和密码,如果配置都正确的话,则显示:Login Successed
镜像仓库的使用
获取镜像列表
命令方式
命令方式只能在镜像仓库到机器上执行才行,其原理就是遍历存放镜像文件到目录,获取所有到镜像。
api方式
镜像仓库提供来一些api接口可以来查询镜像列表。可以通过浏览器或者其他api工具如postman等进行访问。
find /var/lib/docker/registry/ -print | grep 'v2/repositories' | grep 'current' | grep -v 'link' | sed -e 's/\/_manifests\/tags\//:/' | sed -e 's/\/current//' | sed -e 's/^.*repositories/vernlium.con:5005/' | sort
获取镜像列表
GET https://registry_ip:5005/v2/_catalog
获取单个镜像到tag列表
GET https://registry_ip:5005/v2/<name>/tags/list
删除镜像
DELETE https://registry_ip:5005/v2/<name>/manifests/<reference>
镜像仓库的清理
我们都知道docker镜像是以层的形式组织的,而镜像仓库提供的删除镜像的能力只是删除镜像的tag,而非真正的删除镜像的层文件。那么镜像仓库使用一段时间后,就会产生很多废弃的层文件,如何清理这些文件呢?镜像仓库提供了GC能力,就是清理无用的镜像层文件。
执行如下命令进行GC操作:
docker exec -it registry /bin/registry garbage-collect [--dry-run] /etc/docker/registry/config.yml
其中:
--dry-run
参数可选,加上此参数表示:运行GC列出可GC的文件列表,并不真正的进行GC,而不加此参数则表示直接进行GC操作。
参考
resnet-file
KENNETH REITZ
| Vernlium
kubernetes简介-k8s_1
title: kubernetes简介-k8s_1
date: 2017-08-09 20:55:48
tags:
什么是Kubernetes?
Kubernetes(k8s)是自动化容器操作的开源平台,这些操作包括部署,调度和节点集群间扩展。如果你曾经用过Docker容器技术部署容器,那么可以将Docker看成Kubernetes内部使用的低级别组件。Kubernetes不仅仅支持Docker,还支持其他的容器技术,比如Rocket。
使用Kubernetes可以:
- 自动化容器的部署和复制
- 随时扩展或收缩容器规模
- 将容器组织成组,并且提供容器间的负载均衡
- 很容易地升级应用程序容器到新版本
- 提供容器弹性伸缩,强大的故障发现和自我修复能力
由于kubernetes较长,因此很多人都习惯简写成k8s,用8替换中间的8个字母,类似于i18n(internationalization,国际化)。后面我们的介绍都是用k8s替代kubernetes。
“Kubernetes”是古希腊词汇,其原意是“万能的神”,因为希腊是航海大国,不可预测的海上风浪经常会引起翻船事故,为保佑船只和人的安全,古希腊人引用“万能的神”这个词作为舵手。
而Docker的意思是集装箱,正好让舵手掌管装载集装箱的船的航向,很贴切。
k8s相关概念
k8s中的概念很多,如Master、Node、Pod、Container、Service、Namespace、Replication Controller、Deployment、Endpoint、Volume、Label等等。要使用好k8s,首先要了解这些概念,下面来介绍一下这些概念。
在了解这些概念之前,我们先来看一下k8s的一个典型组网。
这个图中涉及了很多相关的概念。
kubernetes集群
kubernets集群由k8s master和node组成。这个组网中就1个master和2个node,在生产环境中master一般有多个(2n+1)组成一个集群,来实现高可用性。node可以有很多个,当node不够用时,可以不断扩容。node的规模需要master来支撑,因为单个master所能管理的node节点的个数是有限的。
master
master是集群的控制节点,负责整个集群的管理和控制。master节点可以运行在物理机或虚拟机中,因为它是整个集群的“大脑”,非常重要,所以要保证它的可用性与可靠性。可以把它独占一个物理机,或者放到虚拟机中,用master集群来保证其可靠性。
k8s master由三个组件(进程)组成:
- kube-apiserver: 提供http rest接口的服务进程,对k8s里面的所有资源进行增、删、改、查等操作。也是集群控制的入口;
- kube-controler-manager: k8s里所有资源对象的自动控制中心,比如各个node节点的状态、pod的状态等;
- kube-scheduler: 负责资源调度;
另外,集群中还有一个etcd进程,k8s里面的所有资源的数据全部保存在etcd中。这里也是一个etcd节点,生产环境中一般用集群。etcd 是一个应用在分布式环境下的 key/value 存储服务。利用 etcd 的特性,应用程序可以在集群**享信息、配置、leader选举或作服务发现,etcd 会在集群的各个节点中复制这些数据并保证这些数据始终正确。
node
除了master,k8s集群中的其他节点是node节点。node节点可以是一台物理机或者虚拟机。node节点是k8s集群中的工作负载节点,master会把一些任务调度到node节点上进行。当某个node出现故障时,master会把这个节点上的任务转移到其他节点上。
每个node节点上会运行3个组件(进程):
- kubelet:与master的apiserver进程保持通信,上报node节点信息和状态,同时负责pod对应的容器的创建、启停等;
- kube-proxy:实现k8s service的通信和负载均衡等;
- Docker Engine:docker引擎,负责本机的容器创建和管理工作。
Node节点可以在运行期间动态添加到k8s集群,在默认情况下kubelet会向Master注册自己。一旦node被纳入集群管理范围,kubelet进程就会定时向Master节点汇报自身的情况,例如操作系统、Docker版本、cpu和内存、运行的pod等,这样master可以获得每个node的资源情况,可以实现高效的资源调度。
而某个Node超过指定时间不上报信息时,Master会认为此node失效了,会把此node的状态标记为不可用(Not Ready),随后会根据一些策略将此Node上的Pod进行转移。
Pod
Pod是Kubernetes最基本的操作单元,包含一个或多个紧密相关的容器;一个Pod中的多个容器应用通常是紧密耦合的,Pod在Node上被创建、启动或者销毁;每个Pod里运行着一个特殊的被称之为“根容器”的Pause容器,还包含一个或多个业务容器,这些业务容器共享Pause容器的网络栈和Volume挂载卷,因此他们之间通信和数据交换更为高效。同一个Pod里的容器之间仅需通过localhost就能互相通信。
k8s会为每个pod都分配一个唯一的IP地址(至少在node节点上是唯一的),称之为PodIP,一个pod的里的多个容器共享PodIP。
Pod被创建后,k8s master会将其调度到某个具体的Node上,随后该Node上的kubelet进程会将Pod中的一组容器启动。在默认情况下,当pod里面的某个容器停止时,k8s会自动检测到这个又问题的Pod,并重新启动这个Pod(重启Pod里的所有容器)。如果Pod所在的Node出现故障,不可用了,则K8s会将这个Node上的Pod重新调度到其他节点上(这个调度需要Replication Controller或者Deployment的支持)。
Label
上图中Node或pod上,都有一个小的标识,那个就是Label。一个Label是一个key=value的键值对,其中key和value均可自定义。Label可以附加到各种资源对象上,例如Pod、Node/Service等,每个资源对象都可以定义任意多个Label,Label可以动态的增加或者删除。
Label的作用是:通过给指定的资源对象绑定一个或多个Label,来实现多维度的资源分组管理,以便灵活的进行资源分配、调度、配置和部署等。给某个资源加上Label后,后面就可以通过Label Selector查询和筛选有某些Label的资源对象。
当前Label Selector有两种表达式:基于等式的(Equality-based)和基于集合的(Set-based)。
- 基于等式的Label Selector通过= 或者!= 来匹配标签,就类似与sql语句中
where name=anan
,例如:name=dbnode
:匹配具有标签是name=dbnode
的资源对象。 - 基于集合的Label Selector,通过in 或not in来匹配标签,例如:
name in (dbnode,biznode,plnode)
,匹配所有具有Label:name=dbnode
或name=biznode
或name=plnode
的资源对象。
EndPoint
每个Pod都会有一个IP,而Pod中的容器都会开放一些端口对外提供服务,这个端口被称为容器端口(ContainerPort),PodIP+ContainerPort就组成了一个新概念——EndPoint。它代表了此Pod中的一个服务进程的对外同曦地址。当然一个Pod可以存在多个EndPoint,这取决于pod中的业务容器。
Service
从上Pod的介绍中,可以知道:Pods是短暂的,可能会重启或者转移,这样IP地址可能会改变,如果这个PodIP经常变化,我们就无法正常的使用它了。
Service是定义一组Pod以及访问这些Pod的策略的一层抽象。Service通过Label找到Pod组。因为Service是抽象的,所以在图表里通常看不到它们的存在,这也就让这一概念更难以理解。
K8s的Service定义了一个服务的访问入口地址,一组应用可以通过这个入口地址访问其背后的一组Pod组成的集群实例,Service可以通过Label Selector将一组pod纳入到此Service中。
每个Pod都有一个IP,而每个Pod都有一个独立的EndPoint被客户端访问,那么多个Pod副本组成的一个集群来提供服务的话,客户端就需要负载均衡来访问它们。K8s的负载均衡是通过Node上的kube-proxy来实现的(后面会专门写一篇博客来讲它,这一块关于网络的东西很有意思,而且我在工作中接触的也比较多,所以比较熟悉,研究了很多实例,并且看了kube-proxy的源码)。可以通过下图来描述Service的使用。
每个Service会分配一个全局唯一的虚拟IP地址,这个IP被称为ClusterIP。这样,每个服务就只有一个唯一入口,客户端调用就无需关心这个服务有多少个Pod提供。ClusterIP在Service的整个生命周期内是不会变化的,所以Pod重启或者因Node失效进行了转移,也不会影响到Service。
上面的讲述中,我们已经提到了3种IP:
- NodeIP: Node节点的IP,是一个真是存在的物理网络(哪怕是虚拟机),外部可以直接访问这个网络。
- PodIP: Pod的IP,这个IP是每个Node上的docker0网桥根据自己的IP地址段进行分配的,是一个虚拟的二层网络,K8s集群中的node之间,无论是nodeIP还是PodIP,都可以和这个网络互通,集群外的机器则无法通信,但是在Pod中可以和外部通信。
- CllusterIP: Service的IP,这个一个虚拟的IP,它仅作用于Service这个对象,由K8s管理和分配,它无法别ping,因为没有一个实体网络对象来响应。它属于k8s集群内部地址,无法直接在集群外部使用。
有一些情况下,服务是要提供给集群外的应用来使用的,当然k8s也提供了方法,后面再详细讲解。
Replication Controller 和 Replica Set
Replication Controller(后文简称RC)是kube-controller-manager组件中的一个Controller,它定义了一个期望的场景,即使得某个Pod的副本数量在任意时刻都满足某种期望值。RC的定义包含如下几个部分:
- Pod期望的副本数(replicas)
- 用于筛选目标Pod的Label Selector
- 当Pod的副本数量小于期望数量的时候,用于创建新Pod的Pod模版(template)
当定义了一个RC并提交到k8s集群后,master上的controller manager就会得到通知,定期检查集群中当前存活的目标Pod,并确保目标Pod的实例的数量等于RC的期望值。如果有过多的Pod在运行,就会停掉多的Pod,如果数量不够,则再根据Pod模版创建一些Pod。
下面的动图展示了一个例子:
Replication Controller是k8s中最开始版本的Pod自动管理和调度工具,它只支持基于等式的Label Selector,有一定的局限性,k8s 1.2版本中引入了Replica Set,是“下一代的RC”,它与RC的唯一区别是:Replica Set支持基于集合的Label Selector。这样Replica Set的功能更强大。
Deployment
Deployment也是k8s 1.2引入的新概念,它的目的是更好的解决Pod的编排问题。Deployment无论是作用与目的、yaml定义文件,还是使用方式,都和RC很相似,可以看作是RC的一次升级。Deployment内部使用Replica Set来实现Pod的部署与调度。
Delpoyment有如下几个典型的使用场景:
- 创建一个Deployment对象来生成对应Replica Set并完成Pod副本的创建
- 检查Deployment的状态来看部署动作是否完成(Pod副本的数量是否达到预期)
- 更新Deployment来创建新的Pod(比如升级镜像等)
- 如果当前Deployment不稳定,则回滚到先前的Deployment版本
- 暂停或恢复一个Deployment
Volume
上文中讲到,Pod可能被重启或转移,那么Pod中的容器中保存的文件就会丢失,为了解决这个问题,k8s提出了Volume的概念。当然,Volume还有一个作用是解决Pod中的容器之间的文件共享。
Volume是Pod中能够被多个容器访问的共享目录。k8s的Volume概念、用途和目的与Docker的Volume很类似,但是又不完全相同。
- k8s中的Volume定义在Pod上,然后被一个Pod中的多个容器挂载到具体的文件目录下。
- k8s的Volume与Pod的生命周期相同,但是与容器的生命周期不相关,当容器终止或重启是,Volume中的数据也不会丢失。
- k8s支持多种类型的Volume,比如GlusterFS、Ceph等分布式文件系统。
小结
上面介绍了k8s的相关概念,对k8s有了一个大概的认识。
多说无益,实践最重要,我们就来手动安装一下k8s集群来进行实践。
参考
- k8s官方文档
- 十分钟带你理解Kubernetes核心概念,文中的动图来自此文,建议多看几遍此文
- 《Kubernetes权威指南 从Docker到Kubernetes实践全接触》
旅行照片
梯度下降法
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.