提交 8512e018 编写于 作者: M miykael

init

上级 0d39f5e5
# 程序员电子书目录
本电子书目录持续更新中,如果您有可公开的电子图书,也欢迎大家通过提交 Issue 或者 合并请求的方式提供给我们。
### 电子书目录
@ 2020-11-01
- [《程序员》杂志 · 2016 精华本]()
- [《程序员》杂志 · 2017 精华本]()
- [《2017 技术大检阅》]()
- [《AI 工程师职业指南》]()
- [《VR 与 AR 开发实战》]()
- [《云计算演进与应用》]()
- [《互联网应用架构面面观》]()
- [《人工智能,为我所用》]()
- [《人工智能学术前沿》]()
- [《关于 C++ 你应该更新的知识》]()
- [《分布式数据库》]()
- [《前端开发创新实践》]()
- [《双 11 · 一场技术的决战》]()
- [《大数据技术深度实践》]()
- [《容器技术经验谈》]()
- [《微信小程序深度解析》]()
- [《技术视野》]()
- [《深入浅出区块链》]()
- [《物联网开发技术栈》]()
- [《移动开发十年》]()
\ No newline at end of file
[book]
authors = ["《程序员》"]
title = "《程序员》杂志 · 2017 精华本"
language = "zh"
multilingual = false
src = "src"
文件已添加
## C++14 实现编译期反射
文/祁宇
>本文将通过分析 magic _ get 源码来介绍 magic _ get 实现的关键技术,深入解析实现 pod 类型反射的原理。
### pod 类型编译期反射
反射是一种根据元数据来获取类内部信息的机制,通过元数据就可以获取对象的字段和方法等信息。C# 和 Java 的反射机制都是通过获取对象的元数据来实现的。反射可以用于依赖注入、ORM 对象-实体映射、序列化和反序列化等与对象本身信息密切相关的领域。比如 Java 的 Spring 框架,其依赖注入的基础是建立在反射的基础之上的,可以根据元数据获取类型的信息并动态创建对象。ORM 对象-实体之间的映射也是通过反射实现的。Java 和 C# 都是基于中间运行时的语言,中间运行时提供了反射机制,所以反射对于运行时语言来说很容易,但是对于没有中间运行时的语言,要想实现反射是很困难的。
在2016年的 CppCon 技术大会上,Antony Polukhin 做了一个关于 C++ 反射的演讲,他提出了一个实现反射的新思路,即无需使用宏、标记和额外的工具即可实现反射。看起来似乎是一件不可能完成的任务,因为 C++ 是没有反射机制的,无法直接获取对象的元信息。但是 Antony Polukhin 发现对 pod 类型使用 Modern C++ 的模版元技巧可以实现这样的编译期反射。他开源了一个 pod 类型的编译期反射库 magic _ get(https://github.com/apolukhin/magic_get),这个库也准备进入 boost。我们来看看 magic _ get 的使用示例。
```
#include <boost pfr="" core.hpp="">
struct foo
{
int some_integer;
char c;
};
foo f {777, '!'};
auto& r1 = boost::pfr::flat_get<0>(f); //通过索引来访问对象foo的第1个字段
auto& r2 = boost::pfr::flat_get<1>(f); //通过索引来访问对象foo的第2个字段</boost>
```
通过这个示例可以看到,magic _ get 确实实现了非侵入式访问 foo 对象的字段,不需要写任何宏、额外的代码以及专门的工具,直接在编译期就可以访问 pod 对象的字段,没有运行期负担,确实有点 magic。
本文将通过分析 magic _ get 源码来介绍 magic _ get 实现的关键技术,深入解析实现 pod 类型反射的原理。
### 关键技术
实现 pod 类型反射的思路是这样的:先将 pod 类型转换为对应的 tuple 类型,接下来将 pod 类型的值赋给 tuple,然后就可以通过索引去访问 tuple 中的元素了。所以实现 pod 反射的关键就是如何将 pod 类型转换为对应的 tuple 类型和 pod 值赋值给 tuple。
#### pod 类型转换为 tuple 类型
pod 类型对应的 tuple 类型是什么样的呢?以上面的 foo 为例,foo 对应的 tuple 应该是 `tuple<int, char>`,即 tuple 中的元素类型和顺序和 pod 类型中的字段完全一一对应。
根据结构体生成一个 tuple 的基本思路是,按顺序将结构体中每个字段的类型萃取出来并保存起来,后面再取出来生成对应的 tuple 类型。然而字段的类型是不同的,C++ 也没有一个能直接保存不同类型的容器,因此需要一个变通的方法,用一个间接的方法来保存萃取出来的字段类型,即将类型转换为一个 size _ t 类型的 id,将这个 id 保存到一个 `array<size_t, N>` 中,后面根据这个 id 来获取实际的 type 并生成对应的 tuple 类型。
这里需要解决的一个问题是如何实现类型和 id 的相互转换。
#### type 和 id 在编译期相互转换
先借助一个空的模版类用来保存实际的类型,再借助 C++ 14 的 constexpr 特性,在编译期返回某个类型对应的编译期 id,就可以实现 type 转换为 id 了。具体代码如下:
```
http://ipad-cms.csdn.net/cms/article/code/3445
```
上面的代码在编译期将类型 int 和 char 做了一个编码,将类型转换为一个具体的编译期常量,后面就可以根据这些编译期常量来获取对应的具体类型。
编译期根据 id 获取 type 的代码如下:
```
constexpr auto id_to_type( std::integral_constant<std::size_t, 6=""> ) noexcept { int res{}; return res; }
constexpr auto id_to_type( std::integral_constant<std::size_t, 9=""> ) noexcept { char res{}; return res; }</std::size_t,></std::size_t,>
```
上面的代码中 id _ to _ type 返回的是 id 对应的类型的实例,如果要获取 id 对应的类型还需要通过 decltype 推导出来。magic _ get 通过一个宏将 pod 基本类型都做了一个编码,以实现 type 和 id 在编译期的相互转换。
```
#define REGISTER_TYPE(Type, Index) \
constexpr std::size_t type_to_id(identity<type>) noexcept { return Index; } \
constexpr auto id_to_type( std::integral_constant<std::size_t, index=""> ) noexcept { Type res{}; return res; } \
// Register all base types here
REGISTER_TYPE(unsigned short , 1)
REGISTER_TYPE(unsigned int , 2)
REGISTER_TYPE(unsigned long long , 3)
REGISTER_TYPE(signed char , 4)
REGISTER_TYPE(short , 5)
REGISTER_TYPE(int , 6)
REGISTER_TYPE(long long , 7)
REGISTER_TYPE(unsigned char , 8)
REGISTER_TYPE(char , 9)
REGISTER_TYPE(wchar_t , 10)
REGISTER_TYPE(long , 11)
REGISTER_TYPE(unsigned long , 12)
REGISTER_TYPE(void* , 13)
REGISTER_TYPE(const void* , 14)
REGISTER_TYPE(char16_t , 15)
REGISTER_TYPE(char32_t , 16)
REGISTER_TYPE(float , 17)
REGISTER_TYPE(double , 18)
REGISTER_TYPE(long double , 19)</std::size_t,></type>
```
将类型编码之后,保存在哪里以及如何取出来是接着要解决的问题。magic _ get 通过定义一个 array 来保存结构体字段类型 id。
```
template <class t,="" std::size_t="" n="">
struct array {
typedef T type;
T data[N];
static constexpr std::size_t size() noexcept { return N; }
};</class>
```
array 中的定长数组 data 中保存字段类型对应的 id,数组下标就是字段在结构体中的位置索引。
#### 萃取 pod 结构体字段
前面介绍了如何实现字段类型的保存和获取,那么这个字段类型是如何从 pod 结构体中萃取出来的呢?具体的做法分为三步:
- 定义一个保存字段类型 id 的 array;
- 将 pod 的字段类型转换为对应的 id,按顺序保存到 array 中;
- 筛除 array 中多余的部分。
下面是具体实现代码:
```
template <class t="">
constexpr auto fields_count_and_type_ids_with_zeros() noexcept {
static_assert(std::is_trivial<t>::value, "Not applyable");
array<std::size_t, sizeof(t)=""> types{};
detect_fields_count_and_type_ids<t>(types.data, std::make_index_sequence<sizeof(t)>{});
return types;
}
template <class t="">
constexpr auto array_of_type_ids() noexcept {
constexpr auto types = fields_count_and_type_ids_with_zeros<t>();
constexpr std::size_t count = count_nonzeros(types);
array<std::size_t, count=""> res{};
for (std::size_t i = 0; i < count; ++i) {
res.data[i] = types.data[i];
}
return res;
}</std::size_t,></t></class></sizeof(t)></t></std::size_t,></t></class>
```
定义 array 时需要定义一个固定的数组长度,长度为多少合适呢?应按结构体最多的字段数来确定。因为结构体的字段数最多为 sizeof(T),所以 array 的长度设置为 sizeof(T)。array 中的元素全部初始化为0。一般情况下,结构体字段数一般不会超过 array 的长度,那么 array 中就就会出现多余的元素,所以还需要将 array 中多余的字段移除,只保存有效的字段类型 id。具体的做法是计算出 array 中非零的元素有多少,接着再把非零的元素赋给一个新的 array。下面是计算 array 非零元素个数,同样是借助 constexpr 实现编译期计算。
```
template <class array="">
constexpr auto count_nonzeros(Array a) noexcept {
std::size_t count = 0;
for (std::size_t i = 0; i < Array::size() && a.data[i]; ++i)
++ count;
return count;
}</class>
```
由于字段是按顺序保存到 array 中的,所以在元素值为0时的 count 就是有效的元素个数。接下来我们来看看 detect _ fields _ count _ and _ type _ ids 的实现,这个 constexpr 函数将结构体中的字段类型 id 保存到 array 的 data 中。
```
detect_fields_count_and_type_ids<t>(types.data, std::make_index_sequence<sizeof(t)>{});</sizeof(t)></t>
```
detect _ fields _ count _ and _ type _ ids 的第一个参数为定长数组 array <std::size _ t, sizeof(T)> 的 data,第二个参数是一个 std::index _ sequence 整形序列。detect _ fields _ count _ and _ type _ ids 具体实现代码如下:
```
template <class t,="" std::size_t="" i0,="" std::size_t...="" i="">
constexpr auto detect_fields_count_and_type_ids(std::size_t* types, std::index_sequence<i0, i...="">) noexcept
-> decltype( type_to_array_of_type_ids<t, i0,="" i...="">(types) )
{
return type_to_array_of_type_ids<t, i0,="" i...="">(types);
}
template <class t,="" std::size_t...="" i="">
constexpr T detect_fields_count_and_type_ids(std::size_t* types, std::index_sequence<i...>) noexcept {
return detect_fields_count_and_type_ids<t>(types, std::make_index_sequence<sizeof...(i) -="" 1="">{});
}
template <class t="">
constexpr T detect_fields_count_and_type_ids(std::size_t*, std::index_sequence<>) noexcept {
static_assert(!!sizeof(T), "Failed for unknown reason");
return T{};
}</class></sizeof...(i)></t></i...></class></t,></t,></i0,></class>
```
上面的代码是为了将 index _ sequence 展开为 0,1,2..., sizeof(T) 序列,得到这个序列之后,再调用 type _ to _ array _ of _ type _ ids 函数实现结构体中的字段类型 id 保存到 array 中。
在讲 type _ to _ array _ of _ type _ ids 函数之前我们先看一下辅助结构体 ubiq。保存 pod 字段类型 id 实际上是由辅助结构体 ubiq 实现的,它的实现如下:
```
template <std::size_t i="">
struct ubiq {
std::size_t* ref_;
template <class type="">
constexpr operator Type() const noexcept {
ref_[I] = type_to_id(identity<type>{});
return Type{};
}
};</type></class></std::size_t>
```
这个结构体比较特殊,我们先把它简化一下。
```
struct ubiq {
template <class type="">
constexpr operator Type() const {
return Type{};
};
};</class>
```
这个结构体的特殊之处在于它可以用来构造任意 pod 类型,比如 int、char、double 等类型。
```
int i = ubiq{};
double d = ubiq{};
char c = ubiq{};
```
因为 ubiq 构造函数所需要的类型由编译器自动推断出来,所以它能构造任意 pod 类型。通过 ubiq 结构体获取了需要构造的类型之后,我们还需要将这个类型转换为 id 按顺序保存到定长数组中。
```
template <std::size_t i="">
struct ubiq {
std::size_t* ref_;
template <class type="">
constexpr operator Type() const noexcept {
ref_[I] = type_to_id(identity<type>{});
return Type{};
}
};</type></class></std::size_t>
```
上面的代码中先将编译器推导出来的类型转换为 id,然后保存到数组下标为 I 的位置。
再回头看 type _ to _ array _ of _ type _ ids 函数。
```
template <class t,="" std::size_t...="" i="">
constexpr auto type_to_array_of_type_ids(std::size_t* types) noexcept -> decltype(T{ ubiq<i>{types}... }) {
return T{ ubiq<i>{types}... };
}</i></i></class>
```
type _ to _ array _ of _ type _ ids 有两个模版参数,第一个 T 是 pod 结构体的类型,第二个 size _ t...为0到 sizeof(T) 的整形序列,函数的入参为 size _ t*,它实际上是 `array<std::size_t, sizeof(T)>` 的 data,用来保存 pod 字段类型 id。
保存字段类型的关键代码是这一行:T{ ubiq〈I〉{types}... },这里利用了 pod 类型的构造函数,通过 initializer _ list 构造,编译器会将 T 的字段类型推导出来,并借助 ubiq 将字段类型转换为 id 保存到数组中。这个就是 magic _ get 中的 magic。
将 pod 结构体字段 id 保存到数组中之后,接下来就需要将数组中的 id 列表转换为 tuple 了。
#### pod 字段 id 序列转换为 tuple
pod 字段 id 序列转换为 tuple 的具体做法分为两步:
- 将 array 中保存的字段类型 id 放入整形序列 std::index _ sequence;
- 将 index _ sequence 中的类型 id 转换为对应的类型组成 tuple。
下面是具体的实现代码:
```
template <std::size_t i,="" class="" t,="" std::size_t="" n="">
constexpr const T& get(const array<t,n>& a) noexcept {
return a.data[I];
}
template <class t,="" std::size_t...="" i="">
constexpr auto array_of_type_ids_to_index_sequence(std::index_sequence<i...>) noexcept {
constexpr auto a = array_of_type_ids<t>();
return std::index_sequence< get<i>(a)...>{};
}</i></t></i...></class></t,n></std::size_t>
```
get 是返回数组中某个索引位置的元素值,即类型 id,返回的 id 放入 std::index _ sequence 中,接着就是通过 index _ sequence 将 index _ sequence 中的 id 转换为 type,组成一个 tuple。
```
template <std::size_t... i="">
constexpr auto as_tuple_impl(std::index_sequence<i...>) noexcept {
return std::tuple< decltype( id_to_type(std::integral_constant<std::size_t, i="">{}) )... >{};
}
template <class t="">
constexpr auto as_tuple() noexcept {
static_assert(std::is_pod<t>::value, "Not applyable");
constexpr auto res = as_tuple_impl(
array_of_type_ids_to_index_sequence<t>(
std::make_index_sequence< decltype(array_of_type_ids<t>())::size() >()
)
);
static_assert(sizeof(res) == sizeof(T), "sizes check failed");
static_assert(
std::alignment_of<decltype(res)>::value == std::alignment_of<t>::value,
"alignment check failed"
);
return res;
}</t></decltype(res)></t></t></t></class></std::size_t,></i...></std::size_t...>
```
id _ to _ type 返回的是某个 id 对应的类型实例,所以还需要 decltype 来推导类型。这样我们就可以根据 T 来获取一个 tuple 类型了,接下来是要将 T 的值赋给 tuple,然后就可以根据索引来访问 T 的字段了。
#### pod 赋值给 tuple
对于 clang 编译器,pod 结构体是可以直接转换为 std::tuple 的,所以对于 clang 编译器来说,到这一步就结束了。
```
template <std::size_t i,="" class="" t="">
decltype(auto) get(const T& val) noexcept {
auto t = reinterpret_cast<const decltype(detail::as_tuple<t="">())*>( std::addressof(val) );
return get<i>(*t);
}</i></const></std::size_t>
```
然而,对于其他编译器,如 msvc 或者 gcc,tuple 的内存并不是连续的,不能直接将 T 转换为 tuple,所以更通用的做法是先做一个内存连续的 tuple,然后就可以将 T 直接转换为 tuple 了。
##### 内存连续的 tuple
下面是实现内存连续的 tuple 代码:
```
template <std::size_t n,="" class="" t="">
struct base_from_member {
T value;
};
template <class i,="" class="" ...tail="">
struct tuple_base;
template <std::size_t... i,="" class="" ...tail="">
struct tuple_base< std::index_sequence<i...>, Tail... >
: base_from_member<i ,="" tail="">...
{
static constexpr std::size_t size_v = sizeof...(I);
constexpr tuple_base() noexcept = default;
constexpr tuple_base(tuple_base&&) noexcept = default;
constexpr tuple_base(const tuple_base&) noexcept = default;
constexpr tuple_base(Tail... v) noexcept
: base_from_member<i, tail="">{ v }...
{}
};
template <>
struct tuple_base<std::index_sequence<> > {
static constexpr std::size_t size_v = 0;
};
template <class ...values="">
struct tuple: tuple_base<
std::make_index_sequence<sizeof...(values)>,
Values...>
{
using tuple_base<
std::make_index_sequence<sizeof...(values)>,
Values...
>::tuple_base;
};</sizeof...(values)></sizeof...(values)></class></std::index_sequence<></i,></i></i...></std::size_t...></class></std::size_t>
```
base _ from _ member 用来保存 tuple 元素的索引和值,tuple _ base 派生于 base _ from _ member,自动生成 tuple 中每一个类型的 base _ from _ member,tuple 派生于 tuple _ base 用来简化 tuple _ base 的定义。再给 tuple 增加一个根据索引获取元素的辅助方法。
```
template <std::size_t n,="" class="" t="">
constexpr const T& get_impl(const base_from_member<n, t="">& t) noexcept {
return t.value;
}
template <std::size_t n,="" class="" ...t="">
constexpr decltype(auto) get(const tuple<t...>& t) noexcept {
static_assert(N < tuple<t...>::size_v, "Tuple index out of bounds");
return get_impl<n>(t);
}</n></t...></t...></std::size_t></n,></std::size_t>
```
这样就可以通过 get 就可以获取 tuple 中的元素了。
到此,magic _ get 的核心代码分析完了。由于实际的代码会更复杂,为了让读者能更容易看懂,我选取的是简化版的代码,完整的代码可以参考 GitHub 上的 [magic_get](https://github.com/apolukhin/magic_get) 或者简化版的代码[https://github.com/qicosmos/cosmos/blob/master/pod_reflection.hpp](https://github.com/qicosmos/cosmos/blob/master/pod_reflection.hpp)
### 总结
magic _ get 实现了对 pod 类型的反射,可以直接通过索引来访问 pod 结构体的字段,而不需要任何额外的宏、标记或工具,确实很 magic。magic _ get 主要是通过 C++11/14 的可变模版参数、constexpr、index _ sequence、pod 构造函数以及很多模版元技巧实现的。那么 magic _ get 可以用来做些什么呢?根据 magic _ get 无需额外的负担和代码就可以实现编译期反射的特点,很适合做 ORM 数据库访问引擎和通用的序列化/反序列化库,我相信还有更多潜力和应用等待我们去发掘。
Modern C++ 的一些看似平淡无奇的特性组合在一起就能产生神奇的魔力,让人不禁赞叹 Modern C++ 蕴藏了无限的可能性与神奇。
\ No newline at end of file
此差异已折叠。
## Heron:Twitter 的新一代流处理引擎原理篇
文 /吕能,吴惠君,符茂松
>本文介绍了流计算的背景和重要概念,并详细分析了 Twitter 目前的流计算引擎—— Heron的结构及重要组件,希望能借此为大家提供一些在设计和构建流计算系统时的经验。
流计算又称实时计算,是继以 Map-Reduce 为代表的批处理之后的又一重要计算模型。随着互联网业务的发展以及数据规模的持续扩大,传统的批处理计算难以有效地对数据进行快速低延迟处理并返回结果。由于数据几乎处于不断增长的状态中,及时处理计算大批量数据成为了批处理计算的一大难题。在此背景之下,流计算应运而生。相比于传统的批处理计算,流计算具有低延迟、高响应、持续处理的特点。在数据产生的同时,就可以进行计算并获得结果。更可以通过 Lambda 架构将即时的流计算处理结果与延后的批处理计算结果结合,从而较好地满足低延迟、高正确性的业务需求。
Twitter 由于本身的业务特性,对实时性有着强烈的需求。因此在流计算上投入了大量的资源进行开发。第一代流处理系统 Storm 发布以后得到了广泛的关注和应用。根据 Storm 在实践中遇到的性能、规模、可用性等方面的问题,Twitter 又开发了第二代流处理系统——Heron,并在2016年将它开源。
### 重要概念定义
在开始了解 Heron 的具体架构和设计之前,我们首先定义一些流计算以及在 Heron 设计中用到的基本概念:
- Tuple:流计算任务中处理的最小单元数据的抽象。
- Stream:由无限个 Tuple 组成的连续序列。
- Spout:从外界数据源获得数据并生成 Tuple 的计算任务。
- Bolt:处理上游 Spout 或者 Bolt 生成的 Tuple 的计算任务。
- Topology:一个通过 Stream 将 Spout 和 Bolt 相连的处理 Tuple 的逻辑计算任务。
- Grouping:流计算中的 Tuple 分发策略。在 Tuple 通过 Stream 传递到下游
Bolt 的过程中,Grouping 策略决定了如何将一个 Tuple 路由给一个具体的
Bolt 实例。典型的 Grouping 策略有:随机分配、基于 Tuple 内容的分配等。
- Physical Plan:基于 Topology 定义的逻辑计算任务以及所拥有的计算资源,生成的实际运行时信息的集合。
在以上流处理基本概念的基础上,我们可以构建出流处理的三种不同处理语义:
- 至多一次(At-Most-Once): 尽可能处理数据,但不保证数据一定会被处理。吞吐量大,计算快但是计算结果存在一定的误差。
- 至少一次(At-Least-Once):在外部数据源允许 Replay(重演)的情况下,保证数据至少被处理一次。在出现错误的情况下会重新处理该数据,可能会出现重复处理多次同一数据的情况。保证数据的处理但是延迟升高。
- 仅有一次(Exactly-Once):每一个数据确保被处理且仅被处理一次。结果精确但是所需要的计算资源增多并且还会导致计算效率降低。
从上可知,三种不同的处理模式有各自的优缺点,因此在选择处理模式的时候需要综合考量一个 Topology 对于吞吐量、延迟、结果误差、计算资源的要求,从而做出最优的选择。目前的 Heron 已经实现支持至多一次和至少一次语义,并且正在开发对于仅有一次语义的支持。
### Heron 系统概览
保持与 Storm 接口(API)兼容是Heron的设计目标之一。因此,Heron 的数据模型与 Storm 的数据模型基本保持一致。每个提交给 Heron 的 Topology 都是一个由 Spout 和 Bolt 这两类结点(Vertex)组成的,以 Stream 为边(Edge)的有向无环图(Directed acyclic graph)。其中 Spout 结点是 Topology 的数据源,它从外部读取 Topology 所需要处理的数据,常见的如 kafka-spout,然后发送给后续的 Bolt 结点进行处理。Bolt 节点进行实际的数据计算,常见的运算如
Filter、Map 以及 FlatMap 等。
我们可以把 Heron 的 Topology 类比为数据库的逻辑查询计划。这种逻辑上的计划最后都要变成实质上的处理计划才能执行。用户在编写 Topology 时指定每个 Spout 和 Bolt 任务的并行度和 Tuple 在 Topology 中结点间的分发策略(Grouping)。所有用户提供的信息经过打包算法(Pakcing)的计算,这些 Spout 和 Bolt 任务(task)被分配到一批抽象容器中。最后再把这些抽象容器映射到真实的容器中,就可以生成一个物理上可执行的计划(Physical plan),它是所有逻辑信息(拓扑图、并行度、计算任务)和运行时信息(计算任务和容器的对应关系、实际运行地址)的集合。
#### 整体结构
总体上,Heron 的整体架构如图1所示。用户通过命令行工具(Heron-CLI)将
Topology 提交给 Heron Scheduler。再由 Scheduler 对提交的 Topology 进行资源分配以及运行调度。在同一时间,同一个资源平台上可以运行多个相互独立 Topology。
<img src="http://ipad-cms.csdn.net/cms/attachment/201706/5934ee61e69cc.png" alt="图1 Heron架构" title="图1 Heron架构" />
与 Storm 的 Service 架构不同,Heron 是 Library 架构。Storm 在架构设计上是基于服务的,因此需要设立专有的 Storm 集群来运行用户提交的 Topology。在开发、运维以及成本上,都有诸多的不足。而 Heron 则是基于库的,可以运行在任意的共享资源调度平台上。最大化地降低了运维负担以及成本开销。
目前的 Heron 支持 Aurora、YARN、Mesos 以及 EC2,而 Kubernetes 和
Docker 等目前正在开发中。通过可扩展插件 Heron Scheduler,用户可以根据不同的需求及实际情况选择相应的运行平台,从而达到多平台资源管理器的支持。
而被提交运行 Topology 的内部结构如图2所示,不同的计算任务被封装在多个容器中运行。这些由调度器调度的容器可以在同一个物理主机上,也可分布在多个主机上。其中每一个 Topology 的第一个容器(容器0)负责整个 Topology 的管理工作,主要运行一个 Topology Master 进程;其余各个容器负责用户提交的计算逻辑的实现,每个容器中主要运行一个 Stream Manager 进程,一个 Metrics Manager 进程,以及多个 Instance 进程。每个 Instance 都负责运行一个 Spout 或者
Bolt 任务(task)。对于 Topology Master、Stream Manager 以及
Instance 进程的结构及重要功能,我们会在本文的后面章节进行详细的分析。
<img src="http://ipad-cms.csdn.net/cms/attachment/201706/5934ee7b5afb6.png" alt="图2 Topology结构" title="图2 Topology结构" />
#### 状态(State)存储和监控
Heron 的 State Manager 是一个抽象的模块,它在具体实现中可以是 ZooKeeper 或者是文件系统。它的主要作用是保存各个 Topology 的各种元信息:Topology 的提交者、提交时间、运行时生成的 Physical Plan 以及 Topology Master 的地址等,从而为 Topology 的自我恢复提供帮助。
每个容器中的 Metrics Manager 负责收集所在容器的运行时状态指标(Metrics),并上传给监控系统。当前 Heron 版本中,简化的监控系统集成在
Topology Master 中。将来这一监控模块将会成为容器0中的一个独立进程。Heron 还提供 Heron-Tracker 和 Heron-UI 这两个工具来查看和监测一个数据中心中运行的所有 Topology。
#### 启动过程
在一个 Topology 中,Topology Master 是整个 Topology 的元信息管理者,它维护着完整的 Topology 元信息。而 Stream Manager 是每个容器的网关,它负责各个 Instance 之间的数据通信,以及和 Topology Master 之间的控制信令。
当用户提交 Topology 之后,Scheduler 便会开始分配资源并运行容器。每个容器中启动一个 Heron Executor 的进程,它区分容器0和其他容器,分别启动
Topology Master 或者 Stream Manager 等进程。在一个普通容器中,Instance 进程启动后会主动向本地容器的 Stream Manager 进行注册。当
Stream Manager 收到所有 Instance 的注册请求后,会向 Topology Master 发送包含了自己的所负责的 Instance 的注册信息。当 Topology Master 收到所有 Stream Manager 的注册信息以后,会生成一个各个 Instance,Stream Manager 的实际运行地址的 Physical Plan 并进行广播分发。收到了 Physical Plan 的各个 Stream Manager 之间就可以根据这一 Physical Plan 互相建立连接形成一个完全图,然后开始处理数据。
Instance 进行具体的 Tuple 数据计算处理。Stream Manager 则不执行具体的计算处理任务,只负责中继转发 Tuple。从数据流网络的角度,可以把 Stream Manager 理解为每个容器的路由器。所有 Instance 之间的 Tuple 传递都是通过 Stream Manager 中继。因此容器内的 Instance 之间通信是一跳(hop)的星形网络。所有的 Stream Manager 都互相连接,形成 Mesh 网络。容器之间的通信也是通过 Stream Manager 中继的,是通过两跳的中继完成的。
### 核心组件分析
#### TMaster
TMaster 是 Topology Master 的简写。与很多 Master-Slave 模式分布式系统中的 Master 单点处理控制逻辑的作用相同,TMaster 作为 Master 角色提供了一个全局的接口来了解 Topology 的运行状态。同时,通过将重要的状态信息(Physical Plan)等记录到 ZooKeeper 中,保证了 TMaster 在崩溃恢复之后能继续运行。
实际产品中的 TMaster 在启动的时候,会在 ZooKeeper 的某一约定目录中创建一个 Ephemeral Node 来存储自己的 IP 地址以及端口,让 Stream Manager 能发现自己。Heron 使用 Ephemeral Node 的原因包括:
- 避免了一个 Topology 出现多个 TMaster 的情况。这样就使得这个 Topology 的所有进程都能认定同一个 TMaster;
- 同一 Topology 内部的进程能够通过 ZooKeeper 来发现 TMaster 所在的位置,从而与其建立连接。
TMaster 主要有以下三个功能:
- 构建、分发并维护 Topology 的 Physical Plan;
- 收集各个 Stream Manager 的心跳,确认 Stream Manager 的存活;
- 收集和分发 Topology 部分重要的运行时状态指标(Metrics)。
由于 Topology 的 Physical Plan 只有在运行时才能确定,因此 TMaster 就成为了构建、分发以及维护 Physical Plan 的最佳选择。在 TMaster 完成启动和向
ZooKeeper 注册之后,会等待所有的 Stream Manager 与自己建立连接。在
Stream Manager 与 TMaster 建立连接之后,Stream Manager 会报告自己的实际 IP 地址、端口以及自己所负责的 Instance 地址与端口。TMaster 在收到所有 Stream Manager 报告的地址信息之后就能构建出 Physical Plan 并进行广播分发。所有的 Stream Manager 都会收到由 TMaster 构建的 Physical Plan,并且根据其中的信息与其余的 Stream Manager 建立两两连接。只有当所有的连接都建立完成之后,Topology 才会真正开始进行数据的运算和处理。当某一个
Stream Manager 丢失并重连之后,TMaster 会检测其运行地址及端口是否发生了改变;若改变,则会及时地更新 Physical Plan 并广播分发,使 Stream Manager 能够建立正确的连接,从而保证整个 Topology 的正确运行。
TMaster 会接受 Stream Manager 定时发送的心跳信息并且维护各个 Stream Manager 的最近一次心跳时间戳。心跳首先能够帮助 TMaster 确认 Stream Manager 的存活,其次可以帮助其决定是否更新一个 Stream Manager 的连接并且更新 Physical Plan。
TMaster 还会接受由 Metrics Manager 发送的一部分重要 Metrics 并且向
Heron-Tracker 提供这些 Metrics。Heron-Tracker 可以通过这些 Metrics 来确定 Topology 的运行情况并使得 Heron-UI 能够基于这些重要的 Metrics 来进行监控检测。典型的 Metrics 有:分发 Tuple 的次数,计算 Tuple 的次数以及处于 backpressure 状态的时间等。
非常值得注意的一点是,TMaster 本身并不参与任何实际的数据处理。因此它也不会接受和分发任何的 Tuple。这一设计使得 TMaster 本身逻辑清晰,也非常轻量,同时也为以后功能的拓展留下了巨大的空间。
#### Stream Manager 和反压(Back pressure)机制
Stmgr 是 Stream Manager 的简写。Stmgr 管理着 Tuple 的路由,并负责中继
Tuple。当 Stmgr 拿到 Physical Plan 以后就能根据其中的信息知道与其余的
Stmgr 建立连接形成 Mesh 网络,从而进行数据中继以及 Backpressure 控制。Tuple 传递路径可以通过图3来说明,图3中容器1的 Instance D(1D)要发送一个
Tuple 给容器4中的 Instance C(4C),这个 Tuple 经过的路径为:容器1的1D,容器1的 Stmgr,容器4的 Stmgr,容器4的4C。又比如从3A到3B的 Tuple 经过的路径为:3A,容器3的 Stmgr,3B。与 Internet 的路由机制对比,Heron 的路由非常简单,这得益于 Stmgr 之间两两相连,使得所有的 Instance 之间的距离不超过2跳。
<img src="http://ipad-cms.csdn.net/cms/attachment/201706/5934ee98f141a.png" alt="图3 Tuple发送路径示例" title="图3 Tuple发送路径示例" />
##### Acking
Stmgr 除了路由中继 Tuple 的功能以外,它还负责确认(Acking)Tuple 已经被处理。Acking 的概念在 Heron 的前身 Storm 中已经存在。Acking 机制的目的是为了实现 At-Least-Once 的语义。原理上,当一个 Bolt 实例处理完一个
Tuple 以后,这个 Bolt 实例发送一个特殊的 Acking Tuple 给这个 bolt 的上游 Bolt 实例或者 Spout 实例,向上游结点确认 Tuple 已经处理完成。这个过程层层向上游结点推进,直到 Spout 结点。实现上,当 Acking Tuple 经过
Stmgr 时候由异或(xor)操作标记 Tuple,由异或操作的特性得知是否处理完成。当一个 Spout 实例在一定时间内还没有收集到 Acking Tuple,那么它将重发对应的数据 Tuple。Heron 的 Acking 机制的实现与它的前任 Storm 一致。
##### Back Pressure
Heron 引入了反压(Back Pressure)机制,来动态调整 Tuple 的处理速度以避免系统过载。一般来说,解决系统过载问题有三种策略:1. 放任不管;2. 丢弃过载数据;3. 请求减少负载。Heron 采用了第三种策略,通过 Backpressure 机制来进行过载恢复,保证系统不会在过载的情况下崩溃。
Backpressure 机制触发过程如下:当某一个 Bolt Instance 处理速度跟不上
Tuple 的输入速度时,会造成负责向该 Instance 转发 Tuple 的 Stmgr 缓存不断堆积。当缓存大小超过一个上限值(Hight Water Mark)时,该 Stmgr 会停止从本地的 Spout 中读取 Tuple 并向 Topology 中的其他所有 Stmgr 发送一个“开始 Backpressure”的信息。而其余的 Stmgr 在接收到这一消息时也会停止从他们所负责的 Spout Instance 处读取并转发 Tuple。至此,整个 Topology 就不再从外界读入 Tuple 而只处理堆积在内部的未处理 Tuple。而处理的速度则由最慢的 Instance 来决定。在经过一定时间的处理以后,当缓存的大小减低到一个下限值(Low Water Mark)时,最开始发送“开始 Backpressure”的 Stmgr 会再次发送“停止 Backpressure”的信息,从而使得所有的 Stmgr 重新开始从 Spout Instance 读取分发数据。而由于 Spout 通常是从具有允许重演(Replay)的消息队列中读取数据,因此即使冻结了也不会导致数据的丢失。
注意在 Backpressure 的过程中两个重要的数值:上限值(High Water Mark)和下限值(Low Water Mark)。只有当缓存区的大小超过上限值时才会触发
Backpressure,然后一直持续到缓存区的大小减低到下限值时。这一设计有效地避免了一个 Topology 不停地在 Backpressure 状态和正常状态之间震荡变化的情况发展,一定程度上保证了 Topology 的稳定。
#### Instance
Instance 是整个 Heron 处理引擎的核心部分之一。Topology 中不论是 Spout 类型结点还是 Bolt 类型结点,都是由 Instance 来实现的。不同于 Storm 的
Worker 设计,在当前的 Heron 中每一个 Instance 都是一个独立的 JVM 进程,通过 Stmgr 进行数据的分发接受,完成用户定义的计算任务。独立进程的设计带来了一系列的优点:便于调试、调优、资源隔离以及容错恢复等。同时,由于数据的分发传送任务已经交由 Stmgr 来处理,Instance 可以用任何编程语言来进行实现,从而支持各种语言平台。
Instance 采用双线程的设计,如图4所示。一个 Instance 的进程包含 Gateway
以及 Task Execution 这两个线程。Gateway 线程主要控制着 Instance 与本地 Stmgr 和 Metrics Manager 之间的数据交换。通过 TCP 连接,Gateway 线程:1. 接受由 Stmgr 分发的待处理 Tuple;2. 发送经 Task Execution 处理的 Tuple 给 Stmgr;3. 转发由 Task Execution 线程产生的 Metrics 给 Metrics Manager。不论是 Spout 还是 Bolt,Gateway 线程完成的任务都相同。
Task Execution 线程的职责是执行用户定义的计算任务。对于 Spout 和 Bolt,Task Execution 线程会相应地去执行 open()和 prepare()方法来初始化其状态。如果运行的 Instance 是一个 Bolt 实例,那么 Task Execution 线程会执行 execute()方法来处理接收到的 Tuple;如果是 Spout,则会重复执行 nextTuple()方法来从外部数据源不停地获取数据,生成 Tuple,并发送给下游的
Instance 进行处理。经过处理的 Tuple 会被发送至 Gateway 线程进行下一步的分发。同时在执行的过程中,Task Execution 线程会生成各种 Metrics(tuple 处理数量,tuple 处理延迟等)并发送给 Metrics Manager 进行状态监控。
<img src="http://ipad-cms.csdn.net/cms/attachment/201706/5934eeb547157.png" alt="图4 Instance结构" title="图4 Instance结构" />
Gateway 线程和 Task Execution 线程之间通过三个单向的队列来进行通信,分别是数据进入队列、数据发送队列以及 Metrics 发送队列。Gateway 线程通过数据进入队列向 Task Execution 线程传入 Tuple;Task Execution 通过数据发送队列将处理完的 Tuple 发送给 Gateway 线程;Task Execution 线程通过
Metrics 发送队列将收集的 Metric 发送给 Gateway 线程。
### 总结
在本文中,我们介绍了流计算的背景和重要概念,并且详细分析了 Twitter 目前的流计算引擎—— Heron 的结构及重要组件。希望能借此为大家提供一些在设计和构建流计算系统时的经验,也欢迎大家向我们提供建议和帮助。如果大家对 Heron 的开发和改进感兴趣,可以在 [Github](https://github.com/twitter/heron) 上进行查看。
\ No newline at end of file
## Web 端 VR 开发初探
文/张乾
>随着硬件和软件技术的发展,产业界对虚拟现实(Virtual Reality)用户体验产生了重大期望。技术的进步也使我们可能通过现代浏览器借助开放 Web 平台获得这种用户体验。这将帮助 Web 成为创建、分发以及帮助用户获得虚拟现实应用和服务生态系统的重要基础平台。
### 引言
2016年最令科技界激动的话题,莫过于 VR 会如何改变世界。一些电影已开始涉足 VR,让用户不仅能看到 3D 影像,更能以“移形换影”之术身临其境,带来前所未有的沉浸式观影体验;此外,游戏领域也开始 VR 化,用户再也不用忍受游戏包里单一的场景。这些酷炫效果带来了巨大想象空间,VR 正在走近人们的生活。然而现实是,除了偶尔体验下黑科技的奇妙外,VR 并没有真正普及,在资本和硬件厂商狂热的背后,质疑声也此起彼伏。
目前,虽然 VR 硬件的发展已经走上了快车道,但内容却非常单薄。一部 VR 电影的成本相当高昂,VR 游戏也不逊色。内容创作成本的居高不下,导致了 VR 的曲高和寡。要想脱下那一层高冷的贵族华裳,飞入寻常百姓家,VR 尚需解决内容供给这一难题。以 HTML5 为代表的 Web 技术的发展,或将改变这一僵局。目前,最新的 Google Chrome 和 Mozilla Firefox 浏览器已经加入面向 HTML5 技术的 WebVR 功能支持,同时各方也正在起草并充实业界最新的 WebVR API 标准。基于 Web端的这些虚拟现实标准将进一步降低 VR 内容的技术创作成本及门槛,有利于世界上最大的开发者群体—HTML5(JavaScript)开发者进入 VR 内容创作领域。这不仅是 Web 技术发展历程上的显著突破,也为 VR 造就了借力腾飞的契机。
### Web 端 VR 的优势
#### Web 可降低 VR 体验门槛
Web 技术不仅使创作 VR 的成本更加低廉,而且大大降低技术门槛。WebVR 依托于 WebGL 技术的高速发展,利用 GPU 执行计算以及游戏引擎技术针对芯片级的 API 优化,提高了图形渲染计算能力,大大降低开发者进入 VR 领域的门槛,同时 WebVR 还可以更好地结合云计算技术,补足 VR 终端的计算能力,加强交互体验。
可以肯定,Web 扩展了 VR 的使用范围,广告营销,全景视频等领域已经涌现一批创新案例,很多生活化的内容也纳入了 VR 的创作之中,如实景旅游、新闻报道、虚拟购物等,其内容展示、交互都可以由 HTML5 引擎轻松创建出来。这无疑给其未来发展带来更多想象空间。
#### Web 开发者基数庞大
除了技术上的实现优势,Web 还能给 VR 带来一股巨大的创新动力,因为它拥有着广泛的应用范围与庞大的开发者基数,能帮助 VR 技术打赢一场人民战争,让 VR 不再只是产业大亨们的资本游戏,而是以平民化的姿态,进入广大用户日常生活的方方面面。
相信假以时日,VR 应用会像现在满目皆是的 App 一样,大量的 VR 开发者借助于 Web 端开发的低门槛而大量进入,同时各种稀奇古怪的创意层出不穷,虚拟现实成为电商商家必须的经营手段等。若到了这个阶段,VR 离真正的繁荣也就不远了。
### 开发 Web 端的 VR 内容
接下来我们通过实践操作来真正制作一些 Web 端的 VR 内容,体验 WebVR 的便捷优势。我们知道,许多 VR 体验是以应用程序的形式呈现的,这意味着你在体验 VR 前,必须进行搜索与下载。而 WebVR 则改变了这种形式,它将 VR 体验搬进了浏览器,Web+VR = WebVR 。在进入实践之前,下面先来分析一下 WebVR 实现的技术现状。
#### WebVR 开发的方式
在 Web 上开发 VR 应用,有下面三种方式:
HTML5+ JavaScnipt + WebGL + WebVR API
传统引擎 + Emscripten[1]
第三方工具,如A-Frame[2]
第一种方法是使用 WebGL 与 WebVR API 结合,在常规 Web 端三维应用的基础上通过 API 与 VR 设备进行交互,进而得到对应的 VR 实现。第二种是在传统引擎开发内容的基础上,比如 Unity、Unreal 等,使用 Emscripten 将 C/C++ 代码移植到 JavaScnipt版本中,进而实现 Web 端的 VR 。第三种是在封装第一种方法的基础上,专门面向没有编程基础的普通用户来生产 Web 端 VR 内容。在本文中我们主要以第一和第三种方法为例进行说明。
#### WebVR 草案
WebVR 是早期和实验性的 JavaScript API,它提供了访问如 Oculus Rift、HTC Vive 以及 Google Cardboard 等 VR 设备功能的 API。VR 应用需要高精度、低延迟的接口,才能传递一个可接受的体验。而对于类似 Device Orientation Event 接口,虽然能获取浅层的 VR 输入,但这并不能为高品质的 VR 提供必要的精度要求。WebVR 提供了专门访问 VR 硬件的接口,让开发者能构建舒适的 VR 体验。
WebVR API 目前可用于安装了 Firefox nightly 的 Oculus Rift、Chrome 的实验性版本和 Samsung Gear VR 的浏览器。
#### 使用 A-Frame 开发 VR 内容
如果想以较低的门槛体验一把 WebVR 开发,那么可以使 MozVR 团队开发的 A-Frame 框架。A-Frame 是一个通过 HTML 创建 VR 体验的开源 WebVR 框架。通过该框架构建的 VR 场景能兼容智能手机、PC、 Oculus Rift和 HTC Vive。MozVR 团队开发 A-Frame 框架的的是:让构建 3D/VR 场景变得更易更快,以吸引 Web 开发社区进入 WebVR 的生态。WebVR 要成功,需要有内容。但目前只有很少一部分 WebGL 开发者,却有数以百万的 Web 开发者与设计师。A-Frame 要把 3D/VR 内容的创造权力赋予给每个人,其具有如下的优势与特点:
- A-Frame 能减少冗余代码。冗余复杂的代码成为了尝鲜者的障碍,A-Frame 将复杂冗余的代码减至一行 HTML 代码,如创建场景则只需一个<a-scene>标签。
- A-Frame 是专为 Web 开发者设计的。它基于 DOM,因此能像其他 Web 应用一样操作 3D/VR 内容。当然,也能结合 box、d3、React 等 JavaScript 框架一起使用。
- A-Frame 让代码结构化。Three.js 代码通常是松散的,A-Frame 在 Three.js 之上构建了一个声明式的实体组件系统(entity-component-system)。另外,组件能发布并分享出去,其他开发者能以 HTML 的形式进行使用。
代码实现如下:
```
// 引入A-Frame框架
<script src="./aframe.min.js"></script>
<a-scene>
<!-- 定义并创建球体 -->
<a-sphere position="0 1 -1" radius="1" color="#EF2D5E"></a-sphere>
<!-- 定义交创建立方体 -->
<a-box width="1" height="1" rotation="0 45 0" depth="1" color="#4CC3D9" position="-1 0.5 1"></a-box>
<!-- 定义并创建圆柱体 -->
<a-cylinder position="1 0.75 1" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
<!-- 定义并创建底板 -->
<a-plane rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane>
<!-- 定义并创建基于颜色的天空盒背景-->
<a-sky color="#ECECEC"></a-sky>
<!-- 设置并指定摄像机的位置 -->
<a-entity position="0 0 4">
<a-camera></a-camera>
</a-entity>
</a-scene>
```
上述代码在 A-Frame 中执行的效果如图1所示。
<img src="http://ipad-cms.csdn.net/cms/attachment/201701/5865f54366e7e.png" alt="图1 A-Frame运行结果" title="图1 A-Frame运行结果" />
图1 A-Frame 运行结果
#### 使用 Three.js 开发 VR 内容
上文中我们提到另外了一种更加靠近底层同时更加灵活生产 WebVR 内容的方法,就是直接使用 WebGL+WebVR 的 API。这种方法相对于 A-Frame 的优势在于可以将VR 的支持方便地引入到我们自己的 Web3D 引擎中,同时对于底层,特别是渲染模块可以做更多优化操作从而提升 VR 运行时的性能与体验。
如果没有自己的 Web3D 引擎也没有关系,可以直接使用成熟的渲染框架,比如 Three.js 和 Babylon.js 等,这些都是比较流行且较为出色的 Web3D 端渲染引擎(框架)。接下来就以 Three.js 为例,说明如何在其上制作 WebVR 内容。
首先,对于任何渲染程序的三个要素是相似的,即是建立好 scene、renderer、camera。设置渲染器、场景以及摄像机的操作如下:
```
var renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// 创建Three.js的场景
var scene = new THREE.Scene();
// 创建Three.js的摄像机
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 10000);
// 调用WebVR API中的摄像机控制器对象,并将其与主摄像机进行绑定
var controls = new THREE.VRControls(camera);
// 设置为站立姿态
controls.standing = true;
// 调用WebVR API中的渲染控制器对象,并将其与渲染器进行绑定
var effect = new THREE.VREffect(renderer);
effect.setSize(window.innerWidth, window.innerHeight);
// 创建一个全局的VR管理器对象,并进行初始化的参数设置
var params = {
hideButton: false, // Default: false.
isUndistorted: false // Default: false.
};
var manager = new WebVRManager(renderer, effect, params);
```
上述代码即完成了渲染前的初始化设置。接下来需要向场景中加具体的模型对象,主要操作如下所示:
```
function onTextureLoaded(texture) {
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(boxSize, boxSize);
var geometry = new THREE.BoxGeometry(boxSize, boxSize, boxSize);
var material = new THREE.MeshBasicMaterial({
map: texture,
color: 0x01BE00,
side: THREE.BackSide
});
// Align the skybox to the floor (which is at y=0).
skybox = new THREE.Mesh(geometry, material);
skybox.position.y = boxSize/2;
scene.add(skybox);
// For high end VR devices like Vive and Oculus, take into account the stage
// parameters provided.
setupStage();
}
// Create 3D objects.
var geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
var material = new THREE.MeshNormalMaterial();
var targetMesh = new THREE.Mesh(geometry, material);
var light = new THREE.DirectionalLight( 0xffffff, 1.5 );
light.position.set( 10, 10, 10 ).normalize();
scene.add( light );
var ambientLight = new THREE.AmbientLight(0xffffff);
scene.add(ambientLight);
var loader = new THREE.ObjectLoader();
loader.load('./assets/scene.json', function (obj){
mesh = obj;
// Add cube mesh to your three.js scene
scene.add(mesh);
mesh.traverse(function (node) {
if (node instanceof THREE.Mesh) {
node.geometry.computeVertexNormals();
}
});
// Scale the object
mesh.scale.x = 0.2;
mesh.scale.y = 0.2;
mesh.scale.z = 0.2;
targetMesh = mesh;
// Position target mesh to be right in front of you.
targetMesh.position.set(0, controls.userHeight * 0.8, -1);
});
```
最后的操作便是在 requestAnimationFrame 设置更新。在 animate 的函数中,我们要不断地获取 HMD 返回的信息以及对 camera 进行更新。
```
// Request animation frame loop function
var lastRender = 0;
function animate(timestamp) {
var delta = Math.min(timestamp - lastRender, 500);
lastRender = timestamp;
// Update VR headset position and apply to camera.
//更新获取HMD的信息
controls.update();
// Render the scene through the manager.
//进行camera更新和场景绘制
manager.render(scene, camera, timestamp);
requestAnimationFrame(animate);
}
```
最后,程序运行的效果如图2所示,可以直接在手机上通过 VR 模式并配合 Google Cardboard 即可体验无需下载的 VR 内容[3]。
![enter image description here](http://images.gitbook.cn/218ed470-0ae4-11e8-b617-813d2e493d45)
图2 Three.js 运行结果
上述示例中的代码实现可从[4]中下载。
### 经验与心得
通过上述介绍我们基本可以实现一个具有初步交互体验的 Web 端 VR 应用,但这只是第一步,单纯技术上的实现距离真正的可工程化还有一定差距。因为最终工程化之后面向用户的产品必须比技术原型要考虑更多具体的东西,比如渲染的质量、交互的流畅度、虚拟化的沉浸度等,这些都最终决定用户是否会持续使用产品、接受产品所提供的服务等,所以将上述技术在工程化应用之前还有很多的优化与改进工作要做。以下是个人在做 Web 端 VR 应用过程中体会的一些心得经验,分享出来供读者参考。
- 引擎的选用。如果是使用已有的 WebGL 引擎,则可参考[5]中的文档来进行 VR SDK 集成。这里边需要做到引擎层与 VR SDK 层兼容,以及 VR 模式与引擎的工具部分的整合,也可以参考桌面引擎如 Unity3D 和 Unreal 在 VR SDK 集成上的开发模式。如果选用第三方的 WebGL 引擎则有 Three.js 或 Babylon.js 等可选,这些主流的 WebGL 引擎都已经(部分功能)集成了 VR SDK。
- 调试的设备。调试 Web 端的 VR 应用同样需要有具体的 VR 设备的支持。对于桌面 WebM 内容还是要尽量使用 HTC Vive 或 Oculus 等强沉浸感 VR 设备。对于移动 Web 应用,由于 Android 平台上的各浏览器的差异较大,表现也会不太一致,所以建议使用 iOS 设备进行开发与调试,但是在最终发布前仍要对更多的 Andnoid 设备进行适配性测试与优化。
- 性能的优化。在 Web 端做三维的绘制与渲染,性能还是主要瓶颈,因而要尽可能的提高实时渲染的性能,这样才能有更多资源留给 VR 部分。目前的 WebVR 在渲染实时中并没有像桌面 VRSDK 一样可以调用众多的 GPU 底层接口做诸如 Stereo rendering 等深层次的优化,因而对性能的占用还是较多。
- 已知的问题。目前,WebVR 仍然不太稳定,还会有诸多的 Bug,比如某些情况下会有设备跟踪丢失的情况,而且效率也不是太高。大多数 WebVR 应用可以作为后期产品的储备和预研,但要推出真正可供用户使用并流畅体验的产品,还是有较长的路要走。
### 结束语
许多人将即将过去的2016称为 VR 元年,在这一年中 VR 的确经历了突飞猛进的发展,体现在技术与生态等各个方面。在新的2017年,相信 VR 必将会有更大的发展与进步,作为技术工作者,我们更应该从自身的技术专长作为出发点,参与到新技术对社会与生活的变革中来。
#### 参考链接
[1] http://kripken.github.io/emscripten-site/<br>
[2] https://aframe.io/<br>
[3] http://www.shxt3d.com/ WebVR /index.html<br>
[4] https://github.com/bugrunnerzhang/hello WebVR.git<br>
[5] https://mozVR.com/
\ No newline at end of file
## 京东分布式数据库系统演进之路
文/张成远
关于数据库的使用,在京东有几个趋势,早期主要用 SQL Server 及 Oracle 也有少量采用 MySQL,考虑到业务发展技术积累及使用成本等因素,很多业务都开始使用 MySQL,包括早期使用 SQL Server 及 Oracle 的很多核心业务也都渐渐开始迁移到 MySQL,单机 MySQL 往往无法支撑这类业务,需要考虑分布式解决方案,另外原本使用 MySQL 的业务随着数据量及访问量的增加也会遇到瓶颈,最终考虑采用分布式解决方案,整个京东随着业务发展采用数据库的趋势如图1所示。
<img src="http://ipad-cms.csdn.net/cms/attachment/201702/588854a1c91b6.png" alt="图1 业务使用数据库演变趋势 " title="图1 业务使用数据库演变趋势 " />
图1 业务使用数据库演变趋势
分布式数据库解决方案有很多种,在各个互联网公司也是非常普遍,本质上就是将数据拆开存储在多个节点上从而缓解单节点的压力,业务层面也可以根据业务特点自行进行拆分,如图2所示,假设有一张 user 表,以 ID 为拆分键,假设拆分成两份,最简单的就是奇数 ID 的数据落到一个存储节点上,偶数 ID 的数据落到另外一个存储节点上,实际部署示意图如图3所示。
<img src="http://ipad-cms.csdn.net/cms/attachment/201702/5888559fb2abd.png" alt="图2 数据拆分示意图" title="图2 数据拆分示意图" />
图2 数据拆分示意图
<img src="http://ipad-cms.csdn.net/cms/attachment/201702/588855cd1bf85.png" alt="图3 系统部署示意图" title="图3 系统部署示意图" />
图3 系统部署示意图
除了业务层面做拆分,也可以考虑采用较为通用的一些解决方案,主要分为两类,一类是客户端解决方案,这种方案是在业务应用中引入特定的客户端包,通过该客户端包完成数据的拆分查询及结果汇总等操作,这种方案对业务有一定侵入性,随着业务应用实例部署的数量越来越多,数据库端可能会面临连接数据库压力也越来越大的问题,另外版本升级也比较困难,优点是链路较短,从应用实例直接到数据库。
另一类是中间件的解决方案,这种方案是提供兼容数据库传输协议及语法规范的代理,业务在连接中间件的时候可以直接使用传统的 JDBC 等客户端,从而大大减轻业务开发层面的负担,弊端是中间件的开发难度会比客户端方案稍微高一点,另外网络传输链路上多走了一段,理论上对性能略有影响,实际使用环境中这些系统都是在机房内网访问,这种网络上的影响完全可以忽略不计。
根据上述分析,为了更好得支撑京东大量的大规模数据量业务,我们开发了一套兼容 MySQL 协议的分布式数据库的中间件解决方案,称之为 JProxy,这套方案经过了多次演变最终完成并支撑了京东全集团的去 Oracle/SQL Server 任务。
JProxy 第一个版本如图4所示,每个 JProxy 都会有一个配置文件,我们会在其中配置相应业务的库表拆分信息及路由信息,JProxy 接收到 SQL 以后会对 SQL 进行解析再根据路由信息决定 SQL 是否需要重写及该发往哪些节点,等各节点结果返回以后再将结果汇总按照 MySQL 传输协议返回给应用。
<img src="http://ipad-cms.csdn.net/cms/attachment/201702/5888560849bc6.png" alt="图4 JProxy版本一" title="图4 JProxy版本一" />
图4 JProxy 版本一
结合上文的例子,当用户查询 user 这张表时假设 SQL 语句是 select * from user where id = 1 or id = 2,当收到这条 SQL 以后,JProxy 会将 SQL 拆分为 select * from user where id=1 及 select * from user where id = 2, 再分别把这两条 SQL 语句发往后端的节点上,最后将两个节点上获取到的两条记录一并返回给应用。
这种方案在业务库表比较少的时候是可行的,随着业务的发展,库表的数量可能会不断增加,尤其是针对去 Oracle 的业务在切换数据库的时候可能是一次切换几张表,下一次再切换另外几张表,这就要求经常修改配置文件。另外 JProxy 在部署的时候至少需要两份甚至多份,如图5所示,此时面临一个问题是如何保证所有的配置文件在不断修改的过程中是完全一致的。在早期运维过程中,我们靠人工修改完一份配置文件,再将相应的配置文件拷贝给其他 JProxy,确保 JProxy 配置文件内容一致,这个过程心智负担较重且容易出错。
<img src="http://ipad-cms.csdn.net/cms/attachment/201702/5888564a1625e.png" alt="图5 配置文件" title="图5 配置文件" />
图5 配置文件
在之后的版本中我们引入了 JManager 模块,这个模块负责的工作是管理配置文件中的路由元信息,如图6所示。JProxy 的路由信息都是到JManager统一获取,我们只需要通过 JManager 往元数据库里添加修改路由元数据,操作完成以后通知各个 JProxy 动态加载路由信息就可以保证每个 JProxy 的路由信息是完全一致的,从而解决维护路由元信息一致性的痛点。
<img src="http://ipad-cms.csdn.net/cms/attachment/201702/5888566f5d22e.png" alt="图6 JProxy版本二" title="图6 JProxy版本二" />
图6 JProxy 版本二
在提到分布式数据库解决方案时一定会考虑的一个问题是扩容,扩容有两种方式,一种我们称之为 Re-sharding 方案,简单的说就是一片拆两片,两片拆为四片,如图7所示,原本只有一个 MySQL 实例一个 shard,之后拆分成 shard1 和 shard2 两个分片,之后再添加新的 MySQL 实例,将 shard1 拆分成 shard11 和 shard12 两个分片,将 shard2 拆分成 shard21 和 shard22 两个分片放到另外新加的 MySQL 实例上,这种扩容方式是最理想的,但具体实现的时候会略微麻烦一点,我们短期之内选择了另一种偏保守一点、在合理预估前提下足以支撑业务发展的扩容模式,我们称之为 Pre-sharding 方案,这种方案是预先拆分在一定时期内足够用的分片数,在前期数据量较少时这些分片可以放在一个或少量的几个 MySQL 实例上,等后期数据量增大以后可以往集群中加新的 MySQL 实例,将原本的分片迁移到新添加的 MySQL 实例上,如图8所示,我们在一开始就拆分成了 shard1、 shard2、 shard3、 shard4 四个分片,这四个分片最初是在一个 MySQL 实例上,数据量增大以后我们可以添加新的 MySQL 实例,将 shard3 和 shard4 迁移新的 MySQL 实例上,整个集群分片数没有发生变化但是容量已经变成了原来的两倍。
<img src="http://ipad-cms.csdn.net/cms/attachment/201702/588856a8ddf45.png" alt="图7 Pre- sharding方案" title="图7 Pre- sharding方案" />
图7 Pre-sharding 方案
<img src="http://ipad-cms.csdn.net/cms/attachment/201702/588856d624392.png" alt="图8 Pre- sharding方案" title="图8 Pre- sharding方案" />
Pre-sharding方案
Pre-sharding方案相当于通过迁移实现扩容的目的,分片位置的变动涉及到数据的迁移验证及路由元数据的变更等一系列变动,所以我们引入了 JTransfer 系统,如图9所示。JTransfer 可以做到在线无缝迁移,迁移扩容时只需提交一条迁移计划,指定将某个分片从哪个源实例迁移到哪个目标实例,可以指定在何时开始迁移任务,等到了时间点系统会自动开始。整个过程中涉及到基础全量数据和迁移过程中业务访问产生的增量数据。一开始会将基础全量数据从源实例中 dump 出来到目标实例恢复,验证数据正确以后开始追赶增量数据,当增量数据追赶到一定程度,系统预估可以快速追赶结束时,我们会做一个短暂的锁定操作,从而确保将最后的增量全部追赶完成。这个锁定时间也是在提交迁移任务时可以指定的一个参数,比如最多只能锁定 20s。如果因为此时访问量突然增大等原因最终剩余的增量没能在 20s 内追赶完成,整个迁移任务将会放弃,确保对线上访问影响达到最小。迁移完成之后会将路由元信息进行修改,同时将路由元信息推送给所有的 JProxy,最后再解除锁定,访问将根据路由打到分片所在的新位置。
<img src="http://ipad-cms.csdn.net/cms/attachment/201702/5888574f8ab11.png" alt="图9 JProxy版本三" title="图9 JProxy版本三" />
图9 JProxy 版本三
系统在生产环境中使用的时候,除了考虑以上的介绍以外还需要考虑很多部署及运维的事情,首先要考虑的就是系统如何活下来,需要考虑系统的自我保护能力,要确保系统的稳定性,要做到性能能够满足业务需求。
在 JProxy 内部我们采用了基于事件驱动的网络 I/O 模型,同时考虑到多核等特点,将整个系统的性能发挥到极致,在压测时 JProxy 表现出来的性能随着 MySQL 实例的增加几乎是呈现线性增长的趋势,而且整个过程中 JProxy 所在机器毫无压力。
保证性能还不够,还需要考虑控制连接数、控制系统内存等,连接数主要是控制连接的数量。这个比较好理解,控制内存主要是指控制系统在使用过程中对内存的需求量,比如在做数据抽取时,SQL 语句是类似 select * from table 这种的全量查询,此时后端所有的 MySQL 数据会通过多条连接并发地往中间件发送数据,从中间件到应用只有一条连接,如果不对内存进行控制就会造成中间件 OOM。在具体实现的时候,我们通过将数据压在 TCP 栈中来控制中间件前后端连接的网络流速,从而很好的保证了整个系统的内存是在可控范围内。
另外还需要考虑权限,哪些 IP 可以访问,哪些 IP 不能访问都需要可以精确控制。具体到某一张表还需要控制增删改查的权限,我们建议业务在写 SQL 的时候尽量都带有拆分字段,保证 SQL 都可以落在某个分片上从而保证整个访问是足够的简单可控,我们为之提供了精细的权限控制,可以做到表级别的增删改查权限,包括是否要带有拆分字段,最大程度做到对 SQL 的控制,保证业务在测试阶段写出不满足期望的 SQL 都能及时发现,大大降低后期线上运行时的风险。
除了基本的稳定性之外,在整个系统全局上还需要考虑到服务高可用方案。JProxy 是无状态的,一个业务在同一个机房内部署至少两个 JProxy 且必须跨机架,保证在同一个机房里 JProxy 是高可用的。在另外的机房会再部署两个 JProxy,做到跨机房的高可用。除了中间件自身的高可用以外,还需要保证数据库层面的高可用,全链路的高可用才是真正的高可用。数据库层面在同一个机房里会按照一主一从部署,在备用机房会再部署一个备份,如图10所示。JProxy 访问 MySQL 时通过域名访问,如果 MySQL 的主出异常,数据库会进行相应的主从切换操作,JProxy可以访问到切换以后新的主。如果整个机房的数据库异常可以直接将数据的域名切换到备用机房,保证 JProxy 可以访问到备用机房的数据库。业务访问 JProxy 时也是通过域名访问,如果一个机房的 JProxy 都出现了异常,和数据库类似,直接将 JProxy 前端的域名切换到备用机房,从而保证业务始终都能正常访问 JProxy。
<img src="http://ipad-cms.csdn.net/cms/attachment/201702/58885791766c7.png" alt="图10 部署示意图" title="图10 部署示意图" />
图10 部署示意图
数据高可靠也是非常关键的点,我们会针对数据进行定期备份到相应的存储系统中,从而保证数据库中的数据即使被删除依然可以恢复。
系统在线上运行时监控报警极其重要。监控可以分多个层次,如图11所示,从主机和操作系统的信息到应用系统的信息到系统内部特定信息的监控等。针对操作系统及主机的监控,京东有 MJDOS 系统可以把系统的内存/CPU/磁/网卡/机器负载等各种信息都纳入监控系统,这些操作系统的基础信息对系统异常的诊断非常关键,比如因为网络丢包等引起的服务异常都可以在这个监控系统中及时找到根源。
<img src="http://ipad-cms.csdn.net/cms/attachment/201702/588857de58759.png" alt="图11 监控体系" title="图11 监控体系" />
图11 监控体系
京东还有统一的监控报警系统 UMP,这个监控系统主要是为所有的应用系统服务。所有的应用系统按照一定的规则暴露接口,在 UMP 系统中注册以后,UMP 系统就可以提供一整套监控报警服务,最基本的比如系统的存活监控以及是否有慢查询等。
除了这两个基本的监控系统以外,我们还针对整套中间件系统开发了定制的监控系统 JMonitor。之所以开发这套监控系统是因为我们需要采集更多的定制的监控信息,在系统发生异常时能够第一时间定位问题。举个例子,当业务发现 TP99 下降时往往伴随着有慢 SQL,应用从发送 SQL 到收到结果这个过程中经过了 JProxy 到 MySQL 又从 MySQL 经过 JProxy 再回到应用,这条链路上任何一个环节都可能慢,不管是哪个阶段耗时,我们需要将这种慢 SQL 的记录精细化,精细到各个阶段都花了多少时间,做到出现慢 SQL 时能快速准确的找到问题根源并快速解决问题。
另外在配合业务去 Oracle/SQL Server 时,我们不建议使用跨库的事务,但是会出现有一种情况,同一个事务里的 SQL 都是带有拆分字段的,每条 SQL 都是单节点的,同一个事务里有多条这种 SQL,结果却出现这个事务是跨库的,这种事务我们都会有详细的记录,业务方可以直接通过 JMonitor 找到这种事务从而更好的进一步改进。除了这个以外,业务系统最初写的 SQL 没有考虑太多的优化可能会出现比较多的慢 SQL,这些慢 SQL 我们都会统一采集在 JMonitor 系统上进行分析处理,帮助业务方快速迭代调整 SQL 语句。
业务在使用这套系统的时候要尽量出现避免跨库的 SQL,有一个很重要的原因是当出现跨库 SQL 时会耗费 MySQL 较多的连接,如图12所示。一条不带拆分字段的 SQL 将会发送到所有的分片上,如果在一个 MySQL 实例上有64个分片,那一条这样的 SQL 就会耗费这个 MySQL 实例上的64个连接,这个资源消耗是非常可观的,如果可以控制 SQL 落在单个分片上可以大大降低 MySQL 实例上的连接压力。
<img src="http://ipad-cms.csdn.net/cms/attachment/201702/5888581b04e54.png" alt="图12 连接数" title="图12 连接数" />
图12 连接数
跨库的分布式事务要尽量避免,一个是基于 MySQL 的分布式数据库中间件的方案无法保证严格的分布式事务语义,另一个即使可以做到严格的分布式事务语义支持依然要尽量避免垮库事务。多个跨库的分布式事务在某个分片上发生死锁将会造成其他分片上的事务也无法继续,从而导致大面积的死锁,即使是单节点上的事务也要尽量控制事务小一点,降低死锁发生的概率。
具体路由策略不同的业务可以特殊对待。以京东分拣中心为例,各个分拣中心的大小差异很大,北京上海等大城市的分拣中心数据量很大,其他城市的分拣中心相对会小一点,针对这种特点我们会给其定制路由策略,做到将大的分拣中心的数据落在特定的性能较好的 MySQL 实例上,其他小的分拣中心的数据可以按照普通的拆分方式处理。
在 JProxy 系统层面我们可以支持多租户模式,但考虑到去 Oracle/SQL Server 的业务往往都是非常重要且数据量巨大的业务,所以我们的系统都是不同的业务独立部署一套,在部署层面避免各个业务之间的互相影响。考虑到独立部署会造成一些资源浪费,我们引入了容器系统,将操作系统资源通过容器的方式进行隔离,从而保证系统资源的充分利用。很多问题没必要一定要在代码层面解决,代码层面解决起来比较麻烦或者不能做到百分之百把控的事情可以通过架构层面来解决,架构层面不好解决的事情可以通过部署的层面来解决,部署层面不好解决的事情可以通过产品层面来解决,解决问题的方式各式各样,需要从整个系统全局角度来综合考量,不管黑猫白猫,能抓老鼠的就是好猫,同样的道理,能支撑住业务发展的系统就是好系统。
另外再简单讨论一下为什么基于 MySQL 的分布式数据库中间件系统无法保证严格的分布式事务语义。所谓分布式事务语义本质上就是事务的语义,包含了 ACID 属性,分别是原子性、一致性、持久性、隔离性。
原子性是指一个事务要么成功要么失败,不能存在中间状态。持久性是指一个事务一旦提交成功那么要做到系统崩溃以后再恢复依然是成功的。隔离性是指各个并发事务之间是隔离的,不可见的,在数据库具体实现上可能会分很多个隔离级别。事务的一致性是指要保证系统要处于一个一致的状态,比如从 A 账户转了500元到 B 账户,那么从整体系统来看系统的总金额是没有发生变化的,不能出现 A 的账户已经减去500元但是 B 账户却没有增加500元的情况。
<img src="http://ipad-cms.csdn.net/cms/attachment/201702/58885869a3535.png" alt="图13 可串行化调度" title="图13 可串行化调度" />
图13 可串行化调度
事务在数据库系统中执行的时候有一个可串行化调度的问题。假设有 T1、T2、T3 三个事务,那么这三个事务的执行效果应该和三个事务串行执行效果一样,也就是最终效果应该是{T1/T2/T3, T1/T3/T2, T2/T1/T3, T2/T3/T1, T3/T1/T2, T3/T2/T1}集合中的一个。当涉及到分布式事务时,每个子事务之间的调度要和全局的分布式事务的调度顺序一致才能满足可串行化调度的要求,如图13所示,T1/T2/T3 的三个分布式事务,在一个库中的调度顺序是 T1/T2/T3 和全局的调度顺序一致,在另一个库中的调度顺序变成了 T3/T2/T1,此时站在全局的角度来看就打破了可串行化调度,可串行化调度保证了隔离性的实现,当可串行化调度被打破时自然隔离性也就随之打破。在基于 MySQL 的分布式中间件方案实现上,因为同一个分布式事务的各个子事务的事务 ID 是在各个 MySQL 上生成的,并没有提供全局的事务 ID 来保证各个子事务的调度顺序和全局的分布式事务一致,导致隔离性是无法保证的,所以说当前基于 MySQL 的分布式事务是无法保证严格的分布式事务语义支持的。当然随着 MySQL 引入 GR 可以做到 CAP 理论中的强一致,再加强中间件的相关功能及定制 MySQL 相关功能,也是有可能做到支持严格的分布式事务的。
\ No newline at end of file
## 大脑理论与智能机器探索者Jeff Hawkins专访
记者/卢鸫翔
“虽然没人确切知道恐龙是怎么灭绝的,与之相关的理论却很多——关于大脑则完全相反。”作为工程师,Jeff Hawkins 创立了两家便携式计算机公司,Palm 和 Handspring,开发了风靡一时的 PalmPilot 和 Treo 智能电话。然而作为科学家,理解大脑运作方式、原理,并按同样原理制造智能机器才是他一生的追求。日前,Jeff Hawkins 接受了《程序员》采访。
### 蜿蜒求索
1979年,从康奈尔大学工程学院毕业的 Jeff Hawkins 选择在 Intel 开启他的计算机行业生涯,然而三个月后,他就发现自己入错了行——那年9月出版的《科学美国人》是大脑研究专刊,专题最后一篇文章中,Francis Crick(DNA 结构发现人之一)写道,“尽管人们积累了大量有关大脑研究的详尽数据,但其工作原理仍是难解之谜。神经科学只是一堆没有任何理论的数据,最明显的是缺乏概念框架”。Crick 甚至没用“理论”这个词,他说,我们根本不知道怎么去想,因为连基本框架都没有——Crick 的话像号角,唤醒了 Hawkins 长久以来研究大脑,制造智能机器的梦想。
Hawkins 那些认为“大脑无法理解自身”的说法除了似有禅意,实则毫无用处,“人们常怀有根深蒂固但错误的假设,正是这种偏见阻止我们探寻答案。翻开科学史,你就会发现,哥白尼的天体运行说,达尔文的进化论和魏格纳的大陆漂移学说都跟大脑理论有诸多相似,都曾有许多无法解析的数据,而一旦拥有理论框架,一切就都变得有意义了。”
<img src="http://ipad-cms.csdn.net/cms/attachment/201703/58b3eba09e408.png" alt="图1 大多数科学理论与数据相互印证,而神经科学拥有海量数据却无框架和理论可用" title="图1 大多数科学理论与数据相互印证,而神经科学拥有海量数据却无框架和理论可用" />
图1 大多数科学理论与数据相互印证,而神经科学拥有海量数据却无框架和理论可用
然而将这一计划付诸实施尚需时日。在 Intel 展开研究自然是最便捷的渠道,Hawkins 致信当时的公司主席 Gordon Moore,建议成立研究小组,专攻大脑工作原理:“该工作可从一个人,即本人开始,随后进一步拓展。本人有信心承担该工作。相信有一天它会给我们带来无限商机。”不过随后的讨论中,公司并未支持他的想法,因为没人相信在可预见的未来能研究出大脑的工作原理。
此路不通,只得另辟蹊径。他首先向当时的人工智能研究“航母”MIT 人工智能研究院发出申请:
“我想设计和制作智能机器,但我的想法是先研究大脑怎么运作。”<br>
“你不需要这样做,我们只需要为计算机编程。”<br>
“不,应该先研究大脑。 ”<br>
“你错了。”<br>
“不,你们错了。”<br>
他们直截了当地告诉 Hawkins,要认识智力和建造机器,没必要研究真正的大脑,“他们认为研究大脑会限制思维,对大脑如何工作毫无兴趣。采取‘只求结果,不问手段’的方式开展研究,甚至有人还为自己跳开了生物学这一阶段而沾沾自喜。”MIT 拒绝了他的申请。
Hawkins 无所适从,但仍一心渴望研究大脑,他参加了人体生理学函授课程——因为函授学校不会拒绝任何人。他努力学习,准备考试,几年后被 UC Berkeley 接收为生物物理学研究生。欣喜若狂之余,意味着原本打算买房生子的计划搁浅,他需要甘心变成一个不能挣钱养家的人。
他原以为这次可以终于可以研究大脑理论了,但学校告诉他,他选择的研究方向,得不到经费。Hawkins 很沮丧,只能回到原点——他熟悉的计算机行业。“我计划干4年,挣点钱,组织自己的家庭,那时自己可能会成熟点,神经系统科学可能也会成熟点。结果比4年长多了,已经大概16年,但我终于做到了。”Hawkins 在这段时间创立了 Palm Computing(也许值得一提的是,Palm 商标目前归中国公司 TCL 所有)和 Handspring,推出了一系列风靡一时的掌上手写电脑。
Palm 使用的手写识别系统 Graffiti 灵感来自 Hawkins 曾学习的一种与大脑有关的数学——1987年夏天,一家名为 Nestor 的公司展示了一种能识别手写文字的神经网络,要价100万美元,“他们在神经网络规则上大作文章,将它吹嘘成一项重大突破,但我觉得手写识别问题可以通过另一种更简单、传统的方法解决。两天后,我设计出一种速度更快,体积更小,使用更灵活的手写识别器。”
### 生物神经网络
终于“挣到点钱”的 Hawkins 将自己的研究方向全面转向神经科学,2002年他建立了非盈利的科学研究机构 Redwood Center for Theoretical Neuroscience,2005年建立了 Numenta 继续他的研究。此前一年,他出版了 *On Intelligence*,向大众介绍大脑和智能理论。书中他提出了“记忆-预测”框架(Memory-prediction framework)——大脑的新皮质、海马体和丘脑联合匹配感官输入,存储记忆模式,并将这个过程如何用于预测。进而根据这一生物学框架发展出了 HTM(Hierarchical Temporal Memory)机器学习模型。Hawkins 将其称为“生物神经网络”(Biological Neural Network),与之对应的,他将 Deep Learning 为代表的神经网络称为“简单神经网络”(Simple Neural Network)。
<img src="http://ipad-cms.csdn.net/cms/attachment/201703/58b3ee7457f64.png" alt="图2 Graffiti手势" title="图2 Graffiti手势" />
图2 Graffiti 手势
“大脑以稀疏分布表示(sparse distributed representations,SDR)表征信息,我相信未来所有智能机器都将基于 SDR。而现有机器学习技术却无法将 SDR
加入其中,因为 SDR 是构建其他一切的基础。生物神经也远比‘简单神经网络’复杂得多。而作为生物神经网络的一种,HTM 已能从数据流中学习结构,做出预测和发现异常,还能从未标记的数据中连续学习。”Hawkins 这样解释“生物神经网络”的独特之处,他还觉得目前人工智能对认知功能被分割了——分为语音,视觉,自然语言等领域,而人脑是具有综合性的认知系统。目前的图像识别需要上千万张照片的收集归类,才能让机器“认出”猫,但人脑善于捕捉和认知流动的信息,也不需要大数据的支持。
表1 生物神经网络与其他技术区别
<img src="http://ipad-cms.csdn.net/cms/attachment/201703/58b3eea30dbbb.png" alt="表1 生物神经网络与其他技术区别" title="表1 生物神经网络与其他技术区别" />
Hawkins 认为,大多数神经网络和人工智能都有个共同缺陷——只注重行为。研究者们都认为智能存在于行为中——执行一个输入后,由另一个程序或神经网络产生行为。电脑程序和神经网络最重要的属性,就是能否进行正确的、令人满意的输出,将智能等同于行为。
而 Hawkins 则说:“智能并不是动作,也不是某种聪明的行为。行为只是智能的一种表现,绝不是智能的主要特征。‘思考’就是有力的证明:当你躺在黑暗中思考时,你就是智能的。如果忽略了头脑中的活动而只关心行为,将对理解智能和发明智能机器造成障碍。”
他认为“只求结果,不问手段”的功能主义解释会将人工智能研究者引入歧途,虽然人工智能的倡导者经常用会举出工程学上的解决方法与自然之道截然不同的例子——飞行器并非模仿鸟类扇动翅膀,轮子比猎豹更快。但他认为智能是大脑内部的特征,因此必须通过研究大脑内部来探究,“神经回路中一定潜藏着巨大的能量等待我们去发觉,而这种能量将超过任何现金的计算机”。
##### 《程序员》:你目前专注哪些研究,终极目标是什么?
Hawkins:我的终极兴趣是尽可能了解宇宙,理解大脑原理是其中的一部分。我相信,建立与大脑原理相同的智能机器将帮助我们发现宇宙的奥秘。
目前我完全专注于大脑新皮层的逆向工程。在 Numenta,我们试图理解大脑是如何对周围世界建模的,即人类智慧的本质是什么。了解大脑如何工作是最有趣的科学问题之一。了解大脑新皮层的原理也将帮助我们创造智能机器,这将对全人类大有裨益。
我们从两方面来解决大脑新皮层逆向工程的问题。一方面从理论出发,我们推断出大脑必须执行的一个或多个要求。另一方面来自经验,我们学习有关脑组织的解剖学和生理学全部知识。然后,我们试图来解决这两组约束。解决方案必须足够详细,这样才能用软件构造,并且进行生物测试。举个例子,我们知道大脑可以学习模式的序列,基于序列预测下一次事件。于是,我们仔细分析神经元的解剖结构和连接模式,探究它们是如何从序列中学习并作出预测的。我们用理论和实验预测证实了自己的理论。实际的过程比我说的复杂多了,但是基本原理就是这样。
##### 《程序员》:Richard Hamming 曾说:“若你对所做之事了如指掌,就不该做科学;而若相反,则不该做工程。” 你将自己视为工程师还是科学家?对于快速进入不同领域,你有哪些诀窍?
Hawkins:我职业生涯的早期阶段,主要身份是工程师,现在我主要的身份是科学家。但是每个方向的技能都有帮助。比如,在 Numenta,我们需要测试自己的理论来探究不同的脑回路的功能。尽管可用软件实现,但是快速实现仿真并验证预期效果需要大量的工程技术支持。
<img src="http://ipad-cms.csdn.net/cms/attachment/201703/58b3ef1899f26.png" alt="图3 Numenta目前拥有15位员工,大多数人有神经科学和机器学习背景" title="图3 Numenta目前拥有15位员工,大多数人有神经科学和机器学习背景" />
图3 Numenta 目前拥有15位员工,大多数人有神经科学和机器学习背景
当需要快速切入一个新领域时,我会一边精读相关资料,一边请教领域内的专家。不要害怕问问题,不管问题多么基础,要刨根问底。我非常享受这个过程。
##### 《程序员》:那么作为科学家,如何在科研和商业间找到平衡?
Hawkins:的确,很难同时兼顾科研目标和商业发展。我们的当务之急是完成科研使命,目前在脑科学领域取得了很好的进展,不想分散注意力影响科研。从商业的角度来看,我们已将知识产权授权给了其他人。今后,也许会往商业化方向投入更多资源,但科研优先级仍旧是第一位。
##### 《程序员》:哪些书对你影响最大,为什么?
Hawkins:我喜欢阅读那些克服障碍去完成伟大事情的故事。最近读的几本这类书包括 David McCullough 的《莱特兄弟》,Jennet Conant 写的《燕尾服的公园:华尔街的大亨和改变二战的秘密科学宫》,Ron Chernow 的《亚历山大·汉密尔顿》。任何努力的成功都在于克服永无止境的一系列障碍。我觉得学习别人面对挑战和坚持的方式颇受鼓舞。
##### 《程序员》:Roy Amara 说,“我们倾向于高估科技的短期影响力,而又低估其长期影响力。”在你看来,对于 AI 的前景,人们是否过于乐观?
Hawkins:我很喜欢这句话,无论是在我从事移动计算,还是做 AI 研究,一直铭记于心。我担心人们夸大了人工智能的发展速度。纵使现在的 AI 技术看上去很强大,但距离真正创建智能机器还有许多事情要做。在智能机器真正腾飞前,可能还会经历一个失望的低谷。
##### 《程序员》:《On Intelligence》已出版十年,你有哪些新发现,如果有机会重写,会有何不同?
Hawkins:在智能领域,有些事情我想去改变,但更多的是想添砖加瓦。自从这本书问世以来,我们理解了概念背后的神经机制,更重要的是,我们发现了几个重要的新原理。
我正在考虑写一本关于大脑和人工智能的新书。我想谈谈很多关于智能机器的误解。另外,我还想说明为什么真正的智能机器是人类长期生存所必需的。
##### 《程序员》:当今的硬件架构是否是实现智能机器的最好选择?还存在哪些限制?
Hawkins:目前的计算机体系结构和半导体器件不是真正智能机的理想选择。智能机器需要大量的分布式内存,但幸运的是智能系统能够容忍许多故障。而学习主要是,通过重新连线(re-wiring)来实现的。实现这些特性的最佳方法仍在争论中。
##### 《程序员》:在你看来,对人脑机制理解的缺乏是我们开发智能机器的最大限制之一,在这个存在许多假设和未知的前沿领域进行研究,怎样判断自己研究的方向和做出的各种选择是否正确?
Hawkins:面临的挑战主要来自确定大脑的哪部分是信息处理必不可少的,哪部分又是生物生存依赖的。确定这一点的方法是首先发展一个全面的脑功能理论。这是一个反复更迭的过程,但在缺乏理论的情况下不可能完成。
##### 《程序员》:HTM 完备了吗,难点在哪儿?
Hawkins:HTM 离完成还有很远!我们知道还有一系列的内容必须加入 HTM 理论,正在逐个解决这些问题。现在正在研究的大问题是如何使用行为来学习。移动身体、手和眼睛是学习的最重要机制。很少有人工智能系统尝试这种方法。去年,在这方面我们有一个重大发现,现在正在测试阶段。
##### 《程序员》:除了生物神经网络,要制造智能系统,还需要哪些技术?
Hawkins:每个真正的智能系统需要某种形式的体现。这包括一组传感器和移动这些传感器的手段。传感器可以与我们在生物学中看到的任何东西都不同,这个化身可以是虚拟的,比如在万维网上“移动”。传感器的形式在基础理论之外可以有千变万化的形式。
##### 《程序员》:生物神经网络与 ANNs/Deep Learning 最大的不同是什么?
Hawkins:真正的智能系统通过运动和操作来建立世界的模型。大脑新皮层随着感官数据的变化建立一个真实环境的模型。这就解释了为什么大脑学习比 ANNs 学会更丰富的模型,为什么大脑学习新事物的速度要快得多。总的来说,我相信随着时间的推移 AI 研究和大脑理论会变得更紧密。
##### 《程序员》:你肯定也听到过同行对基于人脑理论研究 AI 方式的质疑,例如 Yann LeCun 曾说这些理论实例化的难度被严重低估,缺乏数学支撑,也缺少像 MNIST 或 ImageNet 这样的客观检验。
Hawkins:我们不该忘记,AI 已经包含了大脑的原理,如分布式编码与 Hebbian 学习,这些想法已经存在了很长时间。近年来,我的团队在大脑理论方面取得了重大进展。我们的压力在于要证明它们是相关的,这是一项有挑战的任务。
#####《程序员》:有些人担忧智能机器在未来会对人类构成威胁,你怎么看?
Hawkins:我不同意这些担忧,这些想法基于三个错觉。
错觉1:智能机器将掌握自我复制能力。末日场景常描绘机器智能拥有了人类无法控制的自我复制能力。但自我复制和创造智能完全是两码事。将智能赋予那些已经具备自我复制能力的东西将造成糟糕的后果,但是智能本身不会倾向于去自我复制,除非你相信第二个错觉。
错觉2:智能机器将拥有人的欲望。大脑皮质是一个学习系统,但它不具有情绪。大脑的其他部分,如脊髓、脑干和基底神经节这些古老的大脑构造才是负责诸如比饥饿、愤怒、性欲和贪婪这些本能和情绪的。或许有人会试图建造具有欲望和情绪的机器,但这和建造智能机器是两回事。
错觉3:机器将导致智能爆炸。智能是学习的产品,对人类来说,这是个经年累月的缓慢过程。智能机器的区别仅是,它们可以通过复制和传输来获得新知识,减少学习时间。但当它去探索新原理,学习新技能时会遇到与我们一样的困难。对大多数问题,同样需要设计实验、收集数据、预测结果,修正并不断重复这些过程。如果想去探索宇宙,依然需要靠望远镜和星际探测器去太空采集数据。如果它想搞清楚气候变化,依然需要去南极取个冰核样本或者去海里部署测量仪器。
我们对威胁的反应该基于这种威胁离我们有多远。比如说地球将在1.5亿年后因为太阳变热而无法居住。几乎没人因为这个问题感到恐惧——实在太遥远。机器智能的进化过程中存在某些危险趋势,但这些危险有近有远。目前尚未发现已知的威胁。而对于遥远的未来,我们也能容易地改变那些可能会出现的问题。
\ No newline at end of file
## 如何成为一名推荐系统工程师
### 推荐系统工程师成长路线图
![enter image description here](http://images.gitbook.cn/b2626940-c493-11e7-a68e-3d5e4f9f8dae)
《Item-based collaborative filtering recommendation algorithms》这篇文章发表于2001年,在 Google 学术上显示,其被引用次数已经是6599了,可见其给推荐系统带来的影响之大。经过20多年的发展,item-based 已经成为推荐系统的标配,而推荐系统已经成为互联网产品的标配。很多产品甚至在第一版就要被投资人或者创始人要求必须“个性化”,可见,推荐系统已经飞入寻常百姓家,作为推荐系统工程师的成长也要比从前更容易,要知道我刚工作时,即使跟同为研发工程师的其他人如 PHP 工程师(绝无黑的意思,是真的)说“我是做推荐的”,他们也一脸茫然,不知道“推荐”为什么是一个工程师岗位。如今纵然“大数据”,“AI”,这些词每天360度无死角轰炸我们,让我们很容易浮躁异常焦虑不堪,但不得不承认,这是作为推荐系统工程师的一个好时代。
推荐系统工程师和正常码农们相比,无需把 PM 们扔过来的需求给像素级实现,从而堆码成山;和机器学习研究员相比,又无需沉迷数学推导,憋出一个漂亮自洽的模型,一统学术界的争论;和数据分析师相比,也不需绘制漂亮的图表,做出酷炫的 PPT 能给 CEO 汇报,走上人生巅峰。那推荐系统工程师的定位是什么呢?为什么需要前面提到的那些技能呢?容我结合自身经历来一一解答。
我把推荐系统工程师的技能分为四个维度:
1. 掌握核心原理的技能,是一种知其所以然的基础技能;
2. 动手能力:实现系统,检验想法,都需要扎实的工程能力;
3. 为效果负责的能力:这是推荐系统工程师和其他工种的最大区别;
4. 软技能:任何工程师都需要自我成长,需要团队协作。
- 英文阅读:读顶级会议的论文、一流公司和行业前辈的经典论文和技术博客,在
Quora 和 Stack Overflow 上和人交流探讨;
- 代码阅读:能阅读开源代码,从中学习优秀项目对经典算法的实现;
- 沟通表达:能够和其他岗位的人员沟通交流,讲明白所负责模块的原理和方法,能听懂非技术人员的要求和思维,能分别真伪需求并且能达成一致。
#### 掌握最最基础的原理
托开源的福气,现在有很多开箱即用的工具让我们很容易搭建起一个推荐系统。但是浮沙上面筑不起高塔,基础知识必须要有,否则就会在行业里面,被一轮轮概念旋风吹得找不着北。所有基础里面,最最基础的当然就是数学了。
能够看懂一些经典论文对于实现系统非常有帮助:从基本假设到形式化定义,从推导到算法流程,从实验设计到结果分析。这些要求我们对于微积分有基本的知识,有了基本的微积分知识才能看懂梯度下降等基本的优化方法。概率和统计知识给我们建立起一个推荐系统工程师最基本的三观:不要以是非绝对的眼光看待事物,要有用不确定性思维去思考产品中的每一个事件,因为实现推荐系统,并不是像实现界面上一个按钮的响应事件那样明确可检验。
大数据构建了一个高维的数据空间,从数据到推荐目标基本上都可以用矩阵的角度去形式化,比如常见的推荐算法:协同过滤、矩阵分解。而机器学习算法,如果用矩阵运算角度去看,会让我们更加能够理解“向量化计算”和传统软件工程里面的循环之间的巨大差异。高维向量之间的点积,矩阵之间的运算,如果用向量化方式实现比用循环方式实现,高效不少。建立这样的思维模式,也需要学好线性代数。
学好基础的数学知识之外,我们要稍微延伸学习一些信息科学的基础学科,尤其是信息论。信息论是构建在概率基础上的,信息论给了很多计算机领域问题一个基本的框架:把问题看做是通信问题。推荐系统要解决的问题也是一个通信问题:用户在以很不明确的方式向我们的产品发报,告诉我们他最喜欢/讨厌的是什么,我们在收到了之后要解码,并且还要给他们回信,如果沟通不顺畅,那用户就会失联。我的专业是信息与通信工程。读研时从事过
NLP 相关的课题研究,NLP 里面很多问题和方法都用到了信息论知识,这样让我深受信息论影响。有了这些基础知识,再去跟踪不断涌现的新算法新模型,就会容易得多。
推荐系统会用到很多传统数据挖掘和机器学习方法。掌握经典的机器学习算法是一个事半功倍的事情,比如逻辑回归,是一个很简单的分类算法,但它在推荐领域应用之广,其他算法无出其右。在吴恩达的深度学习课程里,从逻辑回归入手逐渐讲到多层神经网络,讲到更复杂的 RNN 等。应该怎么掌握这些经典的算法呢?最直接的办法是:自己从0实现一遍。
推荐系统不只是模型,推荐系统是一整个数据处理流程,所以模型的上游,就是一些数据挖掘的知识也需要掌握,基本的分类聚类知识,降维知识,都要有所掌握。
#### 锻炼扎实的工程能力
前面强调自己实现算法对于掌握算法的必要性,但在实际开发推荐系统的时候,如无必要,一定不要重复造轮子。推荐系统也是一个软件系统,当然要稳定要高效。开源成熟的轮子当然是首选。实现推荐系统,有一些东西是 commonsense,有一些是好用的工具,都有必要列出来。
首当其冲的常识就是 Linux 操作系统。由于 Windows 在 PC 的市场占率的垄断地位,导致很多软件工程师只会在 Windows 下开发,这是一个非常普遍、严重、又容易被忽视的短板。
我自己深有体会,一定要熟练地在 Linux 下的用命令行编程,如果你的个人电脑是 Mac,会好很多,因为 macOS 底层是 Unix 操作系统,和 Linux 是近亲,用 Mac 的终端基本上类似在 Linux 下的命令行,如果不是则一定要有自己的 Linux 环境供自己平时练习,买一台常备的云服务器是一个不错的选择。这里有两个关键点:
- 用 Linux 操作系统;
- 多用命令行而少用 IDE( Eclipse、VS等)。
为什么呢?有以下三点原因:
- 几乎所有推荐系统要用到的开源工具都是首先在 Linux 下开发测试完成的,最后再考虑移植到 Windows 平台上(测试不充分或者根本不移植);
- 键盘比鼠标快,用命令行编程会多用键盘,少用鼠标,熟悉之后效率大大提升。而且
Linux 下的命令非常丰富,处理的也都是标准文本,掌握之后很多时候根本不用写程序就能做很多数据处理工作。
- 几乎 Linux 是互联网公司的服务器操作系统标配,不会 Linux 下的开发,就找不着工作,就问你怕不怕?
常常有人问我,实现推荐系统用什么编程语言比较好。标准的官方回答是:用你擅长的语言。但我深知这个回答不会解决提问者的疑问。实际上我的建议是:你需要掌握一门编译型语言:C++ 或者 Java,然后掌握一门解释型语言,推荐 Python 或者 R。原因如下:
- 推荐系统的开源项目中以这几种语言最常见;
- 快速的数据分析和处理、模型调试、结果可视化、系统原型实现等,Python 和 R 是不错的选择,尤其是 Python;
- 当Python在一些地方有效率瓶颈时,通常是用C++实现,再用Python调用;
- Java在构建后台服务时很有优势,一些大数据开源项目也多用Java来实现;
如果时间有限,只想掌握一门语言的话,推荐 Python。从模型到后端服务到 web 端,都可以用 Python,毋庸置疑,Python 是 AI 时代第一编程语言。
推荐系统是一个线上的产品,无论离线时的模型跑得多么爽,可视化多么酷炫,最终一定要做成在线服务才完整。这就涉及到两方面的工作:1.系统原型;2.算法服务化。这涉及到:
- 数据存储。包括存储模型用于在线实时计算,存储离线计算好的推荐结果。除了传统的关系型数据库 MySQL 之外,还需要掌握非关系型数据库,如 KV 数据库 Redis,列式数据库 Cassandra 和 HBase 常常用来存储推荐结果或模型参数。推荐的候选 Item 也可能存在 MongoDB 中。
- RPC 和 web。需要将自己的算法计算模块以服务的形式提供给别人跨进程跨服务器调用,因此 RPC 框架就很重要,最流行如 thrift 或者 dubbo。在 RPC 服务之上,再做原型还需要会一点基本的 web 开发知识,Python、PHP、Java 都有相应的 web 框架来迅速的完成最基本的推荐结果展示。
当然,最核心的是算法实现。以机器学习算法为主。下面详细列举一下常见的机器学习/深度学习工具:
- Spark MLib:大概是使用最广的机器学习工具了,因为 Spark 普及很广,带动了一个并非其最核心功能的 MLib,MLib 实现了常见的线性模型、树模型和矩阵分解模型等。提供 Scala、Java 和 Python 接口,提供了很多例子,学习 Spark MLib 很值得自己运行它提供的例子,结合文档和源代码学习接口的使用,模型的序列化和反序列化。
- GraphLab/GraphCHI:GraphCHI 是开源的单机版,GraphLab 是分布式的,但并不开源。所以建议推荐系统工程师重点学习一下 GraphCHI,它有 Java 和 C++两个版本,实现了常见的推荐算法,并在单机上能跑出很高的结果。有一个不得不承认的事实是:GraphCHI 和 GraphLab 在业界应用得并不广泛。
- Angel:腾讯在2017年开源的分布式机器学习平台,Java 和 Scala 开发而成,已经在腾讯的10亿维度下有工业级别的应用,最终的是填补了专注传统机器学习(相对于深度学习)分布式计算的空白,值得去学习一下;由于开发团队是中国人,所以文档以中文为主,学习的时候多多和开发团队交流会受益良多,进步神速。
- VW:这是 Yahoo 开源的一个分布式机器学工具,也支持单机,分布式需要借助
Hadoop 实现。由于主要开发者后来跳槽去了微软,所以还支持 Windows 平台。阅读这个工具的源码,非常有助于理解逻辑回归的训练,微博推荐团队和广告团队第一版模型训练都采用了 VW,其开发者在 Yahoo Group 中回答问题很积极,使用期间,我在这个
group 里面提了大大小小十几个问题,基本上都得到解答,这是一个学习成长方法,建议新学者常常在邮件组或者讨论组里提问题,不要在乎问题是否愚蠢,不要在意别人的取笑。
- Xgboost:这个号称 kaggle 神器的机器学习工具,非常值得学习和使用,尤其是对于理解 Boosting 和树模型很有帮助。网上有很多教程,主要开发者陈天奇也是中国人,所以遇到问题是非常容易找到交流的人的。
- libxxx:这里的xxx是一个通配符,包括以 lib 开头的各种机器学习工具,如liblinear、libsvm、libfm、libmf。都是单机版的工具,虽然是单机版,但足够解决很多中小型数据集的推荐问题了,著名的 scikit-learn 中的一些分类算法就是封装的libsvm 等工具。另外,libsvm 不但是一个机器学习工具,而且它还定义了一种应用广泛,成为事实标准的机器学习训练数据格式:libsvm。
- MXNet,TensorFlow,Caffe:深度学习大行其道,并且在识别问题上取到了惊人的效果,自然也间接推动了推荐系统的算法升级,因此,掌握深度学习工具的就很必要,其中尤其以 TensorFlow 为主,它不但有深度学习模型的实现,还有传统机器学习模型的实现,Python 接口,对于掌握 Python 的人来说学习门槛很低。深度学习工具仍然建议去跑几个例子,玩一些有趣的东西会快速入门,如给照片换风格,或者训练一个动物/人脸识别器,可以有一些粗浅的认识。再系统地学习一下吴恩达的在线课程,他的课程对TensorFlow 的使用也有讲解,课后编程作业设计得也很好。
#### 为最终效果负责的能力
推荐系统最终要为产品效果负责。衡量推荐系统效果,分为离线和在线两个阶段。
- 离线阶段。跑出一些模型,会有定义清晰的指标去衡量模型本身对假设的验证情况,如准确率、召回率、AUC 等。这个阶段的效果好,只能说明符合预期假设,但不能保证符合产品最终效果,因此还要有线上实际的检验。
- 在线阶段:除了有一些相对通用的指标,如用户留存率、使用时长、点击率等,更多的是和产品本身的定位息息相关,如短视频推荐关注 vv,新闻推荐关注 CTR 等,这些和商业利益结合更紧密的指标才是最终检验推荐系统效果的指标,推荐系统工程师要为这个负责,而不能仅仅盯着离线部分和技术层面的效果。
了解不同产品的展现形式对推荐系统实现的要求,feed 流、相关推荐、猜你喜欢等不同产品背后技术要求不同,效果考核不同,多观察、多使用、多思考。
最后,要学会用产品语言理解产品本身,将技术能力作为一种服务输出给团队其他成员是一项软技能。
### 推荐系统领域现状
协同过滤提出于90年代,至今二十几年,推荐系统技术上先后采用过近邻推荐、基于内容的推荐,以矩阵分解为代表的机器学习方法推荐,最近几年深度学习的火热自然也给推荐系统带来了明显的提升。推荐系统的作用无人质疑,简单举几个例子,80%的 Netflix 电影都是经由推荐系统被观众观看的,YouTube 上60%的点击事件是由推荐系统贡献的。
推荐系统领域现状是怎么样的呢?这里分别从技术上和产品上来看一看。先看技术上,推荐系统所依赖的技术分为三类:传统的推荐技术、深度学习、强化学习。
首先,传统的推荐技术仍然非常有效。构建第一版推荐系统仍然需要这些传统推荐系统技术,这包括:User-based 和 Item-based 近邻方法,以文本为主要特征来源的基于内容推荐,以矩阵分解为代表的传统机器学习算法。当一个互联网产品的用户行为数据积累到一定程度,我们用这些传统推荐算法来构建第一版推荐系统,大概率上会取得不俗的成绩,实现0的突破。这类传统的推荐算法已经积累了足够多的实践经验和开源实现。由于对推荐系统的需求比以往更广泛,并且这些技术足够成熟,所以这类技术有 SaaS 化的趋势,逐渐交给专门的第三方公司来做,中小型、垂直公司不会自建团队来完成。
深度学习在识别问题上取得了不俗的成绩,自然就被推荐系统工程师们盯上了,已经结合到推荐系统中,比如 YouTube 用 DNN 构建了他们的视频推荐系统,Google 在Google Play 中使用 Wide & Deep 模型,结合了浅层的 logisticregression 模型和深层模型进行 CTR 预估,取得了比单用浅层模型或者单独的深层模型更好的效果,Wide & Deep 模型也以开源的方式集成在了 TensorFlow 中,如今很多互联网公司,都在广泛使用这一深度学习和浅层模型结合的模型。在2014年,Spotify 就尝试了 RNN 在序列推荐上,后来 RNN 又被 Yahoo News 的推荐系统。传统推荐算法中有一个经典的算法叫做 FM,常用于做 CTR 预估,算是一种浅层模型,最近也有人尝试了结合深度学习,提出 DeepFM 模型用于 CTR 预估。
AlphaGo、AlphaMaster、AlphaZero一个比一个厉害,其开挂的对弈能力,让强化学习进入大众视线。强化学习用于推荐系统是一件很自然的事情,把用户看做变化的环境,而推荐系统是 Agent,在和用户的不断交互之间,推荐系统就从一脸懵逼到逐渐“找到北”,迎合了用户兴趣。业界已有应用案例,阿里的研究员仁基就公开分享过淘宝把强化学习应用在搜索推荐上的效果。强化学习还以 bandit 算法这种相对简单的形式应用在推荐系统很多地方,解决新用户和新物品的冷启动,以及取代 ABTest 成为另一种在线实验的框架。
除了技术上推荐系统有不同侧重,产品形式上也有不同的呈现。最初的推荐系统产品总是存活在产品的边角上,如相关推荐,这种产品形式只能算是“锦上添花”,如果推荐系统不小心开了天窗,也不是性命攸关的问题。如今推荐产品已经演化成互联网产品的主要承载形式:信息流。从最早的社交网站动态,到图文信息流,到如今的短视频。信息流是一种推荐系统产品形式,和相关推荐形式比起来,不再是锦上添花,而是注意力收割利器。
推荐系统产品形式的演进,背景是互联网从 PC 到移动的演进,PC 上是搜索为王,移动下是推荐为王,自然越来越重要。随着各种可穿戴设备的丰富,越来越多的推荐产品还会涌现出来。产品和技术相互协同发展,未来会有更多有意思的推荐算法和产品形式问世,成为一名推荐系统工程师永远都不晚。
##### 文 / 陈开江
>希为科技 CTO,曾任新浪微博资深算法工程师,考拉FM算法主管,个性化导购App《Wave》和《边逛边聊》联合创始人,多年推荐系统从业经历,在算法、架构、产品方面均有丰富的实践经验。
## 微信分布式数据存储协议对比:Paxos 和 Quorum
文 / 莫晓东
分布式系统是网络化的计算机系统,海量数据的互联网应用只能通过分布式系统协调大量计算机来支撑。微信后台存储大量使用了分布式数据存储方式的 NoSQL 集群,比如核心业务:账号、支付单据、关系链、朋友圈等。存储设备出现异常是必然,分布式系统通过多节点分布及冗余,避免个别异常节点影响到系统的正常服务,同时提供平行扩展能力。微信自研的分布式存储在发展的不同阶段,分别依赖过 Paxos 和 Quorum 两种方案维护一致性。Paxos 和 Quorum 也是互联网企业主要使用的分布式协议,这里向有兴趣的读者做些分布式算法的粗略介绍,以及为什么需要它们。
### 关于一致性
为什么需要 Paxos 或 Quorum 算法?分布式系统实现数据存储,是通过多份数据副本来保证可靠,假设部分节点访问数据失败,还有其他节点提供一致的数据返回给用户。对数据存储而言,怎样保证副本数据的一致性当属分布式存储最重要的问题。 一致性是分布式理论中的根本性问题,近半个世纪以来,科学家们围绕着一致性问题提出了很多理论模型,依据这些理论模型,业界也出现了很多工程实践投影。何为一致性问题?简而言之,一致性问题就是相互独立的节点之间,在可控的时间范围内如何达成一项决议的问题。
### 强一致写、多段式提交
#### 强一致写
解决这个问题最简单的方法 ,就是强一致写。在用户提交写请求后,完成所有副本更新再返回用户,读请求任意选择某个节点。数据修改少节点少时,方案看起来很好,但操作频繁则有写操作延时问题,也无法处理节点宕机。
#### 两段式提交(2PC 、Two-Phase Commit)
既然实际系统中很难保证强一致,便只能通过两段式提交分成两个阶段,先由Proposer(提议者)发起事物并收集 Acceptor(接受者)的返回,再根据反馈决定提交或中止事务。
- 第一阶段:Proposer 发起一个提议,询问所有 Acceptor 是否接受。
- 第二阶段:Proposer 根据 Acceptor的返回结果,提交或中止事务。如果 Acceptor 全部同意则提交,否则全部终止。
两阶段提交方案是实现分布式事务的关键;但是这个方案针对无反馈的情况,除了“死等”,缺乏合理的解决方案。 Proposer 在发起提议后宕机,阶段二的 Acceptor 资源将锁定死等。如果部分参与者接受请求后异常,还可能存在数据不一致的脑裂问题。
#### 三段式提交(3PC、Three-Phase Commit)
为了解决 2PC 的死等问题,3PC 在提交前增加一次准备提交(prepare commit)的阶段,使得系统不会因为提议者宕机不知所措。接受者接到准备提交指令后可以锁资源,但要求相关操作必须可回滚。
但 3PC 并没有被用在我们的工程实现上,因为3PC无法避免脑裂,同时有其他协议可以做到更多的特性又解决了死等的问题。
![enter image description here](http://images.gitbook.cn/0d1179b0-ff38-11e7-a772-21bfb93cfbfb)
图1 三段式提交,在二段式提交基础上增加 prepare commit 阶段
### 主流的 Paxos 算法
微信后台近期开始主要推广 Paxos 算法用于内部分布式存储。Paxos 是 Leslie Lamport 提出的基于消息传递的一致性算法,解决了分布式存储中多个副本响应读写请求的一致性,Paxos 在目前的分布式领域几乎是一致性的代名词(据传 Google Chubby 的作者Mike Burrows 曾说过这个世界上只有一种一致性算法, 那就是 Paxos,其他算法都是残次品)。Paxos 算法在可能宕机或网络异常的分布式环境中,快速且正确地在集群内部对某个数据的值达成一致,并且保证只要任意多数节点存活,都不会破坏整个系统的一致性。Paxos 的核心能力就是多个节点确认一个值,少数服从多数,获得可用性和一致性的均衡。
![enter image description here](http://images.gitbook.cn/132f5510-ff38-11e7-a772-21bfb93cfbfb)
图2 Paxos 模型,节点间的交互关系
Paxos 可以说是多节点交互的二段提交算法,Basic Paxos 内的角色有 Proposer(提议者)、Acceptor(接受提议者)、Learner(学习提议者),以提出
Proposal(提议)的方式寻求确定一致的值。
- 第一阶段(Prepare):Proposer 对所有 Acceptor 广播自己的 Proposal(值+编号)。Acceptor 如果收到的 Proposal 编号是最大的就接受,否则 Acceptor 必须拒绝。如果 Proposer 之前已经接受过某个 Proposal,就把这个 Proposal 返回给 Proposer。在 Prepare 阶段 Acceptor 始终接受编号最大的 Proposal,多个
Proposer 为了尽快达成一致,收到 Acceptor 返回的 Proposal 编号比自己的大,就修改为自己的 Proposal。因此为了唯一标识每个 Proposal,编号必须唯一。如果 Proposer 收到过半数的 Acceptor 返回的结果是接受,算法进入第二阶段。
- 第二阶段(Accept):Proposer 收到的答复中,如果过半数的 Acceptor 已经接受,Proposer 把第一阶段的 Proposal 广播给所有 Acceptor。而大多 Acceptor
已经接受了其他编号更大的 Proposal 时,Proposer 把这个 Proposal 作为自己的
Proposal 提交。Acceptor 接到请求后,如果 Proposal 编号最大则确认并返回结果给所有 Proposer,如果 Proposer 得到多数派回复,则认为最终一致的值已经确定(Chosen)。Learner 不参与提议,完成后学习这个最终 Proposal。
严格证明是通过数学归纳法,本文只做了直观判断。Paxos 确认这个值利用的是“抽屉原理”,固定数量的节点选取任意两次过半数的节点集合,两次集合交集必定有节点是重复的。所以第一阶段任何已经接受的提议,在第二阶段任意节点宕机或失联,都有某节点已经接受提议,而编号最大的提议和确定的值是一致的。递增的编号还能减少消息交互次数,允许消息乱序的情况下正常运行。就一个值达成一致的方式(Basic Paxos)已经明确了,但实际环境中并不是达成一次一致,而是持续寻求一致,读者可以自己思考和推导,想深入研究建议阅读 Leslie Lamport 的三篇论文_Paxos made simple_、_The Part-Time Parliament_、_Fast Paxos_。实现多值方式(原文为Multi Paxos),通过增加 Leader 角色统一发起提议 Proposal,还能节约多次网络交互的消耗。Paxos 协议本身不复杂,难点在如何将 Paxos 协议工程化。
我们实现 Paxos 存储做了一些改进,使用了无租约版 Paxos 分布式协议,参考 Google MegaStore 做了写优化,并通过限制单次 Paxos 写触发 Prepare 的次数避免活锁问题。虽然 Paxos 算法下只要多数派存在,就可以在分布式环境下达到严格的一致性。但是牺牲的性能代价可观,在大部分应用场景中,对一致性的要求并不是那么严格,这个时候有不少简化的一致性算法,比如 Quorum。
### 简化的 Quorum(NWR)算法
Quorum 借鉴了 Paxos 的思想,实现上更加简洁,同样解决了在多个节点并发写入时的数据一致性问题。比如 Amazon 的 Dynamo 云存储系统中,就应用 NWR 来控制一致性。微信也有大量分布式存储使用这个协议保证一致性。Quorum最初的思路来自“鸽巢原理”,同一份数据虽然在多个节点拥有多份副本,但是同一时刻这些副本只能用于读或者只能用于写。
![enter image description here](http://images.gitbook.cn/19b60460-ff38-11e7-a772-21bfb93cfbfb)
图3 Quorum 模型:微信改进的版本、数据分离结构
Quorum 控制同一份数据不会同时读写,写请求需要的副本数要求超过半数,写操作时就没有足够的副本给读操作;
Quorum 控制同一份数据的串行化修改,因为副本数要求,同一份数据不会被两个写请求同时修改。
Quorum 又被称为 NWR 协议:R 表示读取副本的数量;W 表示写入副本的数量;N 表示总的节点数量。
假设 N=2,R=1,W=1,R+W=N=2,在节点1写入,节点2读取,无法得到一致性的数据;
假设 N=2,R=2,W=1,R+W>N,任意写入某个节点,则必须同时读取所有节点;
假设 N=2,W=2,R=1,R+W>N,同时写入所有节点,则读取任意节点就可以得到结果。
要满足一致性,必须满足 R+W>N。NWR 值的不同组合有不同效果,当 W+R>N 时能实现强一致性。所以工程实现上需要 N>=3,因为冗余数据是保证可靠性的手段,如果 N=2,损失一个节点就退化为单节点。写操作必须更新所有副本数据才能操作完成,对于写频繁的系统,少数节点被写入的数据副本可以异步同步,但是只更新部分节点,读取则需要访问多个节点,读写总和超过总节点数才能保证读到最新数据。可以根据请求类型调整 BWR,需要可靠性则加大 NR,需要平衡读写性能则调整 RW。
微信有大量分布式存储(QuorumKV)使用这个算法保证一致性,我们对这个算法做了改进,创造性地把数据副本分离出版本编号和数据存到不同设备,其中 N=3(数据只有2份,版本编号有3份),在 R=W=2 时仍然可以保证强一致性。因为版本编号存放3份,对版本编号使用 Quorum 方式,通过版本编号协商,只有版本序号达成一致的情况下读写单机数据,从而在保证强一致性的同时实现高读写性能。实际数据只写入一台数据节点,使用流水日志的方式进行同步,并更新版本编号。但是我们的分布式存储(QuorumKV)仍存在数据可靠性比 Paxos 低的问题,因为数据只写一份副本,依靠异步同步。如果数据节点故障,故障节点上没有同步到另一个节点,数据将无法访问。版本节点故障时,如果 Quorum 协议没有设置 W=3,也可能无法访问正确的数据节点副本。
### 后记
分布式存储选用不同的一致性算法,和业务的具体情况相关。我们的分布式存储在发展的不同阶段,使用过不同的算法:业务的发展初期使用 Quorum 算法,成本压力减少而业务稳定需求变大后,就开始使用 Paxos 算法。如果业务模型对数据一致性要求不高,使用
Quorum 则具有一定的成本和开发资源优势。
\ No newline at end of file
## 微信小程序的编程模式
>“轻芒小程序+”是由轻芒团队提出的小程序解决方案,它将替内容创业者免费搭建属于自己的微信小程序。在进行“轻芒小程序+”和其他小程序应用开发的过程中,本文作者与其团队对当前正火热的小程序开发有了更为深度的理解与认识,进而有了本文。
从小程序诞生伊始,就有很多人开始研习其机理与特点,从源代码或整体架构的角度已经有很多不错的文章会令人受益。但理论是一回事,真正理解小程序,还需要实践,才能进一步理解其背后的想法,与已有平台的异同,以及如何去适应它,做出更有趣的小程序。
理解开发平台的特性,一个不错的角度就是从编程模式入手,看在这个平台上开发,需要如何书写和组织自己的代码,进而搞清楚三个问题:
1. 数据如何获取;
2. 界面如何呈现;
3. 交互如何传导。
换而言之,就是从 MVC(Model-View-Controller)的视角去拆解这个平台的特性,从而理解其开发有何特点。
#### 数据如何获取
程序的本质,可说就是数据的呈现和加工。所以,看一个客户端开发平台的基本能力,首先就要看能把哪些数据放在上面处理,有哪些局限?如果缺少了必要的数据获取方式,那对于开发者而言,巧妇也难为无米之炊。
从这点看,小程序提供的数据获取方式非常丰富,大概涵盖:
**通过 HTTPS 请求去服务端获取数据。**支持 HTTPS 是最基本的,小程序对
HTTPS 有限制,除了要求通信协议是 HTTPS,出现的域名必须提前预设之外,还将应用层协议限定到了 JSON 格式下。这一点,可能比任何一个已有客户端平台都更为严苛。站在小程序的平台角度来看,通过这样的协议规定,对应用中流动的数据有了更强的管控能力;而对于开发者而言,则需要花些时间去调整自己的服务协议以便适应小程序的要求。
**可以在本地文件系统上存取数据。**小程序提供了丰富的 API 供开发者在手机系统上存取文件。可用本地文件来做缓存、状态记忆等,为开发提供了便利。
**可以读写设备中的一部分信息。**小程序开放了一些 API,帮助开发者获得设备上的基本信息,比如手机型号、屏幕尺寸、网络状态等。较为有价值的是可以选择获取手机上的图片等多媒体文件,这给做图像应用提供了可能;并且,它还提供了罗盘、重力感应器、地理位置等信息,对开发者理解用户所处的环境大有裨益。
从上面的介绍不难看出,小程序中的数据获取方式,和一般浏览器提供的相仿(也就是和 HTML5 应用能获取的信息),比原生的客户端更局限一些,但对于绝大多数的应用而言足够用了。
除此之外,小程序提供了微信生态中的一些数据,比如账号信息等。这对于微信庞大的生态而言,只是非常小的一部分数据,但却是开发小程序应用中最值得利用的数据。
举个例子,在其他平台上,如果想要获取微信的账号信息,需要通过一次用户授权。假如用户暂时不想提供,则会使程序呈现“未登录”状态,给整个服务的展开带来困难。而在小程序中,只要用户点开,就意味着完成了授权,开发者可以直接读取到小程序的账号信息,并同步到自己的服务端作为该用户的身份标识,从而实现“始终登录”的状态,使得后续服务可以更好地提供。
一份可行的示例如下:
```
// 先调用登录接口,获得请求码
wx.login({
success: function (res) {
// 获取到请求码,继续请求用户的基本信息
var code = res.code
wx.getUserInfo({
success: function (res) {
// 获取到//了加密的用户信息,去服务端解密并存储
var userData = res.encryptedData
var iv = res.iv
wx.request({
url: 'https://my_account/...',
data: { code: code, user_data: userData, iv: iv },
success: function(res) { // 在服务器上,解析并生成自己的账号验证信息 var user = res.data.user
var token = res.data.token
// 并且还可以存在本地存储上,供下次打开使用
wx.setStorage({
key: 'my_token',
data: token
})
}
})
}
})
}
});
```
#### 界面如何呈现
小程序刚发布的时候,一片人开始惊呼 HTML5 的时代就要到来了,因为小程序在界面层使用了 HTML/CSS/JavaScript 这套 HTML5 的技术栈。但很快,随着聪明的程序员们对小程序的理解进一步加深,就发现小程序所说的 HTML/CSS/JavaScript 和 HTML5 中的完全不是一回事,其差异基本等同于 Java 和 JavaScript。
在小程序中,和 HTML 对应的是 WXML,保留下来的只有 HTML 的概念,而传统的`<div>``<a>`标签都完全被抛弃了。和 Facebook 的 React 类似,小程序引入了自己的 HTML 标签,它和`<article>``<section>`这样的语义标签不同,小程序中的标签更像是传统客户端开发中的组件(或者叫控件),每个组件都有自己背后的职能和使用方式。比如:如果需要展示图片,就只能用`<image>`标签,其他的都无法承载。而如果需要提供可选的文本,则只能使用`<text>`标签等。
这样的方式带来最大的问题就是传统的 HTML 页面都无法在小程序中呈现(而小程序正好,没提供类似 WebView 的客户端控件)。比如有大量的内容网站,其文章内容都是存储为一个 HTML 片段,无法直接呈现在小程序中。如果需要展示,一个思路是构建中间服务,将 HTML 转译成一种更简单利于渲染的中间格式数据,然后,在小程序端把中间格式的数据转换成小程序的标签进行呈现。我们在做“轻芒生活”的时候,正好设计并实现了一个转义服务,将任意一个 HTML 页面转换成中间格式(内部名是
RAML),解决了内容性 HTML 页在小程序上的呈现问题,如图1所示。
![enter image description here](http://images.gitbook.cn/b50a1e40-fb89-11e7-9163-3f36c859d544)
图1 在小程序中呈现HTML内容页
和 HTML 相比,小程序的 WXSS 算是比较完整地保留了 CSS 的特征,这一点还蛮出乎意料。WXSS 在语义上最大的不同,一是在于它支持了相对尺寸单位
rpx(responsive pixel),每 750rpx 等价于当前设备的屏幕宽度,它的引入,把那种繁复的屏幕尺寸适配变得简单了不少。而和 CSS 的另一个不同是它更像传统控件样式用法,不支持 CSS3 那么多的选择器,使用中更多的是一个控件一个 class。
小程序中虽然支持 ES6 标准的 JavaScript,但窗口级的 JavaScript 却完全被废弃掉了,开发者无法用 JavaScript 去调用 window、document 对象来修改界面元素完成逻辑。小程序中的 JavaScript 其实直接对应 Node.js 的用法,用来完成后台业务逻辑,而不是直接控制交互。小程序的这个设计,使其可以用到 Virtual Dom 的方式来渲染界面,让界面数据更新时的性能优化成为可能,但付出的代价就是少了窗口级 JavaScript 的那层胶水黏合,使得很多功能的开发变得极其呆板和繁复。
#### 交互如何传导
所谓交互的传导,是当用户和界面发生交互时,平台框架通过何种方式告诉业务层,并将处理后的变化呈现回交互界面上。如果把 WXSS + WXML 绘制的页面看成“前端”,把 JavaScript 撰写的业务逻辑看成“后端”,你会发现,小程序的前后端交互特别像
Web 1.0 的模式,前端把交互行为封装成事件(event)发送到后端,后端处理完成后,通过 setData 方法将数据回传到前端,如图2所示。
![enter image description here](http://images.gitbook.cn/c608a250-fb8b-11e7-9142-87570661ea4f)
图2 小程序的交互传导
小程序提供的 Events,基础的有类似单击、长按、触摸、滑动等,对于视频播放器等控件,还有监听播放、暂停等。这些事件比较基础,没有更高级的手势、多点触控等相关事件,但也还足够让开发者具体了解用户的输入,进而做出响应。
而小程序给界面响应的唯一方式,是通过 Page 中的 setData API 对界面上的数据进行更新,小程序会比较两次调用期间数据的变化,来决策需要更新哪部分的交互界面。
举个实际的例子,假设开发者需要做一个滑动切换页面的效果,在小程序中该如何实现?首先,是将变量数据引入渲染页面:
```
<view class="page" id="current-page" style="left:{{distance}}rpx;" bindtouchstart="movePage" bindtouchcancel="movePage" bindtouchmove="movePage" bindtouchend="movePage">
</view>
```
可以看到,distance 是一个模版参数,它初始值为0,表示移动的距离。通过 bindtouchstart 等函数绑定上 JavaScript 的方法,将事件回传。
```
movePage: function(event) {
var status = {
needUpdate: false,
distance: 0
}
// 处理各种事件,计算是否需要刷新,和移//动方向
if ("touchstart" === event.type) {
// 开始计算移动
...
} else if ("touchend" === event.type) {
// 判定移动的距离是否足够.
...
} else if ("touchcancel" === event.type) {
// 被打断就算了.
...
} else if ("touchmove" === event.type) {
// 计算移动距离
...
}
// 根据移动的距离,来更新界面
if (status.needUpdate) {
this.setData({
distance: status.distance
})
}
}
```
而在 JavaScript 一端,则捕获事件、计算偏移量,然后将新的偏移量送到前端界面。
从这里可以看到,小程序的交互是典型的单向模式,前端回传事件,数据单向地推到前端,而不是通过类似“变量”、“状态”等方式来告知。这样的模式下,开发者对界面变化的控制往往不可能太精准,整个核心都依赖于小程序对两次数据变化的 diff 计算,这将会最终影响整个交互的性能。
#### 小程序开发模式的特点
至此,我们可以来总结一下小程序开发的一些特点了。整体来看,小程序是借了 HTML5 的技术栈,行了传统客户端开发的模式,这一点和 React 等平台会比较相近,可以视为 HTML5 的一个新分支。
从设计思路看,小程序做了大量的“限制”,最大的限制是开发者其实无法通过 JavaScript 这样的编程语言直接对界面进行控制,而是通过数据驱动来间接实现。这对于缺少开发经验的人而言,是有益的事情,因为降低了理解的门槛,但对于复杂的应用来说,这个模式开发起来比较呆板,往往是一个变化多处修改,增加了理解代码的成本。
#### 开发小程序的坑
开发小程序的日子,也是一个踩坑的历程。简单总结,小程序中的坑大概来自这几个方面:
**Web 兼容性。**小程序引入了 HTML/CSS 作为技术栈,并在其基础上进行了定制。很多开发中的问题都来自于“定制”,因为你并不知道哪部分是被定制,哪部分是被继承了。比如,你用了一个 CSS 语法,发现并不生效,或者效果和浏览器中的不一样,于是,只能换一个写法,结果很有可能又会继续发现,这个新的写法可能效果也不对,于是只能继续尝试,如此反复,可能会消耗大量的时间。
**开发环境不稳定。**小程序的开发,是基于微信自制的 IDE,但当下,IDE 的稳定性、易用性都非常差,时常出现 Bug,你以为是程序写错了,但其实,是 IDE 的
Bug,重启一下 IDE,一切都迎刃而解了。于是,当你日后开发小程序时出现某种异样,先重启 IDE,再看问题还在不在,也许是种更节省时间的方式。
**缺少真机调试环境。**小程序的运行时其实就是微信,微信几乎没提供任何真机上的调试工具(也不能说完全没有,有一个只能在真机上瞪着眼睛看的日志框)。在模拟器中调试好的程序,可能在真机上运行起来并不如预期。比如,我们碰到过真机上白屏、位置错乱、动画效果不对,以及 Android 上至今还不能运行等问题。这对于稍微复杂的程序而言,颇为梦魇,想做一些细粒度的调整和优化,基本只能靠猜。
**闭源且缺少学习资料。**小程序整体上是闭源状态(虽然模拟器和 IDE 部分可以通过反编译来看),且缺少足够的学习资料。如果一旦碰到控件如何使用、为什么这么用不对之类的问题,就只能靠不停地试来解决,也需要耗费大量时间。
简而言之,作为一个新的开发平台,微信小程序从本身的稳定性,以及配套的工具链上都不算完善。对于早期开发者而言,需要耗费额外的精力去尝试和探索,但这也许就是一个新平台的价值和代价吧。
><br>范怀宇<br>
>轻芒联合创始人,毕业于清华大学,前豌豆荚技术负责人,专注于移动开发十余年,曾 出版《Android 开发精要》。爱研习好代码和设计,相信好的产品能改变生活,好阅读乐分享。
\ No newline at end of file
此差异已折叠。
## 深度增强学习前沿算法思想
>2016年 AlphaGo 计算机围棋系统战胜顶尖职业棋手李世石,引起了全世界的广泛关注,人工智能进一步被 推到了风口浪尖。而其中的深度增强学习算法是 AlphaGo 的核心,也是通用人工智能的实现关键。本文将 带领大家了解深度增强学习的前沿算法思想,领略人工智能的核心奥秘。
#### 前言
深度增强学习(Deep Reinforcement Learning,DRL)是近两年来深度学习领域迅猛发展起来的一个分支,目的是解决计算机从感知到决策控制的问题,从而实现通用人工智能。以 Google DeepMind 公司为首,基于深度增强学习的算法已经在视频、游戏、围棋、机器人等领域取得了突破性进展。2016年 Google DeepMind 推出的 AlphaGo 围棋系统,使用蒙特卡洛树搜索和深度学习结合的方式使计算机的围棋水平达到甚至超过了顶尖职业棋手的水平,引起了世界性的轰动。AlphaGo 的核心就在于使用了深度增强学习算法,使得计算机能够通过自对弈的方式不断提升棋力。深度增强学习算法由于能够基于深度神经网络实现从感知到决策控制的端到端自学习,具有非常广阔的应用前景,它的发展也将进一步推动人工智能的革命。
#### 深度增强学习与通用人工智能
当前深度学习已经在计算机视觉、语音识别、自然语言理解等领域取得了突破,相关技术也已经逐渐成熟并落地进入到我们的生活当中。然而,这些领域研究的问题都只是为了让计算机能够感知和理解这个世界。以此同时,决策控制才是人工智能领域要解决的核心问题。计算机视觉等感知问题要求输入感知信息到计算机,计算机能够理解,而决策控制问题则要求计算机能够根据感知信息进行判断思考,输出正确的行为。要使计算机能够很好地决策控制,要求计算机具备一定的“思考”能力,使计算机能够通过学习来掌握解决各种问题的能力,而这正是通用人工智能(Artificial General Intelligence,AGI)(即强人工智能)的研究目标。通用人工智能是要创造出一种无需人工编程自己学会解决各种问题的智能体,最终目标是实现类人级别甚至超人级别的智能。
通用人工智能的基本框架即是增强学习(Reinforcement Learning,RL)的框架,如图1所示。
![enter image description here](http://images.gitbook.cn/03334ae0-fa6a-11e7-90fc-07285d18dfef)
图1 通用人工智能基本框架
智能体的行为都可以归结为与世界的交互。智能体观察这个世界,然后根据观察及自身的状态输出动作,这个世界会因此而发生改变,从而形成回馈返回给智能体。所以核心问题就是如何构建出这样一个能够与世界交互的智能体。深度增强学习将深度学习(Deep Learning)和增强学习(Reinforcement Learning)结合起来,深度学习用来提供学习的机制,而增强学习为深度学习提供学习的目标。这使得深度增强学习具备构建出复杂智能体的潜力,也因此,AlphaGo 的第一作者 David Silver 认为深度增强学习等价于通用人工智能 DRL=DL+RL=Universal AI。
#### 深度增强学习的 Actor-Critic 框架
目前深度增强学习的算法都可以包含在 Actor-Critic 框架下,如图2所示。
![enter image description here](http://images.gitbook.cn/091b2220-fa6a-11e7-b1db-d1714f4f2996)
图2 Actor-Critic框架
把深度增强学习的算法认为是智能体的大脑,那么这个大脑包含了两个部分:Actor 行动模块和 Critic 评判模块。其中 Actor 行动模块是大脑的执行机构,输入外部的状态
s,然后输出动作 a。而 Critic 评判模块则可认为是大脑的价值观,根据历史信息及回馈 r 进行自我调整,然后影响整个 Actor 行动模块。这种 Actor-Critic 的方法非常类似于人类自身的行为方式。我们人类也是在自身价值观和本能的指导下进行行为,并且价值观受经验的影响不断改变。在 Actor-Critic 框架下,Google DeepMind 相继提出了 DQN,A3C 和 UNREAL 等深度增强学习算法,其中 UNREAL 是目前最好的深度增强学习算法。下面我们将介绍这三个算法的基本思想。
#### DQN(Deep Q Network)算法
DQN 是 Google DeepMind 于2013年提出的第一个深度增强学习算法,并在2015年进一步完善,发表在2015年的《Nature》上。DeepMind 将 DQN 应用在计算机玩 Atari
游戏上,不同于以往的做法,仅使用视频信息作为输入,和人类玩游戏一样。在这种情况下,基于 DQN 的程序在多种 Atari 游戏上取得了超越人类水平的成绩。这是深度增强学习概念的第一次提出,并由此开始快速发展。
DQN 算法面向相对简单的离散输出,即输出的动作仅有少数有限的个数。在这种情况下,DQN 算法在 Actor-Critic 框架下仅使用 Critic 评判模块,而没有使用 Actor 行动模块,因为使用 Critic 评判模块即可以选择并执行最优的动作,如图3所示。
![enter image description here](http://images.gitbook.cn/10c20e80-fa6a-11e7-8bf2-5726d84cb1cb)
图3 DQN 基本结构
在 DQN 中,用一个价值网络(Value Network)来表示 Critic 评判模块,价值网络输出 Q(s,a),即状态 s 和动作 a 下的价值。基于价值网络,我们可以遍历某个状态 s
下各种动作的价值,然后选择价值最大的一个动作输出。所以,主要问题是如何通过深度学习的随机梯度下降方法来更新价值网络。为了使用梯度下降方法,我们必须为价值网络构造一个损失函数。由于价值网络输出的是 Q 值,因此如果能够构造出一个目标 Q 值,就能够通过平方差 MSE 的方式来得到损失函数。但对于价值网络来说,输入的信息仅有状态 s,动作 a 及回馈 r。因此,如何计算出目标 Q 值是 DQN 算法的关键,而这正是增强学习能够解决的问题。基于增强学习的 Bellman 公式,我们能够基于输入信息特别是回馈 r 构造出目标 Q 值,从而得到损失函数,对价值网络进行更新。
![enter image description here](http://images.gitbook.cn/168b1320-fa6a-11e7-b1db-d1714f4f2996)
图4 UNREAL 算法框图
在实际使用中,价值网络可以根据具体的问题构造不同的网络形式。比如 Atari 有些输入的是图像信息,就可以构造一个卷积神经网络(Convolutional Neural Network,CNN)来作为价值网络。为了增加对历史信息的记忆,还可以在 CNN 之后加上 LSTM 长短记忆模型。在 DQN 训练的时候,先采集历史的输入输出信息作为样本放在经验池(Replay Memory)里面,然后通过随机采样的方式采样多个样本进行 minibatch 的随机梯度下降训练。
DQN 算法作为第一个深度增强学习算法,仅使用价值网络,训练效率较低,需要大量的时间训练,并且只能面向低维的离散控制问题,通用性有限。但由于 DQN 算法第一次成功结合了深度学习和增强学习,解决了高维数据输入问题,并且在 Atari 游戏上取得突破,具有开创性的意义。
#### A3C(Asynchronous Advantage Actor Critic)算法
A3C 算法是2015年 DeepMind 提出的相比 DQN 更好更通用的一个深度增强学习算法。A3C 算法完全使用了 Actor-Critic 框架,并且引入了异步训练的思想,在提升性能的同时也大大加快了训练速度。A3C 算法的基本思想,即 Actor-Critic 的基本思想,是对输出的动作进行好坏评估,如果动作被认为是好的,那么就调整行动网络(Actor Network)使该动作出现的可能性增加。反之如果动作被认为是坏的,则使该动作出现的可能性减少。通过反复的训练,不断调整行动网络找到最优的动作。AlphaGo 的自我学习也是基于这样的思想。
基于 Actor-Critic 的基本思想,Critic 评判模块的价值网络(Value Network)可以采用 DQN 的方法进行更新,那么如何构造行动网络的损失函数,实现对网络的训练是算法的关键。一般行动网络的输出有两种方式:一种是概率的方式,即输出某一个动作的概率;另一种是确定性的方式,即输出具体的某一个动作。A3C 采用的是概率输出的方式。因此,我们从 Critic 评判模块,即价值网络中得到对动作的好坏评价,然后用输出动作的对数似然值(Log Likelihood)乘以动作的评价,作为行动网络的损失函数。行动网络的目标是最大化这个损失函数,即如果动作评价为正,就增加其概率,反之减少,符合
Actor-Critic 的基本思想。有了行动网络的损失函数,也就可以通过随机梯度下降的方式进行参数的更新。
为了使算法取得更好的效果,如何准确地评价动作的好坏也是算法的关键。A3C 在动作价值Q的基础上,使用优势 A(Advantage)作为动作的评价。优势 A 是指动作 a 在状态
s 下相对其他动作的优势。假设状态 s 的价值是 V,那么 A=Q-V。这里的动作价值 Q
是指状态 s 下 a 的价值,与 V 的含义不同。直观上看,采用优势 A 来评估动作更为准确。举个例子来说,假设在状态 s 下,动作1的 Q 值是3,动作2的 Q 值是1,状态s的价值V是2。如果使用 Q 作为动作的评价,那么动作1和2的出现概率都会增加,但是实际上我们知道唯一要增加出现概率的是动作1。这时如果采用优势 A,我们可以计算出动作1的优势是1,动作2的优势是-1。基于优势A来更新网络,动作1的出现概率增加,动作2的出现概率减少,更符合我们的目标。因此,A3C 算法调整了 Critic 评判模块的价值网络,让其输出 V 值,然后使用多步的历史信息来计算动作的 Q 值,从而得到优势 A,进而计算出损失函数,对行动网络进行更新。
A3C 算法为了提升训练速度还采用异步训练的思想,即同时启动多个训练环境,同时进行采样,并直接使用采集的样本进行训练。相比 DQN 算法,A3C 算法不需要使用经验池来存储历史样本,节约了存储空间,并且采用异步训练,大大加倍了数据的采样速度,也因此提升了训练速度。与此同时,采用多个不同训练环境采集样本,样本的分布更加均匀,更有利于神经网络的训练。
A3C 算法在以上多个环节上做出了改进,使得其在 Atari 游戏上的平均成绩是 DQN 算法的4倍,取得了巨大的提升,并且训练速度也成倍的增加。因此,A3C 算法取代了 DQN 成为了更好的深度增强学习算法。
#### UNREAL(UNsupervised REinforcement and Auxiliary Learning)算法
UNREAL 算法是2016年11月 DeepMind 提出的最新深度增强学习算法,在A3C算法的基础上对性能和速度进行进一步提升,在 Atari 游戏上取得了人类水平8.8倍的成绩,并且在第一视角的3D迷宫环境 Labyrinth 上也达到了87%的人类水平,成为当前最好的深度增强学习算法。
A3C 算法充分使用了 Actor-Critic 框架,是一套完善的算法,因此,我们很难通过改变算法框架的方式来对算法做出改进。UNREAL 算法在 A3C 算法的基础上,另辟蹊径,通过在训练 A3C 的同时,训练多个辅助任务来改进算法。UNREAL 算法的基本思想来源于我们人类的学习方式。人要完成一个任务,往往通过完成其他多种辅助任务来实现。比如说我们要收集邮票,可以自己去买,也可以让朋友帮忙获取,或者和其他人交换的方式得到。UNREAL 算法通过设置多个辅助任务,同时训练同一个 A3C 网络,从而加快学习的速度,并进一步提升性能。
在 UNREAL 算法中,包含了两类辅助任务:第一种是控制任务,包括像素控制和隐藏层激活控制。像素控制是指控制输入图像的变化,使得图像的变化最大。因为图像变化大往往说明智能体在执行重要的环节,通过控制图像的变化能够改善动作的选择。隐藏层激活控制则是控制隐藏层神经元的激活数量,目的是使其激活量越多越好。这类似于人类大脑细胞的开发,神经元使用得越多,可能越聪明,也因此能够做出更好的选择。另一种辅助任务是回馈预测任务。因为在很多场景下,回馈 r 并不是每时每刻都能获取的(比如在 Labyrinth 中吃到苹果才能得1分),所以让神经网络能够预测回馈值会使其具有更好的表达能力。在
UNREAL 算法中,使用历史连续多帧的图像输入来预测下一步的回馈值作为训练目标。除了以上两种回馈预测任务外,UNREAL 算法还使用历史信息额外增加了价值迭代任务,即
DQN 的更新方法,进一步提升算法的训练速度。
UNREAL 算法本质上是通过训练多个面向同一个最终目标的任务来提升行动网络的表达能力和水平,符合人类的学习方式。值得注意的是,UNREAL 虽然增加了训练任务,但并没有通过其他途径获取别的样本,是在保持原有样本数据不变的情况下对算法进行提升,这使得 UNREAL 算法被认为是一种无监督学习的方法。基于 UNREAL 算法的思想,可以根据不同任务的特点针对性地设计辅助任务,来改进算法。
#### 小结
深度增强学习经过近两年的发展,在算法层面上取得了越来越好的效果。从 DQN,A3C 到
UNREAL,精妙的算法设计无不闪耀着人类智慧的光芒。在未来,除了算法本身的改进,深度增强学习作为能够解决从感知到决策控制的通用型学习算法,将能够在现实生活中的各种领域得到广泛的应用。AlphaGo 的成功只是通用人工智能爆发的前夜。
\ No newline at end of file
## 深度学习在推荐领域的应用
文/吴岸城
2012年 Facebook 在广告领域开始应用定制化受众(Facebook Custom Audiences)功能后,“受众发现”这个概念真正得到大规模应用,什么叫“受众发现”?如果你的企业已经积累了一定的客户,无论这些客户是否关注你或者是否跟你在 Facebook 上有互动,都能通过 Facebook 的广告系统触达到。“受众发现”实现了什么功能?在没有这个系统之前,广告投放一般情况都是用标签去区分用户,再去给这部分用户发送广告,“受众发现”让你不用选择这些标签,包括用户基本信息、兴趣等。你需要做的只是上传一批你目前已有的用户或者你感兴趣的一批用户,剩下的工作就等着 Custom Audiences 帮你完成了。
Facebook 这种通过一群已有的用户发现并扩展出其他用户的推荐算法就叫 Lookalike,当然 Facebook 的算法细节笔者并不清楚,各个公司实现 Lookalike 也各有不同。这里也包括腾讯在微信端的广告推荐上的应用、Google 在 YouTube 上推荐感兴趣视频等。下面让我们结合前人的工作,实现自己的 Lookalike 算法,并尝试着在新浪微博上应用这一算法。
### 调研
首先要确定微博领域的数据,关于微博的数据可以这样分类:
用户基础数据:年龄、性别、公司、邮箱、地点、公司等。
关系图:根据人—人,人—微博的关注、评论、转发信息建立关系图。
内容数据:用户的微博内容,包含文字、图片、视频。
有了这些数据后,怎么做数据的整合分析?来看看现在应用最广的方式——协同过滤、或者叫关联推荐。协同过滤主要是利用某兴趣相投、拥有共同经验群体的喜好来推荐用户可能感兴趣的信息,协同过滤的发展有以下三个阶段:
第一阶段,基于用户喜好做推荐,用户 A 和用户 B 相似,用户 B 购买了物品 a、b、c,用户 A 只购买了物品 a,那就将物品 b、c 推荐给用户 A。这就是基于用户的协同过滤,其重点是如何找到相似的用户。因为只有准确的找到相似的用户才能给出正确的推荐。而找到相似用户的方法,一般是根据用户的基本属性贴标签分类,再高级点可以用上用户的行为数据。
第二阶段,某些商品光从用户的属性标签找不到联系,而根据商品本身的内容联系倒是能发现很多有趣的推荐目标,它在某些场景中比基于相似用户的推荐原则更加有效。比如在购书或者电影类网站上,当你看一本书或电影时,推荐引擎会根据内容给你推荐相关的书籍或电影。
第三阶段,如果只把内容推荐单独应用在社交网络上,准确率会比较低,因为社交网络的关键特性还是社交关系。如何将社交关系与用户属性一起融入整个推荐系统就是关键。在神经网络和深度学习算法出现后,提取特征任务就变得可以依靠机器完成,人们只要把相应的数据准备好就可以了,其他数据都可以提取成向量形式,而社交关系作为一种图结构,如何表示为深度学习可以接受的向量形式,而且这种结构还需要有效还原原结构中位置信息?这就需要一种可靠的向量化社交关系的表示方法。基于这一思路,在2016年的论文中出现了一个算法 node2vec,使社交关系也可以很好地适应神经网络。这意味着深度学习在推荐领域应用的关键技术点已被解决。
在实现算法前我们主要参考了如下三篇论文:
- Audience Expansion for Online Social Network Advertising 2016
- node2vec: Scalable Feature Learning for Networks Aditya Grover 2016
- Deep Neural Networks for YouTube Recommen dations 2016
第一篇论文是 LinkedIn 给出的,主要谈了针对在线社交网络广告平台,如何根据已有的受众特征做受众群扩展。这涉及到如何定位目标受众和原始受众的相似属性。论文给出了两种方法来扩展受众:
1. 与营销活动无关的受众扩展;
2. 与营销活动有关的受众扩展。
在图1中,LinkedIn 给出了如何利用营销活动数据、目标受众基础数据去预测目标用户行为进而发现新的用户。今天的推荐系统或广告系统越来越多地利用了多维度信息。如何将这些信息有效加以利用,这篇论文给出了一条路径,而且在工程上这篇论文也论证得比较扎实,值得参考。
![enter image description here](http://images.gitbook.cn/b74502e0-fab4-11e7-8e77-7b11967ffccc)
图1 LinkedIn 的 Lookalike 算法流程图
第二篇论文,主要讲的是 node2vec,这也是本文用到的主要算法之一。node2vec 主要用于处理网络结构中的多分类和链路预测任务,具体来说是对网络中的节点和边的特征向量表示方法。
简单来说就是将原有社交网络中的图结构,表达成特征向量矩阵,每一个 node(可以是人、物品、内容等)表示成一个特征向量,用向量与向量之间的矩阵运算来得到相互的关系。
下面来看看 node2vec 中的关键技术——随机游走算法,它定义了一种新的遍历网络中某个节点的邻域的方法,具体策略如图2所示。
![enter image description here](http://images.gitbook.cn/88a58c70-f9bf-11e7-8705-d35a79003718)
图2 随机游走策略
假设我们刚刚从节点 t 走到节点 v,当前处于节点 v,现在要选择下一步该怎么走,方案如下:
![enter image description here](http://images.gitbook.cn/98204190-f9bf-11e7-a8eb-45b2926f7454)
其中 dtx 表示节点 t 到节点 x 之间的最短路径,dtx=0 表示会回到节点 t 本身,dtx=1 表示节点 t 和节点 x 直接相连,但是在上一步却选择了节点 v,dtx=2 表示节点 t 不与 x 直接相连,但节点 v 与 x 直接相连。其中 p 和 q 为模型中的参数,形成一个不均匀的概率分布,最终得到随机游走的路径。与传统的图结构搜索方法(如 BFS 和 DFS)相比,这里提出的随机游走算法具有更高的效率,因为本质上相当于对当前节点的邻域节点的采样,同时保留了该节点在网络中的位置信息。
node2vec 由斯坦福大学提出,并有开源代码,这里顺手列出,这一部分大家不用自己动手实现了。https://github.com/aditya-grover/node2vec
>注:本文的方法需要在源码的基础上改动图结构。
第三篇论文讲的是 Google 如何做 YouTube 视频推荐,论文是在我做完结构设计和流程设计后看到的,其中模型架构的思想和我们不谋而合,还解释了为什么要引入 DNN(后面提到所有的 feature 将会合并经历几层全连接层):引入 DNN 的好处在于大多数类型的连续特征和离散特征可以直接添加到模型当中。此外我们还参考了这篇论文对于隐含层(FC)单元个数选择。图3是这篇论文提到的算法结构。
### 实现
- 数据准备
- 获得用户的属性(User Profile),如性别、
年龄、学历、职业、地域、能力标签等;
- 根据项目内容和活动内容制定一套受众标签(Audience Label);
- 提取用户之间的关注关系,微博之间的转发关系;
- 获取微博 message 中的文本内容;
- 获得微博 message 中的图片内容。
- 用户标签特征处理
- 根据步骤 a 中用户属性信息和已有的部分受众标签系统。利用 GBDT 算法(可以直接用 xgboost)将没有标签的受众全部打上标签。这个分类问题中请注意处理连续值变量以及归一化。
- 将标签进行向量化处理,这个问题转化成对中文单词进行向量化,这里用 word2vec 处理后得到用户标签的向量化信息 Label2vec。这一步也可以使用 word2vec 在中文的大数据样本下进行预训练,再用该模型对标签加以提取,对特征的提取有一定的提高,大约在0.5%左右。
![enter image description here](http://images.gitbook.cn/a947e360-f9bf-11e7-871f-074ef18dadad)
图3 YouTube 推荐结构图
- 文本特征处理
将步骤 a 中提取到的所有微博 message 文本内容清洗整理,训练 Doc2Vec 模型,得到单个文本的向量化表示,对所得的文本作聚类(KMeans,在 30w 的微博用户的 message 上测试,K 取128对文本的区分度较强),最后提取每个 cluster 的中心向量,并根据每个用户所占有的 cluster 获得用户所发微博的文本信息的向量表示 Content2vec。
- 图像特征(可选)
将步骤 a 中提取到的所有的 message 图片信息整理分类,使用预训练卷积网络模型(这里为了平衡效率选取 VGG16 作为卷积网络)提取图像信息,对每个用户 message 中的图片做向量化处理,形成 Image2vec,如果有多张图片将多张图片分别提取特征值再接一层 MaxPooling 提取重要信息后输出。
- 社交关系建立(node2vec 向量化)
将步骤 a 中获得到的用户之间的关系和微博之间的转发评论关系转化成图结构,并提取用户关系 sub-graph,最后使用 node2Vec 算法得到每个用户的社交网络图向量化表示。图4为简历社交关系后的部分图示。
![enter image description here](http://images.gitbook.cn/ce481fe0-fab4-11e7-aea8-4fc758f7cece)
图4 用户社交关系
- 将 bcde 步骤得到的向量做拼接,经过两层 FC,得到表示每个用户的多特征向量集(User Vector Set,UVS)。这里取的输出单元个数时可以根据性能和准确度做平衡,目前我们实现的是输出512个单元,最后的特征输出表达了用户的社交关系、用户属性、发出的内容、感兴趣的内容等的混合特征向量,这些特征向量将作为下一步比对相似性的输入值。
- 分别计算种子用户和潜在目标用户的向量集,并比对相似性,我们使用的是余弦相似度计算相似性,将步骤 f 得到的用户特征向量集作为输入 x,y,代入下面公式计算相似性:
使用余弦相似度要注意:余弦相似度更多的是从方向上区分差异,而对绝对的数值不敏感。因此没法衡量每个维度值的差异,这里我们要在每个维度上减去一个均值或者乘以一个系数,或者在之前做好归一化。
- 受众扩展
- 获取种子受众名单,以及目标受众的数量 N;
- 检查种子用户是否存在于 UVS 中,将存在的用户向量化;
- 计算受众名单中用户和 UVS 中用户的相似度,提取最相似的前 N 个用户作为目标受众。
最后我们将以上步骤串联起来,形成如图5所示。
![enter image description here](http://images.gitbook.cn/decb88c0-f9bf-11e7-8107-bf28bbc85771)
图5 Lookalike 算法示意图
在以上步骤中特征提取完成后,我们使用一个2层的神经网络做最后的特征提取,算法结构示意图如图6所示。
![enter image description here](http://images.gitbook.cn/ed155f00-f9bf-11e7-871f-074ef18dadad)
图6 Lookalike 算法结构图
其中 FC1 层也可以替换成 MaxPooling,MaxPooling 层具有强解释性,也就是在用户特征群上提取最重要的特征点作为下一层的输入,读者可以自行尝试,这里限于篇幅问题就不做展开了。
讲到这里,算法部分就已基本完结,其中还有些工程问题,并不属于本次主题探讨范围,这里也不做讨论了。
### 结果
我司算法团队根据 Lookalike 思想完整实现其算法,并在实际产品中投入试用。针对某客户(乳品领域世界排名前三的品牌主)计算出结果(部分):
表1 部分计算结果
![enter image description here](http://images.gitbook.cn/dfd0cd70-fab4-11e7-aea8-4fc758f7cece)
可以观察到以上微博 ID 的主题基本都是西点企业或西点培训企业,和品牌主售卖的乳品有很高的关联性:乳品是非常重要的西点原料,除终端用户外,西点相关企业就是乳品企业主需要寻找的最重要的受众之一。
### 探讨
#### 特征表达
除了以上提到的特征外,我们也对其他的重要特征表达做了处理和变换:根据我们的需求,需要抽取出人的兴趣特征,如何表达一个人的兴趣?除了他自己生成的有关内容外,还有比较关键的一点是比如“我”看了一些微博,但并没有转发,大多数情况下都不会转发,但有些“我”转发了,有些“我”评论了;“我”转发了哪些?评论了哪些?这次距上次的浏览该人的列表时间间隔多久?都代表“我”对微博的兴趣,而间接的反应“我”的兴趣特征。这些数据看来非常重要,又无法直接取得,怎么办?
下面来定义一个场景,试图描述出我们对看过的内容中哪些是感兴趣的,哪些不是感兴趣的:
- 用户 A,以及用户 A 关注的用户 B;
- 用户 A 的每天动作时间(比如他转发、评论、收藏、点赞)起始时间,我们定义为苏醒时间 A_wake(t);
- 用户 B 每天发帖(转发、评论)时间:B_action(t);
- 简单假设一下A_wake(t)>B_action(t),也就是B_action(t) 的评论都能看到。这就能得到用户 A 对应了哪些帖子;
- 同理,也可知用户 A 在 A_wake(t) 时间内转发了、评论了哪些帖子;
- 结合上次浏览间隔时间,可以描述用户 A 对哪些微博感兴趣(positive),哪些不感兴趣(negative)。
#### 全连接层的激活单元比对提升
在 Google 那篇论文中比对隐含层(也就是我们结构图中的 FC 层)各种单元组合产生的结果,Google 选择的是最后一种组合,如图7所示。
![enter image description here](http://images.gitbook.cn/fa757180-f9bf-11e7-8107-bf28bbc85771)
图7 YouTube 推荐模型隐含层单元选择对比
我们初期选用了 512tanh→56tanh 这种两层组合,后认为输入特征维度过大,512个单元无法完整的表达特征,故又对比了1024→512组合,发现效果确实有微小提升大概在0.7%。另外我们的 FC 层输入在(-1,1)区间,考虑到 relu 函数的特点没有使用它,而是使用 elu 激活函数。测试效果要比 tanh 函数提升0.3%-0.5%。
**附:**
node2vec 伪码:
![enter image description here](http://images.gitbook.cn/21f0dd80-f9c0-11e7-8107-bf28bbc85771)
\ No newline at end of file
## 物联网技术现状与新可能
文/罗未
不管是从商业模式导出的业务模型,还是从技术发展的角度看,文本都倾向于将物联网技术构架看作是互联网技术构架的延展。而与这个观念对立的,是传统嵌入式软件开发的视角。
### 在互联网技术基础上长出来的物联网构架
简单来说,目前的互联网技术构架主流是大前端与后端两个世界:大前端包括 Web 的 JavaScript 技术、Android 和 iOS 技术,着眼于解决用户交互;后端包括数据库、服务构架、运维等,着眼于解决存储、业务逻辑、安全与效率等。当然,现在前后端技术争相更新,比如业务逻辑前置化、微服务构架、JavaScript 全栈化等新的解决方案也开始模糊前后端的差异。而物联网设备端的引入,着实让这些技术有点难以归类,从业务性质上物联网是另外一种前端或是前端的延伸,比如共享单车应用中,自行车端的应用显然是跟人交互的另一个业务场景,也在为后端源源不断地提供着数据,但是自行车又不像网页或者 App 完全是在解决可视化 UI 的事情。而且,现在的设备端开发技术跟前端技术太不像了,由于目前设备端的开发技术都还偏底层,一般来说计算资源如处理能力、本地存储都非常有限,反而像后端一样要考虑资源效率。
那么,我们只好为物联网单独命名一个端,不如我们暂时就叫它设备端。
<img src="http://ipad-cms.csdn.net/cms/attachment/201706/592fe0566abc4.png" alt="图1 整体架构图" title="图1 整体架构图" />
图1 整体架构图
### 新后端
#### MQTT
新后端核心问题在于加入了面向设备的接入服务,实际上在这里,除类似视频对讲或是安防监控的多媒体实时通道外,这个接入服务已经基本事实化为 MQTT。
消息队列遥感传输协议是在TCP/IP协议之上使用的,基于发布/订阅的“轻量级”消息协议,目前为 ISO 标准(ISO/IEC PRF 20922)。它被设计用于轻量级和低带宽的远程连接,发布/订阅消息传递模式需要消息代理,消息代理负责根据消息的主题向需要的端发布消息。
如果需要连接的设备没有超过10万台,使用 8GB 内存的云主机跑 Mosquitto 就可以;如果设备量是几十万台,可以考虑 Mosquitto 做集群负载均衡;如果设备量是大几十万台乃至百万台以上,那你需要专业的团队或专门的投入来维护这件事情,这个细节就不在本文讨论范围了。
#### OTA
固件组件在线升级是必须要做的事情,MQTT传 大文件不靠谱,所以一般传过去一个带 Token 的 URL,设备端去下载就好,HTTP 或者 HTTPS 都可以。业务比较简单,设备端几十万以内没有什么特别的地方。
#### 数据存储与服务
Mosquitto 作为 MQTT 的引擎,需要后端按照业务逻辑去调用,这里按照业务需求写好后端逻辑即可。在各种后端语言中调用 Mosquitto 都非常简单。
### 设备端
设备端是物联网领域最五花八门并且正在发展中的地方。其他领域,后端或者前端,经过十几年的发展,已经出现每个细节的主流技术,基本没有碎片化的情况,但是在设备端,开发技术的碎片化是应用发展还不到位的充分表现。举例讲,选用不同的芯片,就要用不同的操作系统,不同的 C 库封装,各家 IDE 也不尽相同,编译工具链更是从芯片原厂给出。开发起来呢,寄存器、内存分配、硬件中断都要深入进去。这就是传统嵌入式开发的现状,也是物联网设备端开发的现状。
到目前为止,真正生产环境中用到的语言就是 C/C++,极个别会在设备端用到 Python,基本没有其他语言。操作系统超过50种,主流的也有10种以上,其中嵌入式 Linux 份额并不大,各种实时操作系统各具特色,各有一片天地。
简单总结一下相对于物联网开发,传统嵌入式开发的方式主要有以下几个问题:
1. 需要考虑中断、寄存器、内存分配等过于底层的工作;
2. 编译、烧写、观察、借助调试设备进行调试的开发生命周期;
3. 不同 SoC 和系统的差异过大;
4. 缺乏代码复用与开源的习惯;
5. 开发者在开发环境和固件编译上花费的时间过多。
所以我们看到设备端的开发是基于芯片选型完成的。当设备端产品面临一个需求时,现有的流程是判断产品的各项技术参数,从而确定一个芯片,进而使用这个芯片的一整套开发技术。这也是早期嵌入式场景使用的芯片自生技术特性所决定:计算资源(CPU 主频、存储)、外围接口、使用温度、通讯协议等核心参数的不同导致芯片碎片化,芯片碎片化导致嵌入式开发碎片化。
目前这个领域的大趋势是:物联网芯片有望走向趋同,物联网开发环境与技术有望趋同。
#### 物联网芯片
早期由于成本所限,物联网领域使用的芯片总是表现得非常缺资源,很难找到一个各方面(计算资源、外围接口、使用温度、通讯协议等)都比较合适的芯片去适应普遍的场景。随着半导体门槛逐步降低,中国半导体制造业逐步成型,芯片资源开始走向富余,其中的代表芯片是 MTK 的 MT7697、MT7688 和乐鑫的 ESP32。
MT7697 主要参数为:ARM Cortex M4 CPU,带浮点单元,最大主频 192Mhz,内存为 256KB SRAM,可配置 4MB 以上的存储空间,芯片内嵌 WiFi 和 BLE 4.2,有足够的外围接口,并能够适应工业级的使用温度。
MT7688 主要参数为:MIPS 580Mhz CPU,内存最大支持 256MB,可配置 16GB 级别的存储空间,芯片内嵌 WiFi,接口除模拟接口之外数字接口丰富,价格在几十元人民币,功耗较高不适于电池长期使用。但是非常有优势的是其提供的 Linux 开发环境,能够让开发者有一种在普通 x86 机器上使用 linux CLI 的体验,Node.js、MySQL、OpenCV、Nginx 等等在阿里云上怎么用,在这个几十块的物联网小模块上也怎么用。稳定性超强,几年不死机也是正常的。
ESP32 的主要参数为:Tensilica LX6 CP,主频 240 MHz,内存为 520KB SRAM,可配置 4MB 以上的存储空间,芯片内嵌 WiFi 和蓝牙以及 BLE,有足够的外围接口,并能够适应工业级的使用温度。
这几颗芯片共同的特征是计算资源和通讯能力以及接口资源相对于传统 MCU 来说有足够的富余,并保持在同样的价位。因此,在这类芯片上,有足够的资源做抽象化的封装和开发框架实施。我们看到除了这几颗芯片原厂提供的传统嵌入式开发包之外,社区和其他厂商已经在这几颗芯片上加快了新开发技术的实现。
#### 开发技术
物联网设备端开发技术目前有两个比较大的发展方向,一是统一化的物联网操作系统,二是统一化的物联网开发框架。他们共同的目的是形成“软件定义物联网”,与传统从芯片选型开始的,着陆于原厂 SDK 中完成应用开发,与需求和产品设计汇合的流程完全相反,希望从需求和产品设计入手,通过公开统一的软件构架完成开发,再根据开发使用到的资源去落地芯片和外围设备。这样做的好处主要在于提高开发效率和形成可以复用的应用代码。
##### 操作系统
虽然市场上存在的设备端操作系统有数十种之多,但是我们看到活跃的,明显向“软件定义物联网”方向发展的有三家:
- Zephyr
Zephyr 是 Linux 基金会于2016年2月发布的物联网操作系统,背后主要的支持力量来自于 ARM 和 Linaro,具有目前嵌入式小型实时操作系统的普遍特征,比如:轻量到 KB 级的最小系统内存占用,支持多种芯片构架:从 ARM Cortex-M、Intel x86、ARC(DSP 内核)、NIOS II(FPGA 软核)到开源的 RISC V 等,跟 Linux 一样的模块化内核组织方式,如图2所示。
Zephyr 目前已经升级到 V1.7 版本,逐步向一个可以用到生产环境的系统靠拢了。Zephyr 最大的特色并不在于其完备性而在于其开发理念完全来自于“软件定义物联网”,并且有很好的资源支持,在未来应该会有自己的位置。
<img src="http://ipad-cms.csdn.net/cms/attachment/201706/592fe0dd7ecff.png" alt="图2 Zephyr物联网操作系统" title="图2 Zephyr物联网操作系统" />
图2 Zephyr 物联网操作系统
- RTthread
RTthread 是纯国产的小型操作系统,植根于中国的各种使用场景,10年来已经确立了自己的地位,在很多行业有自己的一席之地,目前社区非常活跃,核心团队以创业公司的形式推进,非常专注。技术上的特征作为一个成熟的系统,没有什么可以吐槽的地方。Zephyr 有的技术优势 RTT 都有,而且 RTT 在生产环境的装机量较为可观。
- 华为 LiteOS
华为是全球范围内物联网技术的根源厂商之一,LiteOS 是一个华为内部很多产品都在用的系统,目前也以开源的形式在全力推广。LiteOS 最大的优势在于华为很多根源技术将利用 LiteOS 进行输出,目前最大的例子就是即将全面商用的 NB-IoT 技术,设备端的开发包将会用 LiteOS 输出。
以上几个系统一致的特点包括小型化、芯片适应范围广、通信协议适配比较广泛等,他们也都是开源的系统,研发或推动力量比较活跃。有可能在物联网领域里的类似 Linux 地位的主流操作系统会是其中某个,也或许会一直都存在下去但是在技术上越来越趋同。
##### 开发框架
首先解释一下开发框架,开发框架可以小到是一个细节的工具,也可以大到规定开发的全部边界。最典型的例子是 Android,纯粹操作系统意义上,Android 是 Linux 的一个分支,但是从 App 开发角度,除 NDK 之外,没有任何与 Linux 打交道的地方,所以也把 Android 叫做操作系统。再广泛地看,Android 除了面向手机应用的开发框架,还准备了 Google play 这样的应用分发渠道,这是开发者生态建设。同理,我们看 Node.js 在后端的种种开发模式,也是将所有后端资源都封装到 JavaScript 里,开发时可以随时 npm install 各种包来 require,解决了代码复用问题。
因此我的观点是,开发框架以及背后的代码复用和开发者生态才是真正的操作系统。
目前在物联网领域,正在尝试向生产环境演进的开发框架基本都基于 JavaScript,而在小型实时操作系统上使用的 JavaScript runtime 目前也基本集中到了 JerryScript 上。JerryScript 是三星开发和开源的一个小资源占用的引擎,内存需要 64KB,存储需要 200KB 即可,能够实现完整的事件驱动,符合 ECMAScript 5.1。
如同前文所说,开发框架或是操作系统在当下需要包括以代码复用为目的的开发者生态,甚至需要包括应用分发,所以我们看到在 JerryScript 的基础上,有两家做这类工作的团队值得关注:
- WRTnode
WRTnode 是一个北京的开源硬件团队,提供从开发到硬件交付的全流程服务。他们最近开放的 node.system 和 noyun.io 即是着眼于实现物联网 JavaScript 的开发框架和开发者生态。在 WRTnode 的实现里,设备端的 JavaScript 开发已经变得像 cloud9.io 一样全案在线开发,为开发者屏蔽了嵌入式开发的繁琐编译烧写工作。
- Ruff
Ruff 是位于上海的创业公司,2015年开始一直在演进基于物联网设备端 JavaScript 的开发者生态,提供了较为可行的代码复用框架。目前他们已经开始服务商业客户,为物联网应用的快速实现提供了可能。
同时,Zephyr 和华为 LiteOS 也都有各自的 JavaScript runtime 发布计划。
以上我们看到了设备端开发的一些新的发展,目前这些新的设备端开发技术,已经逐步面向交付转移了。有理由相信经过一段时间的发展,面向效率的商业模式驱动下的物联网开发技术将迎来一大波更新,从而导向物联网应用的真正大发展。
\ No newline at end of file
此差异已折叠。
## 区块链在版权保护方面的探索与实践
文/朱志文
人类传播史上,经历了语言、书写、印刷、电子、互动等5次革命,区块链的出现将把人类带入价值传播的新时代。亿书(英文名 Ebookchain),是目前国内唯一一款专注于版权保护的区块链产品,本文通过简单介绍亿书产品的实现,分享区块链在版权保护方面的探索与实践。
### 版权保护的困局和传统方法的局限
随着互联网,特别是移动互联网的发展,数字出版已经形成较为完整的产业链,给网络作家等相关参与方带来可观的收入。但另一方面,侵权盗版制约着数字出版的进一步发展,各参与方都深受其害。特别是作者等内容生产商一直处于弱势地位,缺少相应的话语权和主导权,创作积极性倍受打击。面对这些问题,国家非常重视,各种政策和扶持计划频出,重拳解决版权保护难题,但是限于技术手段,很难从根本上解决。
传统的版权保护手段非常有限。历史上,有过使用邮戳实现版权保护的方法,即作者把写好的文稿,一式两份同时寄出,一份给出版机构,另一份邮寄给自己。当出现被盗用的情况时,就拿出自己手里的那一份作为诉讼的证据,因为邮戳时间一致、内容一致。到了今天,互联网时代崛起,免费分享盛行,版权保护一度被忽视。当引起人们足够重视的时候,却发现并没有十分可靠的办法,特别是在分享环节更是无能为力。 比如,人们熟知的 CSS/AACS、Key 2 Audio、Always-Online DRM 等比较知名的 DRM 技术,虽然有一定的保护作用,但屡屡被破解,也为分享带来壁垒,甚至演变成商家垄断的工具,引起用户,特别是支持正版的用户的强烈反感和抵触。
难道,真的如某些人所说,版权保护是无解的吗?非也。区块链技术的出现,给彻底解决版权保护顽疾带来了希望,更足可以让盗版无所遁形。目前,市场上已经出现一些基于区块连技术的解决方案,但多数是从传统角度切入的,其中亿书,是国内最先采用真正区块链技术,从创作、分享,到数字出版等各环节都有解决办法的综合解决方案。
### 区块链在版权保护上的主要特点
区块链基于数学原理解决了交易过程中的所有权确认问题,对价值交换活动的记录、传输、存储结果都是可信的。区块链记录的信息一旦生成将永久记录,无法篡改,除非能拥有全网络总算力的51%以上,才有可能修改最新生成的一个区块记录。
那么很显然,我们可以根据区块链的特点,结合版权保护的各个环节分别去理解下面三个方面的问题。
1. 如何进行版权注册?我们知道“可信时间戳”,由权威机构签发,能证明数据电文在一个时间点是已经存在的、完整的、可验证的,是一种具备法律效力的电子凭证。对于原创作品的登记,区块链技术可以非常方便地把时间戳与作者信息、原创内容等元数据一起打包存储到区块链上。而且,它打破了现在的从单点进入数据中心去进行注册登记的模式,可以实现多节点进入,方便快捷。
2. 如何解开版权确认的难题?所有涉及版权的使用和交易环节,区块链都可以记录下使用和交易痕迹,并且可以看到并追溯它们的全过程,直至最源头的版权痕迹。更主要的是,区块链所记录的版权信息是不可逆且不可篡改的。公开、透明、可追溯、无法篡改,保证了信息的真实可信,辅以简单易用的查询工具,版权确权就是非常简单的事情了。
3. 如何进行版权验证?区块链技术大量使用密码学技术,版权持有者在把作品写入区块链时,自动用自己的私钥对作品进行了数字签名,第三方可以用版权持有者的公钥对数字签名进行验证,如果作品的数字签名值验证通过,则表明此作品确实是版权持有者所有,因为只有版权持有者才有私钥能生成该签名值。另外,也可以使用杂凑密码算法 SHA256 计算作品的数字指纹,通过数字指纹比对验证版权情况。再者,辅以分布式检索等基于内容的技术手段,便可覆盖各类复杂验证情况。
很显然,区块链很轻松就能解决当前版权保护的注册、确权和验证问题。
### 亿书在版权保护上的基本实现
下面,我们拿亿书为例,来探讨区块链在版权保护方面的实现思路。
#### 基本的架构设计
从架构设计上来说,可以简单分为三个层次,协议层、扩展层和应用层。其中,协议层又可以分为存储层和网络层,它们相互独立但又不可分割,如图1所示。
<img src="http://ipad-cms.csdn.net/cms/attachment/201701/58660d588001b.png" alt="图1 架构设计" title="图1 架构设计" />
图1 架构设计
所谓的协议层,是指最底层的技术。这个层次类似于我们电脑的操作系统,它维护着网络节点。这个层次是一切的基础,构建了网络环境、搭建了交易通道、制定了节点奖励规则。扩展层类似于电脑的驱动程序,是为了让区块链产品更加实用,可以使用分布式存储、机器学习、物联网、大数据等技术,这个层面与应用层更加接近,也可以理解为 B/S 架构的产品中的服务端(Server)。这样不仅在架构设计上更加科学,让区块链数据更小,网络更独立,同时也可以保证扩展层开发不受约束。应用层类似于电脑中的各种软件程序,是真正面向普通用户的产品。
限于当前区块链技术的发展,只能从协议层出发,把目标指向应用层,同时为第三方开发者提供扩展层的强大支持。
#### 产品工具化
从上面的架构我们可以知道,面向用户的仅仅是一个客户端软件。当然,这个软件可以非常多样化。比方提供一个简单的写作工具,配合底层的协议,为作者提供一个从创作到发布,再到电子书出版的全流程解决方案。这样作者的整个创作过程都会被智能化的保存到区块链上去,一方面简化了操作,另一方面为作者打造了一个真正的自媒体平台。
#### 记录创作时间段
很多人都会有这样的疑惑,如果 A 写的一篇文章被 B 上传到区块链,那么这所谓的版权保护岂不是在保护盗版了。实际上,如果单一的注册备案功能的话,必然会存在这样的问题,区块链仅仅是一项技术,再强大也无法处理链外的数据信息。因此,最好的做法自然是让作者直接在链上工作,变记录单点时间戳为记录时间段,从而避免单点记录时元数据单一无法佐证的弊端。
亿书会忠实记录作者内容创作过程中的关键信息,把单一时间戳汇成时间段,写入区块链。对于那些被盗版直接上传的数字作品,自然有了更多的可以检索验证的条件和信息。
实践中,亿书对作者撰写的作品通过密码技术手段,使用椭圆曲线密码编码学(ECC)对作品进行数字签名,同时用杂凑密码算法(比如 SHA256 算法)生成作品的数字指纹,加上可信的时间戳以及作者真实姓名等信息,一起写入区块链,得到其他节点的确认,从而保证数据的可信及不可篡改。
#### 追踪流通全过程
互联网鼓励分享,版权保护也绝不应该是在封闭状态下的保护。在没有区块链存在的情况下,流通过程中的验证、取证是非常困难的事情,因为盗版信息随时都可能被修改、删除。另外,一般网站也不会提供技术验证的手段,所以给调查取证带来很大困难。无法取证,甚至取证不可信等都给维权带来极大不确定性。
<img src="http://ipad-cms.csdn.net/cms/attachment/201701/58660df1a6894.png" alt="图2 功能和产品" title="图2 功能和产品" />
图2 功能和产品
亿书基于区块链技术,很容易解决这个问题。一方面,在分享、交易等过程中,亿书会忠实记录下全部痕迹,并可轻松追溯它的全过程,直至源头。这一点,轻松解决了人工追溯的繁琐过程,而且正确性、可信度和取证效率都是不可比拟的。其次,区块链对原始信息使用了加密技术以及电子签名技术,从技术上对版权信息做进一步的验证处理,这为取证提供了更加有效的技术手段。第一,亿书基于 P2P 网络,任何一个地方,只要能够联网到亿书网络,就可以使用亿书工具进行直接地验证和取证,便利性大大提高。第二,区块链技术可以把人的力量发挥到极致。亿书优化奖惩规则,鼓励人们举报和反馈。技术上模糊不清的,就可以通过利益驱动,让人参与进来,目前也只有区块链技术可以做到,做得彻底。这为主动防御盗版提供了更加深入细致的方法手段,将盗版防范于无形 。
举个例子,很多人都玩过 Xbox360,大部分玩家都不愿意破解机器玩盗版游戏。原因是破解了 Xbox360,有可能被 Xbox Live 封帐号,再也无法联机玩多人游戏了。为了保证正版率,越来越多的游戏开发商也开始仿效微软的这一做法,不再重视单机游戏,将心血倾注到了多人联机游戏开发上,这里面除了技术层面的演进,还有用户利益层面的驱使。那么,文字、图片、音乐等,这类没有单机模式、联机模式一说的内容怎么办呢?显然,需要一个基于分布式网络和分布式存储的区块链产品。
### 当前背景下的误区
#### 区块链技术法律会认可吗?
互联网产品本来是技术上的实现,与简单的页面截图等证据手段相比,区块链的证信和可靠性是显而易见的,但是涉及到版权,有人质疑区块链技术是否被认可。对此,可以从两个方面给予明确的回答:
1. 工信部在2016年10月21日发布的《中国区块链技术和应用发展白皮书》中,“3.4区块链与文化娱乐”一节,专门描述了区块链技术如何用于版权保护,明确了区块链技术用于版权保护在司法取证中的作用。
2. 笔者在今年8月参加了“2016中国区块链产业大会”,其中专门有一个分会场,主题是《文体卷——支持文化金融发展》,该分会场的嘉宾包括国家知识产权局的相关领导,以及版权保护领域的各界专家、学者和从业人员。可以说,国家层面正在积极推动区块链在版权保护方面的应用。
#### 版权保护与免费分享矛盾吗?
很多人认为,互联网的存在应该是免费分享,版权保护是要收费或被封闭起来才能保护,那岂不是相互矛盾?实际上,“免费”不代表不需要版权保护。笔者写的《Node.js开发加密货币》通过互联网免费分享,任何人都可以免费阅读,如果有人分享这些文章用来盈利(商用),那就触犯了本人的版权保护协议,是可以通过法律途径追究他的法律责任的。
在2016区块链产业大会上,与会的国家版权局的领导是这样回答版权保护与免费分享问题的:
1. 从国家发展角度来说,版权不能得到保护,知识创新被任意免费分享,就会严厉打击创作者的积极性。一个国家、一个民族的创新基因将被吞噬,这个问题在目前的中国已经足够严重,必须出重拳、出奇招解决。
2. 从国外的情况来看,美国的版权保护做的最好,但是美国的网络发展也是最好的,事实上,绝对不会出现“版权保护严格,网络发展就会受限”,这种此消彼长的怪现象。相反,版权得到了保护,大家的创新热情更高,主动分享的意愿和动力反而更足。
3. 从概念上讲,版权保护,并不等于知识不分享,版权保护有很多种方法,国家也在鼓励各类创新技术应用。
#### 区块链技术提倡匿名与版权保护实名的要求不矛盾吗?
区块链是一项技术,匿名并非它的必要条件。版权保护是现实需求,实名则是必须的。确实很多项目是匿名分享,毕竟实名认证存在诸多壁垒,前期很容易让用户抵触。不过,无论要不要版权保护,首先都要尊重法律,尊重用户,保证不负责任的内容无法分享。当然,不提倡并不代表不可以,只是匿名分享会有诸多限制。
### 区块链技术自身的障碍与不足
从技术层面来说,当前区块链的处理能力普遍不高,存在瓶颈,我们通过优化已经大大提高了这一数值,但是仍然无法应对大规模的交易数据和交互流量,还需要进一步优化底层协议,扩展应用层开发。
其次,另一个技术难点是区块链主链的存储与分发技术。比特币的区块链大小已经接近 60G,普通用户使用一个全客户端,同步这么大的数据量要耗费很长的时间。任何一款区块链产品,也都存在区块链数据不断膨胀的问题。亿书采取主链与侧链分离的架构设计,但在未来的某一天,仍然无法规避这个问题,这也需要不断的加以优化改进。
最后,区块链技术是集网络编程、分布式算法、密码学、数据存储技术等各类先进技术于一体的综合架构,技术难度大、人才培养周期长、高水平人才极度匮乏等等都是限制这个行业快速发展的重要因素。此外,当前区块链领域野蛮发展,劣币驱逐良币的现象不断上演,真正沉下心来做产品的团队,总是会面对多方面的压力和无脑黑,政策的监管和国家的扶持亟待尽快出台。加之,版权保护领域,涉及到创作者、使用者和出版发行机构等各参与方,要想快速发展,也需要国家政策层面的大力支持。
\ No newline at end of file
## 大数据引擎 Greenplum 那些事
文/周雷皓
本文介绍了大数据引擎 Greenplum 的架构和部分技术特点。从 GPDB 基本背景开始,在架构的层面上讲解 GPDB 系统内部各个模块的概貌,然后围绕 GPDB 的自身特性、并行执行和运维等技术细节,阐述了为什么选择 Greenplum 作为下一代的查询引擎解决方案。
### Greenplum 的 MPP 架构
Greenplum(以下简称 GPDB) 是一款开源数据仓库,基于开源的 PostgreSQL 改造而来,主要用来处理大规模数据分析任务。相比 Hadoop,Greenplum 更适合做大数据的存储、计算和分析引擎。
GPDB 是典型的 Master/Slave 架构,在 Greenplum 集群中,存在一个 Master 节点和多个 Segment 节点,每个节点上可以运行多个数据库。Greenplum 采用 shared nothing 架构(MPP),典型的 Shared Nothing 系统汇集了数据库、内存 Cache 等存储状态的信息,不在节点上保存状态的信息。节点之间的信息交互都是通过节点互联网络实现的。通过将数据分布到多个节点上来实现规模数据的存储,再通过并行查询处理来提高查询性能。每个节点仅查询自己的数据,所得到的结果再经过主节点处理得到最终结果。通过增加节点数目达到系统线性扩展。
图1为 GPD B 的基本架构,客户端通过网络连接到 gpdb,其中 Master Host 是 GP 的主节点(客户端的接入点),Segment Host 是子节点(连接并提交 SQL 语句的接口),主节点不存储用户数据,子节点存储数据并负责 SQL 查询,主节点负责相应客户端请求并将请求的 SQL 语句进行转换,转换之后调度后台的子节点进行查询,并将查询结果返回客户端。
<img src="http://ipad-cms.csdn.net/cms/attachment/201703/58b520f7d1e66.png" alt="图1 GPDB的基本架构" title="图1 GPDB的基本架构" />
图1 GPDB 的基本架构
#### Greenplum Master
Master 只存储系统元数据,业务数据全部分布在 Segments 上。其作为整个数据库系统的入口,负责建立与客户端的连接,SQL 的解析并形成执行计划,分发任务给 Segment 实例,并且收集 Segment 的执行结果。正因为 Master 不负责计算,所以 Master 不会成为系统的瓶颈。
Master 节点的高可用类似 Hadoop 的 NameNode HA,如图2,Standby Master 通过 synchronization process,保持与 Primary Master 的 catalog 和事务日志一致,当 Primary Master 出现故障时,Standby Master 承担 Master 的全部工作。
<img src="http://ipad-cms.csdn.net/cms/attachment/201703/58b52123ca3fa.png" alt="图2 Master节点的高可用" title="图2 Master节点的高可用" />
图2 Master 节点的高可用
#### Segments
Greenplum 中可以存在多个 Segment,Segment 主要负责业务数据的存储和存取(图3),用户查询 SQL 的执行时,每个 Segment 会存放一部分用户数据,但是用户不能直接访问 Segment,所有对 Segment 的访问都必须经过 Master。进行数据访问时,所有的 Segment 先并行处理与自己有关的数据,如果需要关联处理其他 Segment 上的数据,Segment 可以通过 Interconnect 进行数据的传输。Segment 节点越多,数据就会打的越散,处理速度就越快。因此与 Share All 数据库集群不同,通过增加 Segment 节点服务器的数量,Greenplum 的性能会成线性增长。
<img src="http://ipad-cms.csdn.net/cms/attachment/201703/58b5214eeef38.png" alt="图3 Segment负责业务数据的存取" title="图3 Segment负责业务数据的存取" />
图3 Segment 负责业务数据的存取
每个 Segment 的数据冗余存放在另一个 Segment 上,数据实时同步,当 Primary Segment 失效时,Mirror Segment 将自动提供服务。当 Primary Segment 恢复正常后,可以很方便地使用 gprecoverseg -F 工具来同步数据。
#### Interconnect
Interconnect 是 Greenplum 架构中的网络层(图4),也是 GPDB 系统的主要组件,它默认使用 UDP 协议,但是 Greenplum 会对数据包进行校验,因此可靠性等同于 TCP,但是性能上会更好。在使用 TCP 协议的情况下,Segment 的实例不能超过1000,但是使用 UDP 则没有这个限制。
<img src="http://ipad-cms.csdn.net/cms/attachment/201703/58b5218590a35.png" alt="图4 Greenplum网络层Interconnect" title="图4 Greenplum网络层Interconnect" />
图4 Greenplum 网络层 Interconnect
### Greenplum,新的解决方案
前面介绍了 GPDB 的基本架构,让读者对 GPDB 有了初步了解。下面对 GPDB 的部分特性进行了描述,可以很好地理解为什么选择 GPDB 作为新的解决方案。
#### 丰富的工具包,运维从此不是事儿
对比开源社区中其他项目在运维上面临的困难,GPDB 提供了丰富的管理工具和图形化的 web 监控页面,帮助管理员更好地管理集群,监控集群本身以及所在服务器的运行状况。
最近的公有云集群迁移过程中,impala 总查询段达到100的时候,系统开始变得极不稳定,后来在外援的帮助下发现是系统内核本身的问题,在恶补系统内核参数的同时,发现 GPDB 的工具也变相填充了我们的短板,比如提供了 gpcheck 和 gpcheckperf 等命令,用于检测 GPDB 运行所需的系统配置是否合理以及对相关硬件做性能测试。如下,执行 gpcheck 命令后,检测 sysctl.conf 中参数的设置是否符合要求,如果对参数的含义感兴趣,可以自行搜索学习。
```
[gpadmin@gzns-waimai-do-hadoop280 greenplum]$ gpcheck --host mdw
variable not detected in /etc/sysctl.conf: 'net.ipv4.tcp_max_syn_backlog'
variable not detected in /etc/sysctl.conf: 'kernel.sem'
variable not detected in /etc/sysctl.conf: 'net.ipv4.conf.all.arp_filter'
/etc/sysctl.conf value for key 'kernel.shmall' has value '4294967296' and expects '4000000000'
variable not detected in /etc/sysctl.conf: 'net.core.netdev_max_backlog'
/etc/sysctl.conf value for key 'kernel.sysrq' has value '0' and expects '1'
variable not detected in /etc/sysctl.conf: 'kernel.shmmni'
variable not detected in /etc/sysctl.conf: 'kernel.msgmni'
/etc/sysctl.conf value for key 'net.ipv4.ip_local_port_range' has value '10000 65535' and expects '1025 65535'
variable not detected in /etc/sysctl.conf: 'net.ipv4.tcp_tw_recycle'
hard nproc not found in /etc/security/limits.conf
soft nproc not found in /etc/security/limits.conf
```
另外在安装过程中,用其提供的 gpssh-exkeys 命令打通所有机器免密登录后,可以很方便地使用 gpassh 命令对所有的机器批量操作,如下演示了在 master 主机上执行 gpssh 命令后,在集群的五台机器上批量执行 pwd 命令。
```
[gpadmin@gzns-waimai-do-hadoop280 greenplum]$ gpssh -f hostlist
=> pwd
[sdw3] /home/gpadmin
[sdw4] /home/gpadmin
[sdw5] /home/gpadmin
[ mdw] /home/gpadmin
[sdw2] /home/gpadmin
[sdw1] /home/gpadmin
=>
```
诸如上述的工具 GPDB 还提供了很多,比如恢复 segment 节点的 gprecoverseg 命令,比如切换主备节点的 gpactivatestandby 命令等。这类工具让集群的维护变得很简单,当然我们也可以基于强大的工具包开发自己的管理后台,让集群的维护更加傻瓜化。
#### 查询计划和并行执行,SQL 优化利器
查询计划包括了一些传统的操作,比如扫表、关联、聚合、排序等。另外,GPDB 有一个特定的操作:移动(motion)。移动操作涉及到查询处理期间在 Segment 之间移动的数据。
下面的 SQL 是 TPCH 中 Query 1 的简化版,用来简单描述查询计划。
```
explain select
o_orderdate,
o_shippriority
from
customer,
orders
where
c_mktsegment = 'MACHINERY'
and c_custkey = o_custkey
and o_orderdate < date '1995-03-20'
LIMIT 10;
QUERY PLAN
-----------------------------------
Limit (cost=98132.28..98134.63 rows=10 width=8)
-> Gather Motion 10:1 (slice2; segments: 10) (cost=98132.28..98134.63 rows=10 width=8)
-> Limit (cost=98132.28..98134.43 rows=1 width=8)
-> Hash Join (cost=98132.28..408214.09 rows=144469 width=8)
Hash Cond: orders.o_custkey = customer.c_custkey
-> Append-only Columnar Scan on orders (cost=0.00..241730.00 rows=711519 width=16)
Filter: o_orderdate < '1995-03-20'::date
-> Hash (cost=60061.92..60061.92 rows=304563 width=8)
-> Broadcast Motion 10:10 (slice1; segments: 10) (cost=0.00..60061.92 rows=304563 width=8)
-> Append-only Columnar Scan on customer (cost=0.00..26560.00 rows=30457 width=8) Filter: c_mktsegment = 'MACHINERY'::bpchar
Settings: enable_nestloop=off
Optimizer status: legacy query optimizer
```
执行计划从下至上执行,可以看到每个计划节点操作的额外信息。
1. Segment 节点扫描各自所存储的 customer 表数据,按照过滤条件生成结果数据,并将自己生成的结果数据依次发送到其他 Segment;
2. 每个 Segment 上,orders 表的数据和收到的 rs 做 join,并把结果数据返回给 master。
上面的执行过程可以看出,GPDB 将结果数据给每个含有 orders 表数据的节点都发了一份。为了最大限度地实现并行化处理,GPDB 会将查询计划分成多个处理步骤。在查询执行期间,分发到 Segment 上的各部分会并行地执行一系列处理工作,并且只处理属于自己部分的工作。重要的是,可以在同一个主机上启动多个 postgresql 数据库进行更多表的关联以及更复杂的查询操作,单台机器的性能得到更加充分的发挥。
**如何查看执行计划**
如果一个查询表现出很差的性能,可以通过查看执行计划找到可能的问题点。
1. 计划中是否有一个操作花费时间超长;
2. 规划期的评估是否接近实际情况;
3. 选择性强的条件是否较早出现;
4. 规划期是否选择了最佳的关联顺序;
5. 规划其是否选择性的扫描分区表;
6. 规划其是否合适地选择了 Hash 聚合与 Hash 关联操作。
#### 高效的数据导入,批量不再是瓶颈
前面提到,Greenplum 的 Master 节点只负责客户端交互和其他一些必要的控制,而不承担任何的计算任务。在加载数据的时候,会先进行数据分布的处理工作,为每个表指定一个分发列,接下来所有的节点同时读取数据,根据选定的 Hash 算法,将当前节点数据留下,其他数据通过 interconnect 传输到其他节点上去,保证了高性能的数据导入。通过结合外部表和 gpfdist 服务,GPDB 可以做到每小时导入 2TB 数据,在不改变 ETL 流程的情况下,可以从 impala 快速导入计算好的数据为消费提供服务。
使用 gpfdist 的优势在于其可以确保再度去外部表的文件时,GPDB 系统的所有 Segment 可以完全被利用起来,但是需要确保所有 Segment 主机可以具有访问 gpfdist 的网络。
#### 其他
1. GPDB 支持 LDAP 认证,这一特性的支持,让我们可以把目前 Impala 的角色权限控制无缝迁移到 GPDB;
2. GPDB 基于 Postgresql 8.2 开发,通过 psql 命令行工具可以访问 GPDB 数据库的所有功能,另外支持 JDBC、ODBC 等访问方式,产品接口层只需要进行少量的适配即可使用 GPDB 提供服务;
3. GPDB 支持基于资源队列的管理,可以为不同类型工作负载创建资源独立的队列,并且有效地控制用户的查询以避免系统超负荷运行。比如,可以为 VIP 用户、ETL 生产、任性和 adhoc 等创建不同的资源队列。同时支持优先级的设置,在并发争用资源时,高优先级队列的语句将可以获得比低优先级资源队列语句更多的资源。
最近在对 GPDB 做调研和测试,过程中用 TPCH 做性能的测试。通过和网络上其他服务的对比发现在5个节点的情况下已经有了很高的查询速度,但是由于测试环境服务器问题,具体的性能数据还要在接下来的新环境中得出,不过 GPDB 基于 postgresql 开发,天生支持丰富的统计函数,支持横向的线性扩展,内部容错机制,有很多功能强大的运维管理命令和代码。相比 impala 而言,显然在 SQL 的支持、实时性和稳定性上更胜一筹。
本文只是对 Greenplum 的初窥,接下来更深入的剖析以及在工作中的实践经验分享也请关注 DA 的 wiki。更多关于 Greenplum 基本的语法和特性,也可以参考 PostgreSQL 的官方文档。
\ No newline at end of file
## 安居客 Android 模块化探索与实践
文/张磊
>万维网发明人 Tim Berners-Lee 谈到设计原理时说过:“简单性和模块化是软件工程的基石;分布式和容错性是互联网的生命。”由此可见模块化之于软件工程领域的重要性。本文以安居客为例,分享笔者在模块化探索实践方面的一些经验。
### 前言
从2016年开始,模块化在 Android 社区被越来越多的被提及。随着移动平台的不断发展,移动平台上的软件体积也变得臃肿庞大,为了降低大型软件复杂性和耦合度,同时也为了适应模块重用、多团队并行开发测试等因素,模块化在 Android 平台上变得势在必行。阿里 Android 团队在年初开源了他们的容器化框架Atlas就很大程度说明了当前 Android 平台开发大型商业项目所面临的问题。
### 模块化
那么什么是模块化呢?《Java 应用架构设计:模块化模式与 OSGi》一书中对它的定义是:模块化是一种处理复杂系统分解为更好的可管理模块的方式。
上面这种描述太过生涩难懂,不够直观。下面这种类比的方式则可能加容易理解。
我们可以把软件看做是一辆汽车,开发一款软件的过程就是生产一辆汽车的过程。一辆汽车由车架、发动机、变数箱、车轮等一系列模块组成,同样一款大型商业软件也是由各个不同的模块组成。这些模块是由不同的工厂生产,一辆 BMW 的发动机可能是由位于德国的工厂生产,它的自动变数箱可能是 Jatco(世界三大变速箱厂商之一)位于日本的工厂生产,车轮可能是中国的工厂生产,最后交给华晨宝马的工厂统一组装成一辆完整的汽车。这就类似于我们在软件工程领域里说的多团队并行开发,最后将各个团队开发的模块统一打包成我们可使用的 App。
一款发动机、一款速箱都不可能只应用于一个车型,比如同一款 Jatco 的自动变速箱既可能被安装在 BMW 的车型上,也可能被安装在 Mazda 的车型上。这就如同软件开发领域里的模块重用。
到了冬天,我们需要将汽车的公路胎升级为雪地胎,轮胎可以很轻易地更换。这就是低耦合,一个模块的升级替换不会影响到其它模块,也不会受其他模块的限制;同时这也类似于我们在软件开发领域提到的可插拔。
### 模块化分层设计
上面的类比很清晰地说明了模块化带来的好处:
- 多团队并行开发测试;
- 模块间解耦、重用;
- 可单独编译打包某一模块,提升开发效率。
[《安居客 Android 项目架构演进》](http://blog.csdn.net/baron_leizhang/article/details/58071773)这篇文章中,我介绍了安居客 Android 端的模块化设计方案,这里还是用它来举例。但首先要对本文中的组件和模块做个区别定义:
- 组件:指的是单一的功能组件,如地图组件(MapSDK)、支付组件(AnjukePay)、路由组件( Router )等等;
- 模块:指的是独立的业务模块,如新房模块(NewHouseModule)、二手房模块(SecondHouseModule)、即时通讯模块(InstantMessagingModule)等;模块相对于组件来说粒度更大。
<img src="http://ipad-cms.csdn.net/cms/attachment/201705/58ff0440235fa.png" alt="图1 组件与模块设计方案" title="图1 组件与模块设计方案" />
图1 组件与模块设计方案
具体设计方案如图1所示,整个项目分为三层,从下至上分别是:
- Basic Component Layer:基础组件层,顾名思义就是一些基础组件,包含了各种开源库以及和业务无关的各种自研工具库;
- Business Component Layer:业务组件层,这一层的所有组件都是业务相关的,例如上图中的支付组件 AnjukePay、数据模拟组件 DataSimulator 等等;
- Business Module Layer:业务 Module 层,在 Android Studio 中每块业务对应一个单独的 Module。例如安居客用户 App 我们就可以拆分成新房 Module、二手房 Module、IM Module 等等,每个单独的 Business Module 都必须准遵守我们自己的 MVP 架构。
我们在谈模块化的时候,其实就是将业务模块层的各个功能业务拆分层独立的业务模块。所以我们进行模块化的第一步就是业务模块划分,但是模块划分并没有一个业界通用的标准,因此划分的粒度需要根据项目情况进行合理把控,这就需要对业务和项目有较为透彻的理解。拿安居客来举例,我们会将项目划分为新房模块、二手房模块、IM 模块等等。
每个业务模块在 Android Studio 中都是一个 Module,因此在命名方面我们要求每个业务模块都以 Module 为后缀,如图2所示。
<img src="http://ipad-cms.csdn.net/cms/attachment/201705/58ff0436210ec.png" alt="图2 以Module为后缀命名" title="图2 以Module为后缀命名" />
图2 以 Module 为后缀命名
对于模块化项目,每个单独的 Business Module 都可以单独编译成 APK。在开发阶段需要单独打包编译,项目发布的时候又需要它作为项目的一个 Module 来整体编译打包。简单地说就是开发时是 Application,发布时是 Library。因此需要在 Business Module 的 build.gradle 中加入如下代码:
```
if(isBuildModule.toBoolean()){
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}
```
同样 Manifest.xml 也需要有两套:
```
sourceSets {
main {
if (isBuildModule.toBoolean()) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/release/AndroidManifest.xml'
}
}
}
```
如图3所示:
<img src="http://ipad-cms.csdn.net/cms/attachment/201705/58ff042760ba9.png" alt="图3 两套Manifest.xml" title="图3 两套Manifest.xml" />
图3 两套 Manifest.xml
```
debug模式下的AndroidManifest.xml :
<application ...="">
<activity android:name="com.baronzhang.android.newhouse.NewHouseMainActivity" android:label="@string/new_house_label_home_page">
<intent-filter>
<action android:name="android.intent.action.MAIN">
<category android:name="android.intent.category.LAUNCHER">
</category></action></intent-filter>
</activity>
</application>
```
realease 模式下的 Android Manifest.xml:
```
<application ...="">
<activity android:name="com.baronzhang.android.newhouse.NewHouseMainActivity" android:label="@string/new_house_label_home_page">
<intent-filter>
<category android:name="android.intent.category.DEFAULT">
<category android:name="android.intent.category.BROWSABLE">
<action android:name="android.intent.action.VIEW">
<data android:host="com.baronzhang.android.newhouse" android:scheme="router">
</data></action></category></category></intent-filter>
</activity>
</application>
```
同时针对模块化我们也定义了一些自己的游戏规则:
- 对于 Business Module Layer,各业务模块之间不允许存在相互依赖关系,它们之间的跳转通讯采用路由框架 Router 来实现(后面会介绍 Router 框架的实现);
- 对于 Business Component Layer,单一业务组件只能对应某一项具体的业务,个性化需求对外部提供接口让调用方定制;
- 合理控制各组件和各业务模块的拆分粒度,太小的公有模块不足以构成单独组件或者模块的,我们先放到类似于 CommonBusiness 的组件中,在后期不断的重构迭代中视情况进行进一步的拆分;
- 上层的公有业务或者功能模块可以逐步下放到下层,合理把握好度就好;
- 各 Layer 间严禁反向依赖,横向依赖关系由各业务
Leader 和技术小组商讨决定。
### 模块间跳转通讯(Router)
对业务进行模块化拆分后,为了使各业务模块间解耦,因此各个 Bussiness Module 都是独立的模块,它们之间是没有依赖关系。那么各个模块间的跳转通讯如何实现呢?
比如业务上要求从新房的列表页跳转到二手房的列表页,那么由于是 NewHouseModule 和 SecondHouseModule 之间并不相互依赖,我们通过想如下这种方式实现 Activity 跳转显然是不可能的实现的。
```
Intent intent = new Intent(NewHouseListActivity.this, SecondHouseListActivity.class);
startActivity(intent);
```
有的同学可能会想到用显示跳转来实现:
```
Intent intent = new Intent(Intent.ACTION_VIEW, "<scheme>://<host>:<port>/<path>");
startActivity(intent);</path></port></host></scheme>
```
但是这种代码写起来比较繁琐,且容易出错,出错也不太容易定位问题。因此一个简单易用、解放开发的路由框架是必须的了。我自己实现的路由框架分为路由(Router)和参数注入器(Injector)两部分,如图4所示。
<img src="http://ipad-cms.csdn.net/cms/attachment/201705/58ff0418e7131.png" alt="图4 路由框架" title="图4 路由框架" />
图4 路由框架
#### Router
路由(Router)部分通过 Java 注解结合动态代理来实现,这一点和 Retrofit 的实现原理是一样的。
首先需要定义我们自己的注解(篇幅有限,这里只列出少部分源码)。
用于定义跳转 URI 的注解 FullUri:
```
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FullUri {
String value();
}
```
用于定义跳转传参的 UriParam(UriParam 注解的参数用于拼接到 URI 后面):
```
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface UriParam {
String value();
}
```
用于定义跳转传参的 IntentExtrasParam(IntentExtrasParam 注解的参数最终通过 Intent 来传递):
```
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface IntentExtrasParam {
String value();
}
```
然后实现 Router,内部通过动态代理的方式来实现 Activity 跳转:
```
public final class Router {
...
public <t> T create(final Class<t> service) {
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class[]{service}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
FullUri fullUri = method.getAnnotation(FullUri.class);
StringBuilder urlBuilder = new StringBuilder();
urlBuilder.append(fullUri.value());
//获取注解参数
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
HashMap<string, object=""> serializedParams = new HashMap<>();
//拼接跳转 URI
int position = 0;
for (int i = 0; i < parameterAnnotations.length; i++) {
Annotation[] annotations = parameterAnnotations[i];
if (annotations == null || annotations.length == 0)
break;
Annotation annotation = annotations[0];
if (annotation instanceof UriParam) {
//拼接 URI 后的参数
...
} else if (annotation instanceof IntentExtrasParam) {
//Intent 传参处理
...
}
}
//执行Activity跳转操作
performJump(urlBuilder.toString(), serializedParams);
return null;
}
});
}
...
}</string,></t></t>
```
上面是 Router 实现的部分代码,在使用 Router 来跳转的时候,首先需要定义一个 Interface(类似于 Retrofit 的使用方式):
```
public interface RouterService {
@FullUri("router://com.baronzhang.android.router.FourthActivity")
void startUserActivity(@UriParam("cityName")
String cityName, @IntentExtrasParam("user") User user);
}
```
接下来我们就可以通过如下方式实现 Activity 的跳转传参了:
```
User user = new User("张三", 17, 165, 88);
routerService.startUserActivity("上海", user);
```
Injector
参数注入器(Injector)部分通过 Java 编译时注解来实现,实现思路和 ButterKnife 这类编译时注解框架类似。
首先定义我们的参数注解 InjectUriParam :
```
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface InjectUriParam {
String value() default "";
}
```
然后实现一个注解处理器 InjectProcessor,在编译阶段生成获取参数的代码:
```
@AutoService(Processor.class)
public class InjectProcessor extends AbstractProcessor {
...
@Override
public boolean process(Set<!--? extends TypeElement--> set, RoundEnvironment roundEnvironment) {
//解析注解
Map<typeelement, targetclass="">
targetClassMap = findAndParseTargets(roundEnvironment);
//解析完成后,生成的代码的结构已经有了,它们存在InjectingClass中
for (Map.Entry<typeelement, targetclass=""> entry : targetClassMap.entrySet()) {
...
}
return false;
}
...
}</typeelement,></typeelement,>
```
使用方式类似于 ButterKnife,在 Activity 中我们使用 Inject 来注解一个全局变量:
```
@Inject User user;
```
然后 onCreate 方法中需要调用 inject(Activity activity) 方法实现注入:
```
RouterInjector.inject(this);
```
这样我们就可以获取到前面通过 Router 跳转的传参了。
由于篇幅限制,加上为了便于理解,这里只贴出了极少部分 Router 框架的源码。希望进一步了解 Router 实现原理的可以到 GiuHub 去翻阅源码, Router 的实现还比较简陋,后面会进一步完善功能和文档,之后也会有单独的文章详细介绍。
### 问题及建议
#### 资源名冲突
对于多个 Bussines Module 中资源名冲突的问题,可以通过在 build.gradle 定义前缀的方式解决:
```
defaultConfig {
...
resourcePrefix "new_house_"
...
}
```
而对于 Module 中有些资源不想被外部访问的,我们可以创建 res/values/public.xml,添加到 public.xml 中的 resource 则可被外部方位,未添加的则视为私有:
```
<resources>
<public name="new_house_app_name" type="string">
</public></resources>
```
#### 重复依赖
模块化的过程中我们常常会遇到重复依赖的问题,如果是通过 aar 依赖,gradle 会自动帮我们找出新版本,而抛弃老版本的重复依赖。如果是以 project 的方式依赖,则在打包的时候会出现重复类。对于这种情况我们可以在 build.gradle 中将 compile 改为 provided,只在最终的项目中 compile 对应的 project;
其实从前面的安居客模块化设计图上能看出来,我们的设计方案能一定程度上规避重复依赖的问题。比如我们所有的第三方库的依赖都会放到 OpenSoureLibraries 中,其他需要用到相关类库的项目,只需要依赖 OpenSoureLibraries 就好了。
#### 模块化过程中的建议
对于大型的商业项目,在重构过程中可能会遇到业务耦合严重,难以拆分的问题。我的建议是不急着将各业务模块拆分成不同的 module,可以先在原来的项目中根据业务分包,在一定程度上将各业务解耦后拆分到不同的 package 中。比如之前新房和二手房由于同属于 APP module,因此他们之前是通过隐式的 intent 跳转的,现在可以先将他们改为通过 Router 来实现跳转。又比如新房和二手房中公用的模块可以先下放到 Business Component Layer 或者 Basic Component Layer 中。在这一系列工作完成后再将各个业务拆分成多个 module。
又如前面提到的,太小的公有模块不足以构成单独组件或者模块的,我们先放到类似于 CommonBusiness 的组件中,在后期不断的重构迭代中视情况进行进一步的拆分。
模块化示例项目 ModularizationProject<a href="https://github.com/BaronZ88/ModularizationProject "> 源码地址</a>
\ No newline at end of file
## 微软百度阿里三大物联网云平台探析
文/刘洪峰
>风起云涌的物联网,随着国内外大公司的入局,形式也逐渐明朗起来。物联网不仅仅是硬件接入的一个网,还是接入后,大数据的存储、分析和呈现,以及人工智能技术的深度介入,对各类企业的生产、运维、管理带来的改变。本篇文章以微软的 Azure 云、百度的物接入及物解析云平台、阿里的物联网开发套件为切入点,深入介绍相关物联网平台的技术特色,技术路线。希望能给物联网从业者一些参考和启示。
云山雾罩的物联网随着国内外一些大公司的大力推进,面目日渐清晰。今年年初笔者因项目的关系深入了解当前主流的物联网云平台,又有了不同的感悟。在细说这几个物联网云平台之前,笔者先简单介绍一下如今的物联网。
现在的物联网,必不可少的三要素分别是:云、手机和智能硬件。例如,当前现象级应用摩拜单车就是一个典型案例。
- 智能硬件的作用,一是控制车锁的开启;二是获取当前 GPS 坐标;三是和云端通信,发送位置、车锁状态信息和接收云端指令;
- 手机就是实现用户管理、扫码和位置呈现等功能;
- 云的主要作用是数据接入,指令发出。另外一个重要功能也许是大数据分析,比如车共享频次,故障收集分析等等。
以上结构可以称之为是当前典型物联网应用,是智能硬件和云结合的一个最佳范例。产品功能简单明确,利于复制上量。有了量,也便于大数据分析。智能家居一些应用,其实也可以按这种类似的模式去经营实现。如小米不到千元的智能家居套件,在我亲身试用的大半年里,整体感觉还是非常不错的。
在前几年,智能硬件比较火的时候,第三方云平台,也可以说是智能硬件云平台也非常热络,比如 Yeelink、机智云等。不过去年年底咨询 Yeelink 创始人姜兆宁的时候,他表示这种模式已经很难持续,目前是专注做 Yeelight。机智云是国内比较有影响的第三方物联网云平台,我也曾和其北京的团队有过深入交流,对于物联网云平台对接第三方硬件,发展并不如想象的那么顺利。
从摩拜单车、小米智能家居到 Yeelink、Yeelight 和机智云,似乎隐约告诉我们,智能硬件和云平台紧密结合,做成一个封闭的私有的体系,才更有价值。
那问题来了,微软云、百度云、阿里云做公共物联网云平台,其价值点又在哪里?和以前出现的物联网云平台有什么异同?
都说2016是物联网元年,在这个年头的三月份,微软 Azure 平台的 IoT Hub 开始支持 MQTT,百度差不多也是在这个时候推出了基于 MQTT 的物联网平台,阿里是在下半年推出了基于 MQTT 协议的物联网开发套件(亚马逊、华为、腾讯也各有很好的物联网云平台,在此就不一一展开说明)。
这里不得不提一下 MQTT(Message Queuing Telemetry Transport,消息队列遥测传输),是 IBM 公司1999年开发出来的通信技术。最大的特点是消息质量可以分三种:最多一次,最少一次和仅有一次(本文中所述的三种物联网平台,第三种消息质量“仅有一次”当前是不支持的)。另外 MQTT 不仅可以构建在 TCP/IP 协议栈之上,目前百度和阿里云的物联网平台也支持基于 Web Socket 构建。
以前的物联网云平台在笔者眼中更像一个大应用平台,而不是一个基础平台,类似工控中的组态软件,把物理上的一个个参数,抽象为一个个 I/O 变量,比如布尔型的开关、浮点型的温湿度、整型的灯光亮度、当然还包括一些二进制数据的摄像头数据。这种架构,其实比较适合参变量相对少的智能家居及智能硬件。但是对比较复杂的工控类应用来说,如果每种数据都抽象为一个 I/O 点,那么都需要配置,适用性就不那么强了。现如今的三大物联网平台,就是把硬件和云端通信进行了简化,即数据上传和下发。正是因为这种机制,反而通用性更强了。
换而言之,以前的物联网云平台更在意接入环节,重在通道。而现在的物联网云平台,接入仅仅只是其中的一环而已。
### 微软 Azure 云平台
微软的云平台其实提供了全方位的物联网服务,如图1所示。
<img src="http://ipad-cms.csdn.net/cms/attachment/201704/58dcb0dee1fca.png" alt="图1 微软云平台物联网服务" title="图1 微软云平台物联网服务" />
图1 微软云平台物联网服务
数据采集环节支持三种方式,Event Hubs、Service Bus 和 IoT Hub。其中 IoT Hub 支持三种通信协议 HTTPS、AMQP 和 MQTT,对 Azure 云来说,三种协议不需要预先在云中设定,自适应。从应用的角度来看,HTTPS、AMQP 和 MQTT 三种协议没有太大的区别,同时微软也是刻意隐藏了三种通信的区别,总体来说就是数据上传和数据下发。不过这里需要指出的是,针对数据下发而言 HTTPS 的代价还是比较高的,需要不断请求服务器,以获取数据下发的内容。
接下来从数据流的角度来看 Azure 云服务,如图2所示。
<img src="http://ipad-cms.csdn.net/cms/attachment/201704/58dcb0d6b8938.png" alt="图2 数据流角度解读Azure云服务" title="图2 数据流角度解读Azure云服务" />
图2 数据流角度解读 Azure 云服务
从这两图可以看出,微软云平台的接入仅仅是其中一个环节。更为重要的是数据存储、分析,还有展现。特别是数据和分析部分,是大数据的基础,后续所谓的人工智能会基于这些环节发挥重要作用。
<img src="http://ipad-cms.csdn.net/cms/attachment/201704/58dcb0c831f61.png" alt="图3 百度云平台服务" title="图3 百度云平台服务" />
图3 百度云平台服务
### 百度物联网云平台
百度物联网云平台分为物接入 IoT Hub、物解析 IoT Parser 和物管理 IoT Device 等。事实上,百度物联网云平台和微软类似,其重点也并非接入环节,而是其重金下注的人工智能部分。
从上图可以看出,数据采集后的存储、处理、分析环节也是百度的重点,在这个环节,人工智能技术可以融入进来。
百度物联网平台虽然和微软一样,也支持 MQTT,但是与微软的不同之处在于,百度号称支持原生 MQTT。即 MQTT 协议不仅仅是一个通信信道了,而是充分发挥了 MQTT 本身的优势,比如信息的发布/订阅(微软的信息发布和订阅是固定的,单一的)。但是这种灵活性,个人认为有些粗糙了。这对基于该平台开发的用户来说,需要比较强的规划能力,否则很容易造成信息风暴。
此外值得一提的是,微软的云必须是 SSL 加密才能运行云和端通信,但是百度物联网云并不强制用户一定加密。
<img src="http://ipad-cms.csdn.net/cms/attachment/201704/58dcb0bc813db.png" alt="图4 百度物联网平台" title="图4 百度物联网平台" />
图4 百度物联网平台
### 阿里的物联网开发套件
阿里似乎比较低调,其物联网平台称之为物联网套件 IoT Kit。和微软、百度物联网平台一样,也是支持 MQTT 通信协议。不过相对于微软的封装和百度的完全开放不同,阿里的物联网套件平台做了半封装,比如发布和订阅和微软一样,预先定义了一些关键字,并且除此之外还可以自定义。可以说是介于微软和百度之间的一种模式。并且其通信加密要求是最高的,SSL 的版本必须是 TLSV1.1 或 TLSV1.2 版本。
和微软及百度相比,阿里的物联网平台稍有一些简单,其重点一是接入,二是数据导出。提供了相对丰富的 API 对外接口,对有些智能硬件厂商来说,是一个好消息,相当于阿里提供了一个云端 API 接口,方便和第三方合作方进行系统级别的开发合作。
<img src="http://ipad-cms.csdn.net/cms/attachment/201704/58dcb0b431f6f.png" alt="图5 阿里物联网套件" title="图5 阿里物联网套件" />
图5 阿里物联网套件
### 三大平台对比
从开发的角度来看,微软的物联网云平台 SDK 最丰富完善,提供了各种示例,有设备端的、有网关、有云端等。百度相对小气,其 MQTT 的 SDK 就是百度物联网平台的 SDK 了。阿里的物联网平台也是介于二者之间,特别是在设备端提供了一些基于芯片层面的接入源码,另外 API 接口部分也提供不少示例。
通过以上介绍,我们之前提到的另外一个问题的答案就昭然若揭了。
微软云、百度云和阿里云等公司做公共物联网云平台,其价值点在于数据采集后的价值,及基于大数据分析下的各种衍生价值。换句话说未来大数据的“金矿”的价值,在于如何挖掘和利用。基于这一点,微软和百度似乎走在了前列。
谈及此处,笔者一直秉持的理念也逐渐清晰起来,做有影响力的云平台,还是要靠大公司,而不是自己再去造轮子。站在巨人肩上,去成就另外一个层面的伟大。
所以在物联网飞速发展的时代,我的重点放在了设备端。从2001就开始从事工控领域的我,绝不会把物联网云平台下的端,仅仅抽象为一个设备,一个网关,其中个人认为这只是冰山一角而已,会有更为广阔的操作空间。
在物联网时代,云端有云端的机会,大数据挖掘有大数据挖掘的机会,设备端也有设备端的机会,就看如何去迎接这个新时代的到来了。
\ No newline at end of file
此差异已折叠。
此差异已折叠。
## 追求极简:Docker 镜像构建演化史
文/白明
>对于已经接纳和使用 Docker 技术在日常开发工作中的开发者而言,构建 Docker 镜像已经是家常便饭。但如何更高效地构建以及构建出 Size 更小的镜像却是很多 Docker 技术初学者心中常见的疑问,甚至是一些老手都未曾细致考量过的问题。本文将从一个 Docker 用户角度来阐述 Docker 镜像构建的演化史,希望能起到一定的解惑作用。
自从2013年 dotCloud 公司(现已改名为 Docker Inc)发布 Docker 容器技术以来,到目前为止已经有四年多的时间了。这期间 Docker 技术飞速发展,并催生出一个生机勃勃的、以轻量级容器技术为基础的庞大的容器平台生态圈。作为 Docker 三大核心技术之一的镜像技术在 Docker 的快速发展之路上可谓功不可没:镜像让容器真正插上了翅膀,实现了容器自身的重用和标准化传播,使得开发、交付、运维流水线上的各个角色真正围绕同一交付物,“test what you write, ship what you test”成为现实。
### 镜像:继承中的创新
谈镜像构建之前,我们先来简要说一下镜像。
Docker 技术从本质上说并不是一种新技术,而是将已有技术进行了更好地整合和包装。内核容器技术以一种完整形态最早出现在 Sun 公司的 Solaris 操作系统上,Solaris 是当时最先进的服务器操作系统。2005年 Sun 发布了 Solaris Container 技术,从此开启了内核容器之门。
2008年,以 Google 公司开发人员为主导实现的 Linux Container(即 LXC)功能在被 merge 到 Linux 内核中。LXC 是一种内核级虚拟化技术,主要基于 Namespaces 和 Cgroups 技术,实现共享一个操作系统内核前提下的进程资源隔离,为进程提供独立的虚拟执行环境,这样的一个虚拟的执行环境就是一个容器。本质上说,LXC 容器与现在的 Docker 所提供容器是一样的。 Docker 也是基于 Namespaces 和 Cgroups 技术之上实现的。但 Docker 的创新之处在于其基于 Union File System 技术定义了一套容器打包规范,真正将容器中的应用及其运行的所有依赖都封装到一种特定格式的文件中去,而这种文件就被称为镜像(即image),原理见图1(引自 Docker 官网)。
<img src="http://ipad-cms.csdn.net/cms/attachment/201712/5a25022aab2d8.png" alt="图1: Docker 镜像原理" title="图1: Docker 镜像原理" />
图1: Docker 镜像原理
镜像是容器的“序列化”标准,这一创新为容器的存储、重用和传输奠定了基础,并且容器镜像“坐上了巨轮”传播到世界每一个角落,助力了容器技术的飞速发展。
与 Solaris Container、LXC 等早期内核容器技术不同, Docker 还为开发者提供了开发者体验良好的工具集,这其中就包括了用于镜像构建的 Docker file 以及一种用于编写 Docker fil 的领域特定语言。采用 Docker file 方式构建成为镜像构建的标准方法,其可重复、可自动化、可维护以及分层精确控制等特点是采用传统采用 Docker commit 命令提交的镜像所不能比拟的。
### “镜像是个筐”:初学者的认知
“镜像是个筐,什么都往里面装”这句俏皮话可能是大部分 Docker 初学者对镜像最初认知的真实写照。这里我们用一个例子来生动地展示一下。
我们现在将 httpserver.go 这个源文件编译为 httpd 程序并通过镜像发布。源文件的内容如下:
```
//httpserver.go
package main
import (
"fmt"
"net/http"
)
func main() {
fmt.Println("http daemon start")
fmt.Println(" -> listen on port:8080")
http.ListenAndServe(":8080", nil)
}
```
接下来,我们来编写用于构建目标镜像的 Docker file:
```
// Dockerfile
From ubuntu:14.04
RUN apt-get update \
&& apt-get install -y software-properties-common \
&& add-apt-repository ppa:gophers/archive \
&& apt-get update \
&& apt-get install -y golang-1.9-go \
git \
&& rm -rf /var/lib/apt/lists/*
ENV GOPATH /root/go
ENV GOROOT /usr/lib/go-1.9
ENV PATH="/usr/lib/go-1.9/bin:${PATH}"
COPY ./httpserver.go /root/httpserver.go
RUN go build -o /root/httpd /root/httpserver.go \
&& chmod +x /root/httpd
WORKDIR /root
ENTRYPOINT ["/root/httpd"]
```
执行镜像构建:
```
# docker build -t repodemo/httpd:latest .
//...构建输出这里省略...
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
repodemo/httpd latest 183dbef8eba6 2 minutes ago 550MB
ubuntu 14.04 dea1945146b9 2 months ago 188MB
```
整个镜像的构建过程因环境而定。如果您的网络速度一般,这个构建过程可能会花费你10多分钟甚至更多。最终如我们所愿,基于 repodemo/httpd:latest 这个镜像的容器可以正常运行:
```
# docker run repodemo/httpd
http daemon start
-> listen on port:8080
```
一个 Docker file 产出一个镜像。Docker file 由若干 Command 组成,每个 Command 执行结果都会单独形成一个层(layer)。我们来探索一下构建出来的镜像:
```
# docker history 183dbef8eba6
IMAGE CREATED CREATED BY SIZE COMMENT
183dbef8eba6 21 minutes ago /bin/sh -c #(nop) ENTRYPOINT ["/root/httpd"] 0B
27aa721c6f6b 21 minutes ago /bin/sh -c #(nop) WORKDIR /root 0B
a9d968c704f7 21 minutes ago /bin/sh -c go build -o /root/httpd /root/h... 6.14MB
... ...
aef7700a9036 30 minutes ago /bin/sh -c apt-get update && apt-get... 356MB
.... ...
<missing> 2 months ago /bin/sh -c #(nop) ADD file:8f997234193c2f5... 188MB</missing>
```
我们去除掉那些 Size为0 或很小的 layer,我们看到三个 size 占比较大的 layer,见下图:
<img src="http://ipad-cms.csdn.net/cms/attachment/201712/5a2502054e362.png" alt="图2: Docker 镜像分层探索" title="图2: Docker 镜像分层探索" />
图2: Docker 镜像分层探索
虽然 Docker 引擎利用缓存机制可以让同主机下非首次的镜像构建执行得很快,但是在 Docker 技术热情催化下的这种构建思路让 Docker 镜像在存储和传输方面的优势荡然无存,要知道一个 Ubuntu-server 16.04的虚拟机 ISO 文件的大小也就不过600多 MB 而已。
### “理性的回归”:builder 模式的崛起
Docker 使用者在新技术接触初期的热情“冷却”之后迎来了“理性的回归”。根据上面分层镜像的图示,我们发现最终镜像中包含构建环境是多余的,我们只需要在最终镜像中包含足够支撑 httpd 运行的运行环境即可,而 base image 自身就可以满足。于是我们应该剔除不必要的中间层:
<img src="http://ipad-cms.csdn.net/cms/attachment/201712/5a2501e5c3866.png" alt="图3:去除不必要的分层" title="图3:去除不必要的分层" />
图3:去除不必要的分层
现在问题来了!如果不在同一镜像中完成应用构建,那么在哪里、由谁来构建应用呢?至少有两种方法:
- 在本地构建并 COPY 到镜像中;
- 借助构建者镜像(builder image)构建。
不过方法1本地构建有很多局限性,比如:本地环境无法复用、无法很好融入持续集成/持续交付流水线等。而借助 builder image 进行构建已经成为 Docker 社区的一个最佳实践,Docker 官方为此也推出了各种主流编程语言的官方 base image,包括 go、java、nodejs、python 以及 ruby 的等。借助 builder image 进行镜像构建的流程原理如图4。
<img src="http://ipad-cms.csdn.net/cms/attachment/201712/5a2501c86ac39.png" alt="图4:借助builder image进行镜像构建的流程图" title="图4:借助builder image进行镜像构建的流程图" />
图4:借助 builder image 进行镜像构建的流程图
通过原理图,我们可以看到整个目标镜像的构建被分为了两个阶段:
- 第一阶段:构建负责编译源码的构建者镜像;
- 第二阶段:将第一阶段的输出作为输入,构建出最终的目标镜像。
我们选择 golang:1.9.2 作为 builder base image,构建者镜像的 Docker file.build 如下:
```
// Dockerfile.build
FROM golang:1.9.2
WORKDIR /go/src
COPY ./httpserver.go .
RUN go build -o httpd ./httpserver.go
```
执行构建:
```
# docker build -t repodemo/httpd-builder:latest -f Dockerfile.build .
```
构建好的应用程序 httpd 放在了镜像 repodemo/httpd-builder 中的 /go/src 目录下,我们需要一些“胶水”命令来连接两个构建阶段,这些命令将 httpd 从构建者镜像中取出并作为下一阶段构建的输入:
```
# docker create --name extract-httpserver repodemo/httpd-builder
# docker cp extract-httpserver:/go/src/httpd ./httpd
# docker rm -f extract-httpserver
# docker rmi repodemo/httpd-builder
```
通过上面的命令,我们将编译好的 httpd 程序拷贝到了本地。下面是目标镜像的 Docker file:
```
// Dockerfile.target
From ubuntu:14.04
COPY ./httpd /root/httpd
RUN chmod +x /root/httpd
WORKDIR /root
ENTRYPOINT ["/root/httpd"]
```
接下来我们来构建目标镜像:
```
<script src="dialog.js"></script>
<script>
org.CoolSite.Dialog.init({ /* 传入配置 */ });
</script>
```
我们来看看这个镜像的“体格”:
```
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
repodemo/httpd latest e3d009d6e919 12 seconds ago 200MB
```
200MB,目标镜像的 Size 降为原来的1/2还多。
### “像赛车那样减去所有不必要的东西”:追求最小镜像
前面我们构建出的镜像的 Size 已经缩小到 200MB,但这还不够。200MB 的“体格”在我们的网络环境下缓存和传输仍然很难令人满意。我们要为镜像进一步减重,减到尽可能的小,就像赛车那样,为了能减轻重量将所有不必要的东西都拆除掉:我们仅保留能支撑我们的应用运行的必要库、命令,其余的一律不纳入目标镜像。当然不仅仅是 Size 上的原因,小镜像还有额外的好处,比如:内存占用小,启动速度快,更加高效;不会因其他不必要的工具、库的漏洞而被攻击,减少了“攻击面”,更加安全等。
<img src="http://ipad-cms.csdn.net/cms/attachment/201712/5a2501840b872.png" alt="图5 目标镜像还能更小些么?" title="图5 目标镜像还能更小些么?" />
图5 目标镜像还能更小些么
一般应用开发者不会从 scratch 镜像从头构建自己的 base image 以及目标镜像的,开发者会挑选适合的 base image。一些“蝇量级”甚至是“草量级”的官方 base image 的出现为这种情况提供了条件。
从图6看,我们可以有两个选择:busybox 和 alpine。
<img src="http://ipad-cms.csdn.net/cms/attachment/201712/5a25010eedaa0.png" alt="图6 一些base image的Size比较(来自imagelayers.io截图)" title="图6 一些base image的Size比较(来自imagelayers.io截图)" />
图6 一些 base image 的 Size 比较(来自 imagelayers.io 截图)
单从镜像的 size 上来说,busybox 更小。不过 busybox 默认的 libc 实现是 uClibc,而我们通常运行环境使用的 libc 实现都是 glibc,因此我们要么选择静态编译程序,要么使用 busybox:glibc 镜像作为 base image。
而 alpine image 是另外一种蝇量级 base image,它使用了比 glibc 更小更安全的 musl libc 库。不过和 busybox image 相比,alpine image 体积还是略大。除了因为 musl 比 uClibc 大一些之外,alpine 还在镜像中添加了自己的包管理系统 apk,开发者可以使用 apk 在基于 alpine 的镜像中添加需要的包或工具。因此,对于普通开发者而言,alpine image 是更佳的选择。不过 alpine 使用的 libc 实现为 musl,与基于 glibc 上编译出来的应用程序并不兼容。如果直接将前面构建出的 httpd 应用塞入 alpine,在容器启动时会遇到下面错误,因为加载器找不到 glibc 这个动态共享库文件:
standard_init_linux.go:185: exec user process caused "no such file or directory"
对于 Go 应用来说,我们可以采用静态编译的程序,但一旦采用静态编译,也就意味着我们将失去一些 libc 提供的原生能力,比如:在 linux 上,你无法使用系统提供的 DNS 解析能力,只能使用 Go 自实现的 DNS 解析器。
我们还可以采用基于 alpine 的 builder image,golang base image 就提供了 alpine 版本。接下来,我们就用这种方式构建出一个基于 alpine base image 的极小目标镜像。
我们新建两个用于 alpine 版本目标镜像构建的 Docker file: Docker file.build.alpine 和 Docker file.target.alpine:
```
//Dockerfile.build.alpine
FROM golang:alpine
WORKDIR /go/src
COPY ./httpserver.go .
RUN go build -o httpd ./httpserver.go
// Dockerfile.target.alpine
From alpine
COPY ./httpd /root/httpd
RUN chmod +x /root/httpd
WORKDIR /root
ENTRYPOINT ["/root/httpd"]
```
构建 builder 镜像:
```
http://ipad-cms.csdn.net/cms/article/code/3949
```
执行“胶水”命令:
```
# docker create --name extract-httpserver repodemo/httpd-alpine-builder
# docker cp extract-httpserver:/go/src/httpd ./httpd
# docker rm -f extract-httpserver
# docker rmi repodemo/httpd-alpine-builder
```
构建目标镜像:
```
# docker build -t repodemo/httpd-alpine -f Dockerfile.target.alpine .
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
repodemo/httpd-alpine latest 895de7f785dd 13 seconds ago 16.2MB
```
16.2MB,目标镜像的 Size 降为不到原来的十分之一,我们得到了预期的结果。
### “要有光,于是便有了光”:对多阶段构建的支持
至此,虽然我们实现了目标 Image 的最小化,但是整个构建过程却是十分繁琐,我们需要准备两个 Docker file、需要准备“胶水”命令、需要清理中间产物等。作为 Docker 用户,我们希望用一个 Docker file就能解决所有问题,于是就有了 Docker 引擎对多阶段构建(multi-stage build)的支持。注意:这个特性非常新,只有 Docker 17.05.0-ce 及以后的版本才能支持。
现在我们就按照“多阶段构建”的语法将上面的 Docker file.build.alpine 和 Docker file.target.alpine 合并到一个 Docker file 中:
```
//Dockerfile
FROM golang:alpine as builder
WORKDIR /go/src
COPY httpserver.go .
RUN go build -o httpd ./httpserver.go
From alpine:latest
WORKDIR /root/
COPY --from=builder /go/src/httpd .
RUN chmod +x /root/httpd
ENTRYPOINT ["/root/httpd"]
```
Docker file 的语法还是很简明和易理解的,即使是你第一次看到这个语法也能大致猜出六成含义。与之前 Dockefile 最大的不同在于在支持多阶段构建的 Docker file 中我们可以写多个“From baseimage”的语句,每个 From 语句开启一个构建阶段,并且可以通过“as”语法为此阶段构建命名(比如这里的 builder)。我们还可以通过 COPY 命令在两个阶段构建产物之间传递数据,比如这里的传递的 httpd 应用,这个工作之前我们是使用“胶水”代码完成的。
构建目标镜像:
```
# docker build -t repodemo/httpd-multi-stage .
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
repodemo/httpd-multi-stage latest 35e494aa5c6f 2 minutes ago 16.2MB
```
我们看到通过多阶段构建特性构建的 Docker Image 与我们之前通过 builder 模式构建的镜像在效果上是等价的。
### 来到现实
沿着时间的轨迹,Docker 镜像构建走到了今天。追求又快又小的镜像已成为了 Docker 社区的共识。社区在自创 builder 镜像构建的最佳实践后终于迎来了多阶段构建这柄利器,从此构建出极简的镜像将不再困难。
\ No newline at end of file
## OLTP 类系统数据结转最佳实践
文/王宝令
### 背景介绍
业务系统在长期运行的过程中会积累大量的数据,这些数据有些是需要长期保存的,例如一些订单数据,有些只需要短期保存,例如一些日志信息。业务数据一般都会有一个生命周期,生命周期内的我们叫生产数据,生命周期之外(即业务已经关闭)的叫历史数据,我们这里提到的数据结转,指的是将需要长期保存的历史数据从生产库迁移到历史库(转),而将需要短期保存的数据定期删除(结)。
我们已经进入了大数据时代,但在 OLTP 类系统中,关系型数据库依然占据主导地位,在关系型数据库中,如果不及时进行数据结转,会严重影响系统的性能。
关系型数据库单机容量有限,因此业界普遍的做法是进行垂直分库和水平分片,一些大型互联网企业由于业务量庞大,仅分片的集群规模就能达到上千节点,再加上分库的集群,规模非常巨大。传统的数据归档方法往往针对单库操作,难以处理如此大规模集群的数据归档。
同时,在大型互联网企业,每日的数据增长量非常大,数据结转的频率远大于传统行业,这些行业的 IT 系统往往是7*24小时不间断提供服务,而且全天24小时的并发量都很大,因此数据结转操作必须尽量减少对生产库的性能影响。
为此,我们自主研发了数据结转平台,以解决大数据背景下的数据结转问题。
### 技术架构
#### 设计要点
**尽量减少对生产库的影响**
数据结转操作没有复杂的业务逻辑,因此对数据库性能的影响主要体现在 IO 方面,减少对生产库的影响,最主要的就是减少对生产库的 IO 操作。目前我们采用的方案是通过从库查询数据,将数据插入历史库,然后再从主库中删除,如图1数据结转逻辑图所示,将查询的 IO 操作转嫁到从库上,可以大大减轻对主库的影响。为了保障数据库的高可用,业内基本都采用了主从部署模式,因此这个方案具有很高的通用性。
<img src="http://ipad-cms.csdn.net/cms/attachment/201703/58b658108625c.jpg" alt="图1 数据结转逻辑图" title="图1 数据结转逻辑图" />
图1 数据结转逻辑图
**支持分库分片集群**
我们希望数据结转平台的配置足够简单并且易于理解。在和用户的沟通过程中,我们发现他们最强烈的需求就是分库分片集群的数据结转。传统的单机数据结转操作可以抽象描述为:将数据库实例 A 中表 B 的历史数据结转到历史库 C,用户的配置主要有4个元素:生产库实例 A、结转表 B、结转条件和历史库。对于大规模的分库分片集群规模,如果采用传统单机数据结转的配置方式,每一个数据库实例都要配置4个元素,配置量非常大。
在我们的方案中,按照图2所示对数据库集群进行划分,将主库、从库、历史库作为一个结转单元,对于分片的数据库集群,表结构相同,我们将其作为一个分组,对于分库的集群,表结构不同则划分为不同的分组。用户进行配置的时候不是面向一个数据库实例,而是面向一个分组,数据结转操作抽象为:结转分组 X 中表 B 的历史数据,用户的配置元素有3个:分组 X、结转表 B 和结转条件。分组信息仅需配置一次。这样大大简化了用户的配置工作。
<img src="http://ipad-cms.csdn.net/cms/attachment/201703/58b6581a71fb0.jpg" alt="图2 数据库集群模型" title="图2 数据库集群模型" />
图2 数据库集群模型
**支持水平扩展**
由于数据库集群规模较大,数据结转平台应该具备水平扩展能力。我们采用的方案是将数据结转最核心的组件定时任务和数据库操作(数据结转执行器)独立出来,进行分布式部署。如图3所示,配置中心为用户的入口,用户通过配置中心定义数据结转任务,任务的关键属性包括:触发条件、执行条件、目标分组等,配置中心将结转任务分发给代理程序,同时对代理程序的执行状态进行监控。结转任务的触发条件配置在代理程序中的定时任务中,而执行条件和目标分组则作为数据结转执行器的执行参数。通过水平扩展代理程序,我们对更多的数据库进行结转。
<img src="http://ipad-cms.csdn.net/cms/attachment/201703/58b65825225cd.jpg" alt="图3 数据结转组件关系图" title="图3 数据结转组件关系图" />
图3 数据结转组件关系图
#### 总体架构
综合上面提到的3个设计要点,我们得到图4所示的总体架构,需要特别说明的是,对于水平分片的分组,我们采用的是多线程结转,对于不同结转单元不存在数据共享问题,所以无需考虑并发锁等问题。
<img src="http://ipad-cms.csdn.net/cms/attachment/201703/58b6582dcff09.jpg" alt="图4 数据结转总体架构图" title="图4 数据结转总体架构图" />
图4 数据结转总体架构图
### 一些经验总结
#### 配置中心与代理程序之间的信息同步
配置中心和代理程序在我们的方案中被设计为一种松耦合结构:在系统的运行过程中,代理程序宕机不会影响配置中心的运行,同样配置中心短暂的不可用也不会影响代理程序的运行。松耦合结构可以大大增强系统的可用性,而且配置中心、代理程序升级的时候不会影响整个系统的正常运行。
为了实现松耦合的结构,配置中心与代理程序之间的信息同步我们都是采用的异步处理,比如配置中心向代理程序分发结转任务,实际处理的时候我们采用的是拉的方式,而不是推的方式,我们在配置中心和代理程序之间维持了一个心跳,心跳的内容是代理程序负载的所有结转任务的校验码(该校验码在代理程序向配置中心发送心跳信息时由配置中心计算),当代理程序发现从配置中心得到的校验码和本地校验码不同时,则说明用户对结转任务进行了修改(包括新增、修改、删除),此时代理程序主动向配置中心发起同步结转任务的请求。这样做的好处是,代理程序在发生宕机重启后,会自动进行任务的同步。
#### 进度可视化
结转任务的进度在我们的方案中是实时汇总到配置中心的,我们称为进度可视化,代理程序通过一个独立的线程来异步处理进度可视化,一方面这样可以降低对结转任务性能的干扰,另一方面可以避免由于网络问题、配置中心暂时不可用等问题导致结转任务异常。进度可视化对于用户来说非常重要,用户在第一次定义结转任务并执行该任务的时候,进度可视化信息是用户和系统互动的唯一窗口,对用户来说是莫大的心理安慰。
#### 异常可视化
代理程序在执行数据结转任务时,会遇到各种异常信息,比如数据库 URL 配置错误,历史库生产库表结构不一致等,对于这些异常信息,除了在本地记录日志外,我们还将它们发送到了配置中心。将这些异常可视化,而不是让用户在大量的日志中去检索,这种方式非常便于在线问题的诊断。
#### 事务一致性
将生产库数据转到历史库本身是一个分布式的事务,在我们的方案中,不能保证数据的强一致性,比如在历史数据 Insert 到历史库的瞬间,用户修改了生产库的数据,我们的方案不会检测这种变化,会导致用户的修改并不会反映到历史库中,造成数据不一致。虽然在生产库中删除历史数据时,可以增加强一致性的校验,以解决这种问题,但是这样会对生产库造成一定的压力,同时考虑到这种情况发生的概率极低,因此并没有进行特殊处理。
历史数据 Insert 到历史库后,可能由于某种异常导致生产库执行 Delete 操作时失败,此时会造成数据冗余(生产库和历史库存在相同数据)。对于这种问题,我们的方案是利用 Redo Log(重做日志)机制,在结转任务重新执行时根据 Redo Log 恢复异常现场,纠正异常数据。
#### 结转数据的回滚
我们提供了一个数据回滚功能,可以将已经结转到历史库的数据逆向回滚到生产库,用户可以配置 Where 条件精确指定需要回滚的数据。有些特殊情况,业务上需要对已经结转的历史数据进行修改,该功能主要用于处理这种情况。同时在测试阶段,我们可以通过该功能快速恢复测试数据,方便对数据结转平台的测试。
#### 代理程序的自动升级
代理程序和配置中心本质上是一种典型的 C/S(客户端/服务端)结构,客户端是多实例部署,服务器端是集群部署,为了系统能够平滑地进行升级,我们需要对客户端的版本进行统一管理,同时我们提供了代理程序的自动升级功能,系统管理员可以通过配置中心对代理程序部署实例进行升级。自动升级功能,统一了代理程序的版本,使得我们可以不用被兼容性问题羁绊,是我们能够进行快速迭代开发有力支撑。
\ No newline at end of file
## 区块链技术在零售供应链的商业化应用
文/张作义
众所周知,区块链技术还处在不断完善和发展的阶段,成熟的商业化应用尚未大规模出现,国际上众多领先的IT互联网企业已积极投入到区块链技术的研究和推广工作之中,京东也已着手开展区块链的研究和应用工作。下面,我将着重为大家分享区块链在零售供应链领域的研究心得和应用畅想。
### 零售供应链所使用的技术和不足
零售供应链涉及商品选取、采购、定价、库存、销售、配送所涉及到的所有环节,往往表现出多主体、多区域、长时间跨度、大量交互协作的特征,而整个供应链运行过程中产生的各类信息被离散地保存在上下游企业各自的系统内,信息流不透明且极易被篡改,客户和买家缺少一种可靠的方法去验证及确认他们所购买的产品和服务的真正价值,这也就意味着他们支付的价格无法准确地反映产品的真实成本。同时,很难去对重点事件进行全程有效追踪或是对事故进行快速精准的调查。这是现有供应链一个典型的问题。
为了提升数据真实性和有效性,很多企业引入了自动化设备和系统进行信息的采集和存储,但由于是企业各自主导进行,信息基准不统一,很难进行有效协同,而且企业对自己的信息拥有100%的修改和删除权限,导致信息的可信度完全建立在企业自我合规管理能力和品牌信誉之上,存在严重的潜在性系统风险。另外,一旦发生事故导致企业数据完全丢失,将很难进行恢复。另一方面,为了进行供应链企业间的快速协同,很多企业也在通过 EDI 数据对接等方式开展信息交换,这首先就要求企业具备很高的数据对接能力和运维能力,对于目前中国众多的中小企业,甚至某些中大型企业来说,仍然颇具挑战。与此同时,企业间的数据对接只能是端到端逐个进行的,以京东为例,我们有超过10万合作商家,每个商家因其业务形态和对接需求的不同,随之开展的对接适配工作也不同,后期的运维工作量更是绵长而巨大。
### 区块链技术相比传统方式的优势
区块链技术有望以低成本、高效率的方式彻底解决以上问题,改变业务乃至机构的运作方式。区块链技术是利用块链式数据结构来验证与存储数据、利用分布式节点共识算法来生成和更新数据、利用密码学的方式保证数据传输和访问的安全、利用由自动化脚本代码组成的智能合约来编程和操作数据的一种全新的分布式基础架构与计算范式。
利用区块链技术,企业可以确保商品和交易信息的绝对安全,同传统方式相比,区块链的密码学特性和分布式存储特性可以确保信息无法被恶意篡改,通过设定查询权限,可以轻松实现数据加密和授权浏览,同时,一旦某个节点数据完全丢失,可以基于分布式账本实现数据的快速恢复,为企业内部信息管理提供有力的安全保障。区块链所具有的数据不可篡改和时间戳的存在性证明的特质能很好地运用于供应链溯源防伪。例如,可以用区块链技术进行奢侈品钻石身份认证及流转过程记录——为每一颗钻石建立唯一的电子身份并存放至区块链中。这颗钻石的来源出处、流转历史记录、归属以及所在地都会被忠实的记录在链,只要有非法的交易活动或是欺诈造假的行为,就会被侦测出来。此外,区块链技术也可用于生鲜、药品、艺术品等的溯源防伪。试想一下,未来我们在购物后,可以轻松的知道自己购买商品的全部真实可溯的信息,不必担心假冒伪劣、食品安全、囤积居奇,真正实现好物低价、买的放心、吃的安心。企业也可以通过消费者在区块链上的反馈,进行有针对性的服务改进,创造更好的服务体验。
在企业间协同方面,区块链技术同样发挥着革命性的重要作用。零售供应链的企业之间,可以通过区块链共享某些关键性共识信息,从而保证这些信息在供应链中的高度一致和可信赖性;基于云端的区块链,无需繁杂的数据对接和大量运维工作,即可轻松实现企业间的价值流转和高效协同;基于区块链的自动化智能合约可以实现给定供应商或经销商的合约条款数字化,使计算机可以在达到约定条件后自动执行程序,轻松实现支付货款、扣除罚金等工作。同时,因为区块链具有不可篡改、可追溯和基于密码学的公私钥安全体系,交易双方完全不用担心交易记录被恶意篡改、无法查询或被公开给不相关的第三方。
### 区块链技术在商业化实现中的难点与障碍
首先,区块链技术本身的发展还不成熟,特别是分布式账本和共识机制带来的算力消耗和存储空间大量占用的题,仍然需要找到更为有效的解决方案。
其次,作为新兴技术,要实现商业化,必须基于具体的应用场景,联合相关的企业、监督机构共同投入才能最终落地。重点和难点在于如何向参与的企业决策层展示区块链技术的优势和发展潜力,从而充分获得支持和投入;如何联合优质供应商共同参与区块链建设和高效运营工作,获得供应商的积极配合和信任。技术的发展离不开企业的商业化应用场景,更离不开广大科研机构和爱好者的持续投入、关注和宣传,任何能提升用户体验及成本效率的技术,都将拥有广阔的发展空间。
\ No newline at end of file
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册