1741 ℉

15.08.25

探寻Rust ownership系统

(Explore the ownership system in Rust)

原作者:Nercury 原文链接 探寻Rust ownership系统 Jan 19, 2015

这篇将由两部分组成的指南主要面向已了解Rust的基本语法和构建单元但是尚未领会ownershipborrowing的工作原理的读者.

首先我们会从简单的概念开始,随后慢慢了解更深入复杂的概念,直至深入理解完整的细节概念。这份指南假设读者对let, fn, struct, traitimpl有基本的了解。

我们的目标是在使用Rust进行开发时不会被ownership和borrowing所困扰。

首先从Ownership开始:

简介

和大家通常熟悉的语言类似,Rust基于Scope/stack的内存管理是非常直观的。

当下面的代码中的i在main函数返回时会发生什么?

fn main() {     
    let i = 5; 
}

它离开了他的生存范围进而被释放。

如果我把这个i传递给其它的函数例如foo,那么它会被释放多少次呢?

fn foo(i: i64) { 
    // another function foo
    // something 
}  
fn main() {
    let i = 5;
    foo(i);
    // call function foo 
}

对,它会被释放两次。第一次释放发生在函数foo结束的时候,另外一次发生在main函数结束的时候。如果你在foo函数中对它的值进行了修改,这个修改不会被反映到main函数中的值。

因为这个值在调用foo函数的过程中进行了一次拷贝。

与C++(以及一些其它的语言)相似,除了integer这样内置的类型,Rust还可以使用用户自定义的类型。这些类型的值可以在当前的stack上进行分配并且在函数返回离开其生命范围后得到释放(如果类型有destructor,那么destructor也会被调用)。

然而对于没有实现Copy Traits的类型来说,Rust编译器则会选择一个不同的基于ownership的规则来处理对象的生命周期。所以我们先来讨论一下Copy trait。

Copy Trait

实现Copy Traits可以使得用户自定义类型按照内置的integer类型一样来工作,每当发生拷贝赋值或者以值方式进行函数调用的时候,它的数据会被直接进行字节级别的拷贝复制来产生一个新的对象。

于很多其它的类型一样,内置类型i64(一种integer)也实现了这个Copy trait。

同样如果我们有一个自定义类型Info,我们也可以实现Copy使得我们的类型拥有可拷贝的语义:

struct Info {
    value: i64, 
}
impl Copy for Info {}

或者与上面功能上等同,我们也可以使用 #[derive(Copy)] 注解来达到一样的目的。

#[derive(Copy)]
struct Info {
    value: i64, 
}

没有实现Copy trait的类型在需要的时候则会遵循ownership原则来转移到其它的地方。

Ownership

Ownership规则确保不可拷贝的对象在任何一个时间只有一个拥有者并且只有这个拥有者可以对它进行修改。

所以如果一个函数负责释放某一个对象,那么它可以确定不会有任何一个其它的所有者会同时或者以后对同一个对象进行访问、修改或者删除。

现在我们已经有足够的基本了解了,让我们来开始动手试验吧。

Bob你好

为了演示对象的ownership是如何被转移的,我们来创建一个新的不实现Copy trait的类型Bob

struct Bob {
    name: String,
}

我们会在Bob的构造函数new中打印出它被构造的信息:

impl Bob {
    fn new(name: &str) -> Bob {
        println!("new bob {:?}", name); // announce
        Bob { name: name.to_string() }
    }
}

通过实现Drop::drop trait,我们在Bob被释放时打印出Bob的名称;

impl Drop for Bob {
    fn drop(&mut self) {
        println!("del bob {:?}", self.name);
    }
}

除此之外我们为Bob类型实现了内置的Debug::fmt trait来方便打印Bob对象的内容:

use std::fmt;

impl fmt::Debug for Bob {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "bob {:?}", self.name)
    }
}

开始测试吧!

当我们在main函数中创建一个Bob对象时,我们得到了预期的结果:

fn main() {
    Bob::new("A");
}

new bob "A"
del bob "A"

好了对象被释放了,但是它是在什么时候被释放的呢?

让我们在函数退出之前打印一行信息:

fn main() {
    Bob::new("A");
    println!("end is near");
}

new bob "A"
del bob "A"
end is near

原来对象是在函数返回之前被释放的。原因是返回值没有被赋予给任何局部变量,所以编译器在返回的Bob对象上直接调用了drop方法并释放了对象。

那么如果我们把返回值绑定到某一个局部变量上呢?

fn main() {
    let bob = Bob::new("A");
    println!("end is near");
}
new bob "A"
end is near
del bob "A"

当我们使用let来将返回值绑定到一个变量后,新创建的Bob对象在离开这个变量的范围,也就是函数退出的时候被释放。所以编译器的做法是在离开某一个范围后将所有属于这个范围的绑定变量进行释放。

要么释放要么转移

注意:一个对象可以被转移给其它的owner,一旦它被转移了它就不会被被转移前的作用域所有也不会被其释放。

那么怎么才能转移一个对象呢? 其实很简单,只要把对象以值的方式传递给其它函数。

现在让我们来试试把我们新建的Bob对象传递给black_hole函数吧:

fn black_hole(bob: Bob) {
    println!("imminent shrinkage {:?}", bob);
}

fn main() {
    let bob = Bob::new("A");
    black_hole(bob);
    println!("end is near");
}
new bob "A"
imminent shrinkage bob "A"
del bob "A"
end is near 

Try it yourself!

现在bob不再是在main函数退出的时候被释放了,它在black_hole函数返回的时候就已经被释放了!

但是让我们再想想,如果我们使用同样的Bob对象来调用black_hole函数两次会怎么样?

fn main() {
    let bob = Bob::new("A");
    black_hole(bob);
    black_hole(bob);
}
<anon>:35:16: 35:19 error: use of moved value: `bob`
<anon>:35     black_hole(bob);
                         ^~~
<anon>:34:16: 34:19 note: `bob` moved here because it has type `Bob`, which is non-copyable
<anon>:34     black_hole(bob);
                         ^~~

非常简单,编译器不允许我们使用一个已经被转移的对象,并且在出错的时候给出了非常明确而友好的错误信息。

没有魔法只有规则

与垃圾回收的情况下编译器需要在代码里到处跟踪对象的使用情况不同,Rust实现内存安全的方式不依赖于垃圾回收。Rust可以从当前函数范围内对对象的使用情况得出对象是否已经被释放的结论。

当你明白规则后,一切就很清晰了。到目前为止我们已经了解到了下面几个规则:

  • 不被使用的返回值会被立即释放
  • 所有使用let绑定的值如果不在中间被转移给其它所有者,这些值会在函数退出对象离开它们的存活范围时得到释放。

所以到目前为止我们了解的内存安全的基础是在一个时刻一个值只能有一个所有者。

然而我们之前所讨论的是只读绑定,当一个值可能被修改的情况下规则需要进行加强。

可变值的Ownership

一个值的拥有者可以对值进行修改,只要我们在使用let绑定变量是增加mut指令。比如我们可以对bob的name部分进行修改:

fn main() {
    let mut bob = Bob::new("A");
    bob.name = String::from_str("mutant");
}
new bob "A"
del bob "mutant"

我们先创建了一个name为“A”的对象,但是当释放它的时候它已经变成了“mutant”了。 如果我们把这个值传递给另外的函数mutate,我们同样可以将其标记为mut

fn mutate(value: Bob) {
    let mut bob = value;
    bob.name = String::from_str("mutant");
}

fn main() {
    mutate(Bob::new("A"));
}
new bob "A"
del bob "mutant"

由上可见在任何时候我们都可以将一个自己所有的对象变成可变的。

除了以let方式获得一个可变的绑定,我们还可以直接将函数的参数标记为可变的,它们的工作方式是一样的。所以之前的试验代码可以写的更短一些:

fn mutate(mut value: Bob) { // use mut directly before the arg name
    value.name = String::from_str("mutant");
}

修改一个可变绑定

当我们修改一个可变绑定的时候会发生什么呢?

fn main() {
    let mut bob = Bob::new("A");
    println!("");
    for &name in ["B", "C"].iter() {
        println!("before overwrite");
        bob = Bob::new(name);
        println!("after overwrite");
        println!("");
    }
}
new bob "A"

before overwrite
new bob "B"
del bob "A"
after overwrite

before overwrite
new bob "C"
del bob "B"
after overwrite

del bob "C"

老的值被释放,如果没有被重新赋值或者转移ownership,新赋值的值会在离开绑定变量的生命范围后时被释放。

可变变量的Ownership规则

下面是可变变量的Ownership规则:

  • 不被使用的返回值会被立即释放
  • 所有使用let绑定的值如果不在中间被转移给其它所有者,这些值会在函数退出对象离开它们的存活范围时得到释放。
  • 被替换的值会被释放

所以Rust是通过确保只有一个owner拥有一个对象来满足上面的规则的。

Ownership系统的威力

最初一看这些ownership规则好像限制很多,其实只是因为我们已经习惯于其它的规则了。这些规则实际上并没有限制我们可以做什么事情,它们只是给我们了一个不同的基础来构建更高层的结构。

在其它语言中这些高层的结构可能会更难做到像Rust一样的安全,如果想要做到Rust一样的编译期的安全检查则更加难。

现在让我们来看一看标准库所提供的这些高层结构。

内存分配

前面我们讨论了像integer一样的可以在stack上分配的值类型。我们的测试类型Bob就是这样的一个类型。虽然很多流行的语言也可以将对象保存在stack中(例如C#中的struct,或者不使用new构造的c++对象),还有很多语言做不到这一点。

通常来说,一个新创建的对象实例所需要的内存(在很多语言中使用new operator创建的实例)是在heap中进行分配的。

基于heap的内存分配有很多有点。首先它不受stack尺寸的限制。在stack上分配一个巨大的对象可能会导致stack overflow。另外一旦内存分配完成,对象的地址不再发生变化。相反在stack上分配的对象在移动或者发生复制的时候地址会发生变化,对象的内容需要从stack的一个地方拷贝到另外的地方。 虽然在对象尺寸很小的时候这种拷贝是有效率的,但是当对象的尺寸变大的时候拷贝就会明显的降低性能。

Rust使用Box来解决这个问题。当对象的内存从heap中被分配并构建完成后,对象的指针会被包裹在Box中,而Box则作为一个智能指针存在于stack中。

例如我们可以这样在heap中创建Bob对象:

fn main() {
    let bob = Box::new(Bob::new("A"));
}
new bob A
del bob A

从Box::new返回的对象类型是Box,这个generic类型对Bob对象的生命周期进行管理,并且确保在Box被释放时一并释放Bob对象。

因为Box类型是不可拷贝的,因此它遵循前面所讨论的同样的ownership规则。当它在stack上离开了作用域后会被释放,而它的drop方法会在这时被调用,而drop方法会进一步将分配在heap上的Bob对象释放。

其它语言通常要求开发者手动显式的释放内存(通过容易忘记或者因错误被调用多次的delete语句),或者使用Garbage Collector来跟踪内存中所有的指针并且在指针不存在活引用的时候释放它们。跟这些语言比起来Rust这个简单安全并且有效的内存释放机制对用户来说非常易用。

当这个简单的做法不能满足需求的时候,我们还有其它的工具来处理更复杂的场景。

垃圾回收

Rust提供了足够的底层工具以外部库的方式实现垃圾回收。目前Rust直接内置的垃圾回收库是基于引用计数的。

例如我们可以这样来把bob对象交给Rc管理:

use std::rc::Rc;  fn main() {
    let bob = Rc::new(Bob::new("A"));
    println!("{:?}", bob);
}
new bob A
Rc(bob A)
del bob A 

Try it here!

接着我们可以修改black_hole函数以Rc为参数类型,然后我们可以检查对象是否被black_hole函数释放。实际上我们可以把这个函数写的更通用,让它接受任意实现了Show trait的类型作为参数,让我们顺便把它改成gereric的吧:

fn black_hole<T>(value: T) where T: fmt::Show {
    println!("imminent shrinkage {:?}", value);
}

新版本和以前的使用方法一致,并且以后我们不再因为类型变更给对其进行调整了。

现在我们用Rc 作为参数来调用 black_hole函数!

fn main() {
    let bob = Rc::new(Bob::new("A"));
    black_hole(bob.clone()); // clone call
    println!("{:?}", bob);
}
new bob A
imminent shrinkage Rc(bob A)
Rc(bob A)
del bob A

Try it here

太好了,bob从black_hole中存活下来了!那么它是怎么做到的呢?

在bob对象被保存在Rc中后,只要任何地点拥有一个活的Rc副本存在,bob就不会被释放。在Rc内部使用了 Box来保存heap中分配的bob对象的指针同时维持了对这个对象的引用计数。

每当一个新的副本被创建时(调用Rc的clone方法)引用计数会被增加,而当副本离开生命域后会减少引用计数。当引用计数到达0的时候Rc所维持的对象会被释放将内存返回给heap。

注意,前面介绍的Rc是不可变的,如果需要对其包装的Bob对象进行修改,我们可以将其进一步封装于一个RefCell类型中。RefCell类型允许borrow一个对象的可变引用 。在下面的示例中它会被mutate函数修改。

fn mutate(bob: Rc<RefCell<Bob>>) {
    bob.borrow_mut().name = String::from_str("mutant");
}

fn main() {
    let bob = Rc::new(RefCell::new(Bob::new("A")));
    mutate(bob.clone());
    println!("{:?}", bob);
}
new bob A
Rc(RefCell { value: bob mutant })
del bob mutant

这个例子很好的演示了如何组合底层工具在最小的代价下来达到需要的目的。

同样,虽然Rc只能在一个线程中使用,但是我们可以使用Arc类型来在多个线程中实现Atomic的引用计数。如果多个对象互相引用,那么一个可变的Rc可能产生循环引用。但是我们可以把Rc复制为Weak引用,因为Weak引用不参与引用计数,从而解决循环引用问题。我们可以从官方文档得到更详细的介绍。

更重要的是,更高级的垃圾回收机制可以并一定会在将来以附加库的方式来实现。

并发

与我们常见的模式不同,Rust中使用的线程方式非常有趣。在默认情况下线程之间不会发生任何数据访问竞争(data races)。这并不是因为在线程之间存在特殊的隔离体来避免竞争访问的发生,实际上因为ownership模型自身是线程安全的,用户甚至可以开发自己的线程库并提供相似的安全特征。

现在思考一下当我们把两个值,一个可移动的Bob和一个可拷贝的integer,发送给新的Rust线程时会发生什么:

use std::thread;

fn main() {
    let bob = Bob::new("A");
    let i : i64 = 12;

    let child = thread::spawn(move || {
        println!("From thread, {:?} and {:?}!", bob, i);
    });

    println!("waiting for thread to end");
    child.join();
}

new bob "A"
waiting for thread to end
From thread, bob "A" and 12!
del bob "A"

Try it here!

What is happening there? First, we create two values: bob and i. Then we create a new thread with thread::spawn and pass a closure for it to execute. This closure is going to capture our variables bob as i.

Capturing means different things for Bob and i. Because the Bob is non-Copy, it will be moved to the new thread. The i will be copied there. When the theead is running, we can modify original copy of i (if needed). It does not influence the copy that was passed to the thread.

Bob, however, is now owned by this new thread, and can not be modified unless the thread returns it back somehow. If we wanted, we could return it to the main thread over child.join() (the join waits for the thread to finish).

fn main() {
    let mut bob = Bob::new("A");

    let child = thread::spawn(move || {
        mutate(&mut bob);
        bob
    });

    println!("waiting for thread to end");

    if let Ok(bob) = child.join() {
        println!("{:?}", bob);
    }
}

fn mutate(bob: &mut Bob) {
    bob.name = "mutant".to_string();
}
new bob "A"
waiting for thread to end
bob "mutant"
del bob "mutant"

Experiment more here!

One could say that this does not change much the way we used to work with threads - we know not to share same memory location between threads without some kind of synchronisation. The difference here is that Rust can enforce these rules at compile-time.

Of course, more things are possible in Rust, for example, the channels can be used for sending and receiving data between threads in more efficient ways. More is available in official threading documentation, channel documentation, and the book.

还有什么?

通过这部分我们熟悉了Rust的ownership系统,应该可以开始阅读文档开发安全的应用程序了。

但是到目前为止我们还完全没有讨论Rust的borrowing系统,在这个指南的第二部分我们会学习到为什么我们需要borrowing以及怎么更好的利用borrowing。

jack.zh 标签:Rust 继续阅读

1784 ℉

15.07.23

MySQL数据类型和常用字段属性总结

前言

这里先总结数据类型。MySQL中的数据类型大的方面来分,可以分为:日期和时间、数值,以及字符串。下面就分开来进行总结。

日期和时间

MySQL数据类型 含义
date 3字节,日期,格式:2014-09-18
time 3字节,时间,格式:08:42:30
datetime 8字节,日期时间,格式:2014-09-18 08:42:30
timestamp 4字节,自动存储记录修改的时间
year 1字节,年份

数值数据类型

整型

MySQL数据类型 含义(有符号)
tinyint 1字节,范围(-128~127)
smallint 2字节,范围(-32768~32767)
mediumint 3字节,范围(-8388608~8388607)
int 4字节,范围(-2147483648~2147483647)
bigint 8字节,范围(+-9.22*10的18次方)

面定义的都是有符号的,当然了,也可以加上unsigned关键字,定义成无符号的类型,那么对应的取值范围就要翻翻了,比如:

tinyint unsigned的取值范围为0~255。

浮点型

MySQL数据类型 含义
float(m, d) 4字节,单精度浮点型,m总个数,d小数位
double(m, d) 8字节,双精度浮点型,m总个数,d小数位
decimal(m, d) decimal是存储为字符串的浮点数

我在MySQL中建立了一个表,有一列为float(5, 3);做了以下试验:

  • 1.插入123.45678,最后查询得到的结果为99.999;
  • 2.插入123.456,最后查询结果为99.999;
  • 3.插入12.34567,最后查询结果为12.346;

所以,在使用浮点型的时候,还是要注意陷阱的,要以插入数据库中的实际结果为准。

字符串数据类型

MySQL数据类型 含义
char(n) 固定长度,最多255个字符
varchar(n) 可变长度,最多65535个字符
tinytext 可变长度,最多255个字符
text 可变长度,最多65535个字符
mediumtext 可变长度,最多2的24次方-1个字符
longtext 可变长度,最多2的32次方-1个字符
  • 1.char(n) varchar(n) 中括号中n代表字符的个数,并不代表字节个数,所以当使用了中文的时候(UTF8)意味着可以插入m个中文,但是实际会占用m*3个字节。
  • 2.同时charvarchar最大的区别就在于char不管实际value都会占用n个字符的空间,而varchar只会占用实际字符应该占用的空间+1,并且实际空间+1<=n
  • 3.超过charvarchar的n设置后,字符串会被截断。
  • 4.char的上限为255字节,varchar的上限65535字节,text的上限为65535。
  • 5.char在存储的时候会截断尾部的空格,varchartext不会。
  • 6.varchar会使用1-3个字节来存储长度,text不会。

其它类型

  • 1.enum(“member1″, “member2″, … “member65535″)

enum数据类型就是定义了一种枚举,最多包含65535个不同的成员。当定义了一个enum的列时,该列的值限制为列定义中声明的值。如果列声明包含NULL属性,则NULL将被认为是一个有效值,并且是默认值。如果声明了NOT NULL,则列表的第一个成员是默认值。

  • 2.set(“member”, “member2″, … “member64″)

set数据类型为指定一组预定义值中的零个或多个值提供了一种方法,这组值最多包括64个成员。值的选择限制为列定义中声明的值。

数据类型属性

上面大概总结了MySQL中的数据类型,当然了,上面的总结肯定是不全面的,如果要非常全面的总结这些内容,好几篇文章都不够的。下面就再来总结一些常用的属性。

1.auto_increment

auto_increment能为新插入的行赋一个唯一的整数标识符。为列赋此属性将为每个新插入的行赋值为上一次插入的ID+1。

MySQL要求将auto_increment属性用于作为主键的列。此外,每个表只允许有一个auto_increment列。例如:

id smallint not null auto_increment primary key

2.binary

binary属性只用于char和varchar值。当为列指定了该属性时,将以区分大小写的方式排序。与之相反,忽略binary属性时,将使用不区分大小写的方式排序。例如:

hostname char(25) binary not null

3.default

default属性确保在没有任何值可用的情况下,赋予某个常量值,这个值必须是常量,因为MySQL不允许插入函数或表达式值。此外,此属性无法用于BLOB或TEXT列。如果已经为此列指定了NULL属性,没有指定默认值时默认值将为NULL,否则默认值将依赖于字段的数据类型。例如:

subscribed enum('0', '1') not null default '0'

4.index

如果所有其他因素都相同,要加速数据库查询,使用索引通常是最重要的一个步骤。索引一个列会为该列创建一个有序的键数组,每个键指向其相应的表行。以后针对输入条件可以搜索这个有序的键数组,与搜索整个未索引的表相比,这将在性能方面得到极大的提升。

create table employees
(
id varchar(9) not null,
firstname varchar(15) not null,
lastname varchar(25) not null,
email varchar(45) not null,
phone varchar(10) not null,
index lastname(lastname),
primary key(id)
);

我们也可以利用MySQL的create index命令在创建表之后增加索引:

create index lastname on employees (lastname(7));

这一次只索引了名字的前7个字符,因为可能不需要其它字母来区分不同的名字。因为使用较小的索引时性能更好,所以应当在实践中尽量使用小的索引。

5.not null

如果将一个列定义为not null,将不允许向该列插入null值。建议在重要情况下始终使用not null属性,因为它提供了一个基本验证,确保已经向查询传递了所有必要的值。

6.null

为列指定null属性时,该列可以保持为空,而不论行中其它列是否已经被填充。记住,null精确的说法是“无”,而不是空字符串或0。

7.primary key

primary key属性用于确保指定行的唯一性。指定为主键的列中,值不能重复,也不能为空。为指定为主键的列赋予auto_increment属性是很常见的,因为此列不必与行数据有任何关系,而只是作为一个唯一标识符。主键又分为以下两种:

(1)单字段主键

如果输入到数据库中的每行都已经有不可修改的唯一标识符,一般会使用单字段主键。注意,此主键一旦设置就不能再修改。

(2)多字段主键

如果记录中任何一个字段都不可能保证唯一性,就可以使用多字段主键。这时,多个字段联合起来确保唯一性。如果出现这种情况,指定一个auto_increment整数作为主键是更好的办法。

8.unique

被赋予unique属性的列将确保所有值都有不同的值,只是null值可以重复。一般会指定一个列为unique,以确保该列的所有值都不同。例如:

email varchar(45) unique

9.zerofill

zerofill属性可用于任何数值类型,用0填充所有剩余字段空间。例如,无符号int的默认宽度是10;因此,当“零填充”的int值为4时,将表示它为0000000004。例如:

orderid int unsigned zerofill not null

jack.zh 标签:MySQL 继续阅读

1871 ℉

15.07.02

递归与尾递归的讲解

这篇文章的目的比较那个啥,原因是这样的,一大牛自吹:写C++或者Java写一年,没超过三个BUG,递归随便写。然后就探讨了一下递归的问题,在C++中,还有在Java中堆栈的问题和办法,发现他根本不懂,然后我说了下何为尾递归,他听说之后,只有一句,不用递归就算了,全改成循环。你想打他吗?使劲打,我不会拉你的。

1、递归

关于递归的概念,我们都不陌生。简单的来说递归就是一个函数直接或间接地调用自身,是为直接或间接递归 递归一般用于解决三类问题:

  • (1)数据的定义是按递归定义的。(Fibonacci函数,n的阶乘)
  • (2)问题解法按递归实现。(回溯)
  • (3)数据的结构形式是按递归定义的。(二叉树的遍历,图的搜索)

记住一点,递归有线性递归(普通的递归)和尾递归。尾递归包含于递归之中,但有特殊的地方,关于为什么叫尾递归,一会再说。

我们现在就实现一个递归的算法,用线性递归实现Fibonacci函数,程序如下所示:

// a.c:

int FibonacciRecursive(int n)
{
        if( n < 2)
                return n;
        return (FibonacciRecursive(n-1)+FibonacciRecursive(n-2));
}

看不懂的请举手,拖出去斩了。

2、尾递归

对递归有些了解的朋友一定猜得到,如果上面的N足够大,那么上面这个方法就可能会遇到栈溢出,也就是抛出StackOverflowError。这是由于每个线程在执行代码时,都会分配一定尺寸的栈空间(Windows系统中为8K-1M,Linux为8M,可以改变,后面附录会说),每次方法调用时都会在栈里储存一定信息(如参数、局部变量、返回地址等等),这些信息再少也会占用一定空间,成千上万个此类空间累积起来,自然就超过线程的栈空间了。不过这个问题并非无解,这就是我们的尾递归该出场了。

顾名思义,尾递归就是从最后开始计算, 每递归一次就算出相应的结果, 也就是说, 函数调用出现在调用者函数的尾部, 因为是尾部, 所以根本没有必要去保存任何局部变量. 直接让被调用的函数返回时越过调用者, 返回到调用者的调用者去。尾递归就是把当前的运算结果(或路径)放在参数里传给下层函数,深层函数所面对的不是越来越简单的问题,而是越来越复杂的问题,因为参数里带有前面若干步的运算路径。

如果你还看不懂我上面说的是什么意思,那我就搬出来尾调用来说话。

尾调用

原因很多人的都知道,让我们先回顾一下函数调用的大概过程:

  • 1)调用开始前,调用方(或函数本身)会往栈上压相关的数据,参数,返回地址,局部变量等。
  • 2)执行函数。
  • 3)清理栈上相关的数据,返回。

尾调用的概念非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。记住啊,上面的 (FibonacciRecursive(n-1)+FibonacciRecursive(n-2))可不是偶,

尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。

好的,我们就用尾递归来实现一下上面的东西:

// b.c
int FibonacciTailRecursive(int n,int ret1,int ret2)
{
        if(n==0)
                return ret1;
        return FibonacciTailRecursive(n-1,ret2,ret1+ret2);
}

3、对比与解释

我知道你不信我上面说的,我们还是代码说话,汇编一下普通递归:

        .file   "a.c"
        .text
        .globl  FibonacciRecursive
        .type   FibonacciRecursive, @function
FibonacciRecursive:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        pushq   %rbx
        subq    $24, %rsp
        .cfi_offset 3, -24
        movl    %edi, -20(%rbp)
        cmpl    $1, -20(%rbp)
        jg      .L2
        movl    -20(%rbp), %eax
        jmp     .L3
.L2:
        movl    -20(%rbp), %eax
        subl    $1, %eax
        movl    %eax, %edi
        call    FibonacciRecursive
        movl    %eax, %ebx
        movl    -20(%rbp), %eax
        subl    $2, %eax
        movl    %eax, %edi
        call    FibonacciRecursive
        addl    %ebx, %eax
.L3:
        addq    $24, %rsp
        popq    %rbx
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   FibonacciRecursive, .-FibonacciRecursive
        .ident  "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4"
        .section        .note.GNU-stack,"",@progbits

再看一下优化过的尾递归:

        .file   "b.c"
        .text
        .globl  FibonacciTailRecursive
        .type   FibonacciTailRecursive, @function
FibonacciTailRecursive:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        subq    $16, %rsp
        movl    %edi, -4(%rbp)
        movl    %esi, -8(%rbp)
        movl    %edx, -12(%rbp)
        cmpl    $0, -4(%rbp)
        jne     .L2
        movl    -8(%rbp), %eax
        jmp     .L3
.L2:
        movl    -12(%rbp), %eax
        movl    -8(%rbp), %edx
        addl    %eax, %edx
        movl    -4(%rbp), %eax
        leal    -1(%rax), %ecx
        movl    -12(%rbp), %eax
        movl    %eax, %esi
        movl    %ecx, %edi
        call    FibonacciTailRecursive
.L3:
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   FibonacciTailRecursive, .-FibonacciTailRecursive
        .ident  "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4"
        .section        .note.GNU-stack,"",@progbits

看见差别没有,** 调用栈 **

而尾递归在某些语言的实现上,能避免上述所说的问题,注意是某些语言上,尾递归本身并不能消除函数调用栈过长的问题,那什么是尾递归呢?在上面写的一般递归函数 FibonacciRecursive() 中,我们可以看到,FibonacciRecursive(n) 是依赖于 FibonacciRecursive(n-1) 的,FibonacciRecursive(n) 只有在得到 FibonacciRecursive(n-1) 的结果之后,才能计算它自己的返回值,因此理论上,在 FibonacciRecursive(n-1) 返回之前,FibonacciRecursive(n),不能结束返回。因此FibonacciRecursive(n)就必须保留它在栈上的数据,直到FibonacciRecursive(n-1)先返回,而尾递归的实现则可以在编译器的帮助下,消除这个限制

4、然并卵

通过上面的解释,我知道你认为你明白了我所说的,但是我不能确定你是不是意识到了我所表达的并不是你认为的意思。这句话的埋伏在这里:注意是某些语言上,尾递归本身并不能消除函数调用栈过长的问题。

在Java Python JS等语言里面,普通的尾递归是没用的,为什么呢?这个你自己去探寻去吧,讲这个就没完没了了,可我们一般有自己的魔法,OK,一个一个的来。

Python

来个尾递归:

def Fib(n,b1=1,b2=1,c=3):
    if n<3:
        return 1
    else:
        if n==c:
            return b1+b2
        else:
            return Fib(n,b1=b2,b2=b1+b2,c=c+1)
Fib(10000)

我猜你一定会遇到类似这样的东西:

    return Fib(n,b1=b2,b2=b1+b2,c=c+1)
  File "1.py", line 40, in Fib
    return Fib(n,b1=b2,b2=b1+b2,c=c+1)
  File "1.py", line 40, in Fib
    return Fib(n,b1=b2,b2=b1+b2,c=c+1)
  File "1.py", line 40, in Fib
    return Fib(n,b1=b2,b2=b1+b2,c=c+1)
  File "1.py", line 40, in Fib
    return Fib(n,b1=b2,b2=b1+b2,c=c+1)
  File "1.py", line 40, in Fib
    return Fib(n,b1=b2,b2=b1+b2,c=c+1)
  File "1.py", line 40, in Fib
    return Fib(n,b1=b2,b2=b1+b2,c=c+1)
  File "1.py", line 40, in Fib
    return Fib(n,b1=b2,b2=b1+b2,c=c+1)
  File "1.py", line 40, in Fib
    return Fib(n,b1=b2,b2=b1+b2,c=c+1)
RuntimeError: maximum recursion depth exceeded

来点黑魔法:

@tail_call_optimized
def Fib(n,b1=1,b2=1,c=3):
    if n<3:
        return 1
    else:
        if n==c:
            return b1+b2
        else:
            return Fib(n,b1=b2,b2=b1+b2,c=c+1)

Fib(10000)

哈哈,海阔天空了,没问题。当然,为了探究这个装饰器干嘛了,查看一下他的源码:

import sys  

class TailRecurseException:  
  def __init__(self, args, kwargs):  
    self.args = args  
    self.kwargs = kwargs  

def tail_call_optimized(g):  
  """  
  This function decorates a function with tail call  
  optimization. It does this by throwing an exception  
  if it is it's own grandparent, and catching such  
  exceptions to fake the tail call optimization.  

  This function fails if the decorated  
  function recurses in a non-tail context.  
  """  
  def func(*args, **kwargs):  
    f = sys._getframe()  
    if f.f_back and f.f_back.f_back and f.f_back.f_back.f_code == f.f_code:  
      raise TailRecurseException(args, kwargs)  
    else:  
      while 1:  
        try:  
          return g(*args, **kwargs)  
        except TailRecurseException, e:  
          args = e.args  
          kwargs = e.kwargs  
  func.__doc__ = g.__doc__  
  return func

我说,你是上帝派来玩我的吗?你家的内存不花钱?好吧,没有银弹。

其他语言也有相对应的办法来解决,我这里就不搬过来了,我这里列举了几个相对熟悉一点的语言解决方案

Java
Clojure

5、附录

C实现的一个尾递归实现
#include <stdio.h>
#include <stdlib.h>

typedef struct node
{
  int data;
  struct node* next;
}node,*linklist;

void InitLinklist(linklist* head)
{
     if(*head != NULL)
        free(*head);
     *head = (node*)malloc(sizeof(node));
     (*head)->next = NULL;
}

void InsertNode(linklist* head,int d)
{
     node* newNode = (node*)malloc(sizeof(node));
     newNode->data = d;
     newNode->next = (*head)->next;
     (*head)->next = newNode;
}

//直接递归求链表的长度 
int GetLengthRecursive(linklist head)
{
    if(head->next == NULL)
       return 0;
    return (GetLengthRecursive(head->next) + 1);
}
//采用尾递归求链表的长度,借助变量acc保存当前链表的长度,不断的累加 
int GetLengthTailRecursive(linklist head,int *acc)
{
    if(head->next == NULL)
      return *acc;
    *acc = *acc+1;
    return GetLengthTailRecursive(head->next,acc);
}

void PrintLinklist(linklist head)
{
     node* pnode = head->next;
     while(pnode)
     {
        printf("%d->",pnode->data);
        pnode = pnode->next;
     }
     printf("->NULL\n");
}

int main()
{
    linklist head = NULL;
    int len = 0;
    InitLinklist(&head);
    InsertNode(&head,10);
    InsertNode(&head,21);
    InsertNode(&head,14);
    InsertNode(&head,19);
    InsertNode(&head,132);
    InsertNode(&head,192);
    PrintLinklist(head);
    printf("The length of linklist is: %d\n",GetLengthRecursive(head));
    GetLengthTailRecursive(head,&len);
    printf("The length of linklist is: %d\n",len);
    system("pause");
}
Linux下改变线程堆栈大小

linux查看修改线程默认栈空间大小 ulimit -s

  • 1、通过命令 ulimit -s 查看linux的默认栈空间大小,默认情况下 为10240 即10M
  • 2、通过命令 ulimit -s 设置大小值 临时改变栈空间大小:ulimit -s 102400, 即修改为100M
  • 3、可以在/etc/rc.local 内 加入 ulimit -s 102400 则可以开机就设置栈空间大小
  • 4、在/etc/security/limits.conf 中也可以改变栈空间大小

重新登录,执行ulimit -s 即可看到改为102400 即100M

阮一峰的尾递归的讲解

http://www.ruanyifeng.com/blog/2015/04/tail-call.html

最后

少用递归多用其他办法的确为最好的解决办法。这个是银弹。

jack.zh 继续阅读

2038 ℉

15.06.30

Python GIL的说明

GIL 是什么东西?它对我们的 python 程序会产生什么样的影响?

上面的问题几乎是我面试Python必问的问题,今天我就大概说一下

我们先来看一个问题。运行下面这段 python 程序,CPU 占用率是多少?

def dead_loop():
    while True:
        passdead_loop()

答案是什么呢,占用 100% CPU?那是单核!还得是没有超线程的古董 CPU。在我的双核 CPU 上,这个死循环只会吃掉我一个核的工作负荷,也就是只占用 50% CPU。那如何能让它在双核机器上占用 100% 的 CPU 呢?答案很容易想到,用两个线程就行了,线程不正是并发分享 CPU 运算资源的吗。可惜答案虽然对了,但做起来可没那么简单。下面的程序在主线程之外又起了一个死循环的线程

import threading
def dead_loop():
    while True: pass
# 新起一个死循环线程
t =threading.Thread(target=dead_loop)
t.start()
# 主线程也进入死循环
dead_loop()t.join()

按道理它应该能做到占用两个核的 CPU 资源,可是实际运行情况却是没有什么改变,还是只占了 50% CPU 不到。这又是为什么呢?难道 python 线程不是操作系统的原生线程?打开 system monitor 一探究竟,这个占了 50% 的 python 进程确实是有两个线程在跑。那这两个死循环的线程为何不能占满双核 CPU 资源呢?其实幕后的黑手就是 GIL。

GIL 的迷思:痛并快乐着

GIL 的全程为Global Interpreter Lock ,意即全局解释器锁。在 Python 语言的主流实现 CPython 中,GIL 是一个货真价实的全局线程锁,在解释器解释执行任何 Python 代码时,都需要先获得这把锁才行,在遇到 I/O 操作时会释放这把锁。如果是纯计算的程序,没有 I/O 操作,解释器会每隔 100 次操作就释放这把锁,让别的线程有机会执行(这个次数可以通过sys.setcheckinterval 来调整)。

所以虽然 CPython 的线程库直接封装操作系统的原生线程,但 CPython 进程做为一个整体,同一时间只会有一个获得了 GIL 的线程在跑,其它的线程都处于等待状态等着 GIL 的释放。这也就解释了我们上面的实验结果:虽然有两个死循环的线程,而且有两个物理 CPU 内核,但因为 GIL 的限制,两个线程只是做着分时切换,总的 CPU 占用率还略低于 50%。

看起来 python 很不给力啊。GIL 直接导致 CPython 不能利用物理多核的性能加速运算。那为什么会有这样的设计呢?我猜想应该还是历史遗留问题。

多核 CPU 在 1990 年代还属于类科幻,Guido van Rossum 在创造 python 的时候,也想不到他的语言有一天会被用到很可能 1000+ 个核的 CPU 上面,一个全局锁搞定多线程安全在那个时代应该是最简单经济的设计了。简单而又能满足需求,那就是合适的设计(对设计来说,应该只有合适与否,而没有好与不好)。怪只怪硬件的发展实在太快了,摩尔定律给软件业的红利这么快就要到头了。短短 20 年不到,代码工人就不能指望仅仅靠升级 CPU 就能让老软件跑的更快了。

在多核时代,编程的免费午餐没有了。如果程序不能用并发挤干每个核的运算性能,那就意谓着会被淘汰。对软件如此,对语言也是一样。那该咋整呢?

Python 的对策

Python 的应对很简单,以不变应万变。在最新的 python 3 中依然有 GIL。之所以不去掉,原因嘛,不外以下几点:

欲练神功,挥刀自宫:

CPython 的 GIL 本意是用来保护所有全局的解释器和环境状态变量的。如果去掉 GIL,就需要多个更细粒度的锁对解释器的众多全局状态进行保护。或者采用 Lock-Free 算法。无论哪一种,要做到多线程安全都会比单使用 GIL 一个锁要难的多。而且改动的对象还是有 20 年历史的 CPython 代码树,更不论有这么多第三方的扩展也在依赖 GIL。对 Python 社区来说,这不异于挥刀自宫,重新来过。

就算自宫,也未必成功:

有位牛人曾经做了一个验证用的 CPython,将 GIL 去掉,加入了更多的细粒度锁。但是经过实际的测试,对单线程程序来说,这个版本有很大的性能下降,只有在利用的物理 CPU 超过一定数目后,才会比 GIL 版本的性能好。这也难怪。单线程本来就不需要什么锁。单就锁管理本身来说,锁 GIL 这个粗粒度的锁肯定比管理众多细粒度的锁要快的多。而现在绝大部分的 python 程序都是单线程的。再者,从需求来说,使用 python 绝不是因为看中它的运算性能。就算能利用多核,它的性能也不可能和 C/C++ 比肩。费了大力气把 GIL 拿掉,反而让大部分的程序都变慢了,这不是南辕北辙吗。

难道 Python 这么优秀的语言真的仅仅因为改动困难和意义不大就放弃多核时代了吗?其实,不做改动最最重要的原因还在于:不用自宫,也一样能成功!

其它神功:

那除了切掉 GIL 外,果然还有方法让 Python 在多核时代活的滋润?让我们回到本文最初的那个问题:如何能让这个死循环的 Python 脚本在双核机器上占用 100% 的 CPU?其实最简单的答案应该是:运行两个 python 死循环的程序!也就是说,用两个分别占满一个 CPU 内核的 python 进程来做到。确实,多进程也是利用多个 CPU 的好方法。只是进程间内存地址空间独立,互相协同通信要比多线程麻烦很多。

有感于此,Python 在 2.6 里新引入了multiprocessing 这个多进程标准库,让多进程的 python 程序编写简化到类似多线程的程度,大大减轻了 GIL 带来的不能利用多核的尴尬。

这还只是一个方法,如果不想用多进程这样重量级的解决方案,还有个更彻底的方案,放弃 Python,改用 C/C++。当然,你也不用做的这么绝,只需要把关键部分用 C/C++ 写成 Python 扩展,其它部分还是用 Python 来写,让 Python 的归 Python,C 的归 C。一般计算密集性的程序都会用 C 代码编写并通过扩展的方式集成到 Python 脚本里(如 NumPy 模块)。在扩展里就完全可以用 C 创建原生线程,而且不用锁 GIL,充分利用 CPU 的计算资源了。不过,写 Python 扩展总是让人觉得很复杂。好在 Python 还有另一种与 C 模块进行互通的机制 :ctypes

利用 ctypes 绕过 GIL

ctypes 与 Python 扩展不同,它可以让 Python 直接调用任意的 C 动态库的导出函数。你所要做的只是用 ctypes 写些 python 代码即可。最酷的是,ctypes 会在调用 C 函数前释放 GIL。所以,我们可以通过 ctypes 和 C 动态库来让 python 充分利用物理内核的计算能力。让我们来实际验证一下,这次我们用 C 写一个死循环函数

extern "C" {
   void DeadLoop() {
       while (true);
  }
}

用上面的 C 代码编译生成动态库libdead_loop.so(Windows 上是dead_loop.dll)

接着就要利用 ctypes 来在 python 里 load 这个动态库,分别在主线程和新建线程里调用其中的DeadLoop

from ctypes import *
from threading import Thread
lib =cdll.LoadLibrary("libdead_loop.so")
t = Thread(target=lib.DeadLoop)
t.start()
lib.DeadLoop()

这回再看看 system monitor,Python 解释器进程有两个线程在跑,而且双核 CPU 全被占满了,ctypes 确实很给力!需要提醒的是,GIL 是被 ctypes 在调用 C 函数前释放的。但是 Python 解释器还是会在执行任意一段 Python 代码时锁 GIL 的。如果你使用 Python 的代码做为 C 函数的 callback,那么只要 Python 的 callback 方法被执行时,GIL 还是会跳出来的。比如下面的例子:

extern"C"{
  typedef void Callback();
  void Call(Callback* callback) { 
        callback();
  }
}

from ctypes import * 
from threading import Thread
def dead_loop(): 
    while True:
        pass
lib = cdll.LoadLibrary("libcall.so")
Callback = CFUNCTYPE(None)
callback = Callback(dead_loop)
t = Thread(target=lib.Call, args(callback,))
t.start()
lib.Call(callback)

注意这里与上个例子的不同之处,这次的死循环是发生在 Python 代码里 (DeadLoop函数) 而 C 代码只是负责去调用这个 callback 而已。运行这个例子,你会发现 CPU 占用率还是只有 50% 不到。GIL 又起作用了。 其实,从上面的例子,我们还能看出 ctypes 的一个应用,那就是用 Python 写自动化测试用例,通过 ctypes 直接调用 C 模块的接口来对这个模块进行黑盒测试,哪怕是有关该模块 C 接口的多线程安全方面的测试,ctypes 也一样能做到。

其他Python与C/C++的扩展,请看我的github

结语

虽然 CPython 的线程库封装了操作系统的原生线程,但却因为 GIL 的存在导致多线程不能利用多个 CPU 内核的计算能力。好在现在 Python 有了易经筋(multiprocessing), 吸星大法(C 语言扩展机制)和独孤九剑(ctypes),足以应付多核时代的挑战,GIL 切还是不切已经不重要了,不是吗。

不重要的重点还在于,我们在重IO的程序中 GIL的影响很小。而在重CPU的程序中,Python并不是最好的工具。当然很多人会说Python Web的利用多核部署,你怎么办,难道我要去用multiprocessing or ctypes?稍安勿躁,你当Nginx是白痴吗?

jack.zh 标签:python 继续阅读

2223 ℉

15.06.30

詹姆斯放弃得也太早了把[詹黑]

转自 虎扑

1

这球库里的上空蓝,如果詹姆斯想封盖应该是可以的,结果大家都知道了,目送库里轻松得分…还有2分钟就放弃了,詹姆斯是不是放弃得太早了?


2

库里过掉香波特,詹姆斯站篮下目送库里上篮,这时比分变成98:85


3

骑士队叫暂停,詹姆斯掩面。


4

暂停时间快到,这时米勒过来。


5

暂停时间到,詹姆斯擦干泪水发边线球


6

球发出吗,过了2秒钟,詹姆斯的位置


7

JR三分打铁,德拉拼到前场篮板。 这时距离发边线球过了4秒钟,詹姆斯的位置。。。


8

德拉抢到前场板二次进攻, 这时距离发出边线球过了6秒,


9

德拉二次进攻不中,TT继续拼抢前场篮板,这时距离詹姆斯发出边线球过了8秒,詹姆斯纹丝不动。


10

TT拨出前场篮板,德拉和格林拼抢倒地,这时距离发出边线球过去了9秒,詹姆斯稳如邓肯,哦不,稳如石佛。


11

上一轮进攻骑士依靠德拉和TT死命拼抢获得罚球二罚一中搬回1分,但似乎12分的差距还是让詹姆斯感到绝望,于是JR继续干拔,詹姆斯老地方继续。。。


12

JR三分干拔命中,比分缩小到9分,时间还有一分钟,这时库里转换进攻快速杀到骑士篮下,被詹姆斯一肘。。。


13

库里罚球2罚全中,真不给詹姆斯面子,肘子好痛。。

JR转眼杀回继续干拔3分球进!! 詹姆斯这时观看点换了个地方


14

依靠JR两记三分骑士把比分追到只剩7分,这时比赛时间还有35秒!!!大声点!!想到了谁?? JR继续上演干拔三分!! 速度太快了,詹姆斯还没进边线!!


15

JR连中3记三分, 而勇士期间只得到2分。比分缩小至6分,比赛时间还有25秒。 又是JR!!!超远距离3分出手,咦,詹姆斯呢?????

jack.zh 标签:NBA 继续阅读

1708 ℉

15.06.24

NGINX引入线程池 性能提升9倍

原文:nginx.com

1. 引言(Introduction)

It’s well known that NGINX uses an asynchronous, event-driven approach to handling connections. This means that instead of creating another dedicated process or thread for each request (like servers with a traditional architecture), it handles multiple connections and requests in one worker process. To achieve this, NGINX works with sockets in a non-blocking mode and uses efficient methods such as epoll and kqueue.

Because the number of full-weight processes is small (usually only one per CPU core) and constant, much less memory is consumed and CPU cycles aren’t wasted on task switching. The advantages of such an approach are well-known through the example of NGINX itself. It successfully handles millions of simultaneous requests and scales very well.

正如我们所知,NGINX采用了[异步、事件驱动]((http://nginx.com/blog/inside-nginx-how-we-designed-for-performance-scale/)的方法来处理连接。这种处理方式无需(像使用传统架构的服务器一样)为每个请求创建额外的专用进程或者线程,而是在一个工作进程中处理多个连接和请求。为此,NGINX工作在非阻塞的socket模式下,并使用了[epoll](http://man7.org/linux/man-pages/man7/epoll.7.html) 和 kqueue这样有效的方法。

因为满负载进程的数量很少(通常每核CPU只有一个)而且恒定,所以任务切换只消耗很少的内存,而且不会浪费CPU周期。通过NGINX本身的实例,这种方法的优点已经为众人所知。NGINX可以非常好地处理百万级规模的并发请求。

1

Each process consumes additional memory, and each switch between them consumes CPU cycles and trashes L-caches

每个进程都消耗额外的内存,而且每次进程间的切换都会消耗CPU周期并丢弃CPU高速缓存中的数据。

Each process consumes additional memory, and each switch between them consumes CPU cycles and trashes L-caches But the asynchronous, event-driven approach still has a problem. Or, as I like to think of it, an “enemy”. And the name of the enemy is: blocking. Unfortunately, many third-party modules use blocking calls, and users (and sometimes even the developers of the modules) aren’t aware of the drawbacks. Blocking operations can ruin NGINX performance and must be avoided at all costs.

Even in the current official NGINX code it’s not possible to avoid blocking operations in every case, and to solve this problem the new “thread pools” mechanism was implemented in NGINX version 1.7.11. What it is and how it supposed to be used, we will cover later. Now let’s meet face to face with our enemy.

但是,异步、事件驱动方法仍然存在问题。或者,我喜欢将这一问题称为“敌兵”,这个敌兵的名字叫阻塞(blocking)。不幸的是,很多第三方模块使用了阻塞调用,然而用户(有时甚至是模块的开发者)并不知道阻塞的缺点。阻塞操作可以毁掉NGINX的性能,我们必须不惜一切代价避免使用阻塞。

即使在当前官方的NGINX代码中,依然无法在全部场景中避免使用阻塞,NGINX1.7.11中实现的线程池机制解决了这个问题。我们将在后面讲述这个线程池是什么以及该如何使用。现在,让我们先和我们的“敌兵”进行一次面对面的碰撞。

2. 问题(The Problem)

First, for better understanding of the problem a few words about how NGINX works.

In general, NGINX is an event handler, a controller that receives information from the kernel about all events occurring on connections and then gives commands to the operating system about what to do. In fact, NGINX does all the hard work by orchestrating the operating system, while the operating system does the routine work of reading and sending bytes. So it’s very important for NGINX to respond fast and in a timely manner.

首先,为了更好地理解这一问题,我们用几句话说明下NGINX是如何工作的。

通常情况下,NGINX是一个事件处理器,即一个接收来自内核的所有连接事件的信息,然后向操作系统发出做什么指令的控制器。实际上,NGINX干了编排操作系统的全部脏活累活,而操作系统做的是读取和发送字节这样的日常工作。所以,对于NGINX来说,快速和及时的响应是非常重要的。

2

The worker process listens for and processes events from the kernel

工作进程监听并处理来自内核的事件

The events can be timeouts, notifications about sockets ready to read or to write, or notifications about an error that occurred. NGINX receives a bunch of events and then processes them one by one, doing the necessary actions. Thus all the processing is done in a simple loop over a queue in one thread. NGINX dequeues an event from the queue and then reacts to it by, for example, writing or reading a socket. In most cases, this is extremely quick (perhaps just requiring a few CPU cycles to copy some data into memory) and NGINX proceeds through all of the events in the queue in an instant.

事件可以是超时、socket读写就绪的通知,或者发生错误的通知。NGINX接收大量的事件,然后一个接一个地处理它们,并执行必要的操作。因此,所有的处理过程是通过一个线程中的队列,在一个简单循环中完成的。NGINX从队列中取出一个事件并对其做出响应,比如读写socket。在多数情况下,这种方式是非常快的(也许只需要几个CPU周期,将一些数据复制到内存中),NGINX可以在一瞬间处理掉队列中的所有事件。

3

All processing is done in a simple loop by one thread

所有处理过程是在一个简单的循环中,由一个线程完成

But what will happen if some long and heavy operation has occurred? The whole cycle of event processing will get stuck waiting for this operation to finish.

So, by saying “a blocking operation” we mean any operation that stops the cycle of handling events for a significant amount of time. Operations can be blocking for various reasons. For example, NGINX might be busy with lengthy, CPU-intensive processing, or it might have to wait to access a resource (such as a hard drive, or a mutex or library function call that gets responses from a database in a synchronous manner, etc.). The key point is that while processing such operations, the worker process cannot do anything else and cannot handle other events, even if there are more system resources available and some events in the queue could utilize those resources.

Imagine a salesperson in a store with a long queue in front of him. The first guy in the queue asks for something that is not in the store but is in the warehouse. The salesperson goes to the warehouse to deliver the goods. Now the entire queue must wait a couple of hours for this delivery and everyone in the queue is unhappy. Can you imagine the reaction of the people? The waiting time of every person in the queue is increased by these hours, but the items they intend to buy might be right there in the shop.

但是,如果NGINX要处理的操作是一些又长又重的操作,又会发生什么呢?整个事件处理循环将会卡住,等待这个操作执行完毕。

因此,所谓“阻塞操作”是指任何导致事件处理循环显著停止一段时间的操作。操作可以由于各种原因成为阻塞操作。例如,NGINX可能因长时间、CPU密集型处理,或者可能等待访问某个资源(比如硬盘,或者一个互斥体,亦或要从处于同步方式的数据库获得相应的库函数调用等)而繁忙。关键是在处理这样的操作期间,工作进程无法做其他事情或者处理其他事件,即使有更多的可用系统资源可以被队列中的一些事件所利用。

我们来打个比方,一个商店的营业员要接待他面前排起的一长队顾客。队伍中的第一位顾客想要的某件商品不在店里而在仓库中。这位营业员跑去仓库把东西拿来。现在整个队伍必须为这样的配货方式等待数个小时,队伍中的每个人都很不爽。你可以想见人们的反应吧?队伍中每个人的等待时间都要增加这些时间,除非他们要买的东西就在店里。

4

Everyone in the queue has to wait for the first person’s order

队伍中的每个人不得不等待第一个人的购买

Nearly the same situation happens with NGINX when it asks to read a file that isn’t cached in memory, but needs to be read from disk. Hard drives are slow (especially the spinning ones), and while the other requests waiting in the queue might not need access to the drive, they are forced to wait anyway. As a result, latencies increase and system resources are not fully utilized.

在NGINX中会发生几乎同样的情况,比如当读取一个文件的时候,如果该文件没有缓存在内存中,就要从磁盘上读取。从磁盘(特别是旋转式的磁盘)读取是很慢的,而当队列中等待的其他请求可能不需要访问磁盘时,它们也得被迫等待。导致的结果是,延迟增加并且系统资源没有得到充分利用。

5

Just one blocking operation can delay all following operations for a significant time

一个阻塞操作足以显著地延缓所有接下来的操作

Some operating systems provide an asynchronous interface for reading and sending files and NGINX can use this interface (see the aio directive). A good example here is FreeBSD. Unfortunately, we can’t say the same about Linux. Although Linux provides a kind of asynchronous interface for reading files, it has a couple of significant drawbacks. One of them is alignment requirements for file access and buffers, but NGINX handles that well. But the second problem is worse. The asynchronous interface requires the O_DIRECT flag to be set on the file descriptor, which means that any access to the file will bypass the cache in memory and increase load on the hard disks. That definitely doesn’t make it optimal for many cases.

To solve this problem in particular, thread pools were introduced in NGINX 1.7.11. They are not included by default in NGINX Plus yet, but contact sales if you’d like to try a build of NGINX Plus R6 that has thread pools enabled.

Now let’s dive into what thread pools are about and how they work.

一些操作系统为读写文件提供了异步接口,NGINX可以使用这样的接口(见AIO指令)。FreeBSD就是个很好的例子。不幸的是,我们不能在Linux上得到相同的福利。虽然Linux为读取文件提供了一种异步接口,但是存在明显的缺点。其中之一是要求文件访问和缓冲要对齐,但NGINX很好地处理了这个问题。但是,另一个缺点更糟糕。异步接口要求文件描述符中要设置O_DIRECT标记,就是说任何对文件的访问都将绕过内存中的缓存,这增加了磁盘的负载。在很多场景中,这都绝对不是最佳选择。

为了有针对性地解决这一问题,在NGINX 1.7.11中引入了线程池。默认情况下,NGINX+还没有包含线程池,但是如果你想试试的话,可以联系销售人员,NGINX+ R6是一个已经启用了线程池的构建版本。

现在,让我们走进线程池,看看它是什么以及如何工作的。

3. 线程池(Thread Pools)

Let’s return to our poor sales assistant who delivers goods from a faraway warehouse. But he has become smarter (or maybe he became smarter after being beaten by the crowd of angry clients?) and hired a delivery service. Now when somebody asks for something from the faraway warehouse, instead of going to the warehouse himself, he just drops an order to a delivery service and they will handle the order while our sales assistant will continue serving other customers. Thus only those clients whose goods aren’t in the store are waiting for delivery, while others can be served immediately.

让我们回到那个可怜的,要从大老远的仓库去配货的售货员那儿。这回,他已经变聪明了(或者也许是在一群愤怒的顾客教训了一番之后,他才变得聪明的?),雇用了一个配货服务团队。现在,当任何人要买的东西在大老远的仓库时,他不再亲自去仓库了,只需要将订单丢给配货服务,他们将处理订单,同时,我们的售货员依然可以继续为其他顾客服务。因此,只有那些要买仓库里东西的顾客需要等待配货,其他顾客可以得到即时服务。

6

Passing an order to the delivery service unblocks the queue

传递订单给配货服务不会阻塞队伍

In terms of NGINX, the thread pool is performing the functions of the delivery service. It consists of a task queue and a number of threads that handle the queue. When a worker process needs to do a potentially long operation, instead of processing the operation by itself it puts a task in the pool’s queue, from which it can be taken and processed by any free thread.

对NGINX而言,线程池执行的就是配货服务的功能。它由一个任务队列和一组处理这个队列的线程组成。 当工作进程需要执行一个潜在的长操作时,工作进程不再自己执行这个操作,而是将任务放到线程池队列中,任何空闲的线程都可以从队列中获取并执行这个任务。

7

The worker process offloads blocking operations to the thread pool

工作进程将阻塞操作卸给线程池

It seems then we have another queue. Right. But in this case the queue is limited by a specific resource. We can’t read from a drive faster than the drive is capable of producing data. Now at least the drive doesn’t delay processing of other events and only the requests that need to access files are waiting.

The “reading from disk” operation is often used as the most common example of a blocking operation, but actually the thread pools implementation in NGINX can be used for any tasks that aren’t appropriate to process in the main working cycle.

At the moment, offloading to thread pools is implemented only for two essential operations: the read() syscall on most operating systems and sendfile() on Linux. We will continue to test and benchmark the implementation, and we may offload other operations to the thread pools in future releases if there’s a clear benefit.

那么,这就像我们有了另外一个队列。是这样的,但是在这个场景中,队列受限于特殊的资源。磁盘的读取速度不能比磁盘产生数据的速度快。不管怎么说,至少现在磁盘不再延误其他事件,只有访问文件的请求需要等待。

“从磁盘读取”这个操作通常是阻塞操作最常见的示例,但是实际上,NGINX中实现的线程池可用于处理任何不适合在主循环中执行的任务。

目前,卸载到线程池中执行的两个基本操作是大多数操作系统中的read()系统调用和Linux中的sendfile()。接下来,我们将对线程池进行测试(test)和基准测试(benchmark),在未来的版本中,如果有明显的优势,我们可能会卸载其他操作到线程池中。

4. 基准测试(Benchmarking)

t’s time to move from theory to practice. To demonstrate the effect of using thread pools we are going to perform a synthetic benchmark that simulates the worst mix of blocking and non-blocking operations.

It requires a data set that is guaranteed not to fit in memory. On a machine with 48 GB of RAM, we have generated 256 GB of random data in 4-MB files, and then have configured NGINX 1.9.0 to serve it.

The configuration is pretty simple:

现在让我们从理论过度到实践。我们将进行一次模拟基准测试(synthetic benchmark),模拟在阻塞操作和非阻塞操作的最差混合条件下,使用线程池的效果。

另外,我们需要一个内存肯定放不下的数据集。在一台48GB内存的机器上,我们已经产生了每文件大小为4MB的随机数据,总共256GB,然后配置NGINX,版本为1.9.0。

配置很简单:

worker_processes 16;

events {
    accept_mutex off;
}

http {
    include mime.types;
    default_type application/octet-stream;

    access_log off;
    sendfile on;
    sendfile_max_chunk 512k;

    server {
        listen 8000;

        location / {
            root /storage;
        }
    }
}

As you can see, to achieve better performance some tuning was done: logging and accept_mutex were disabled, sendfile was enabled, and sendfile_max_chunk was set. The last directive can reduce the maximum time spent in blocking sendfile() calls, since NGINX won’t try to send the whole file at once, but will do it in 512-KB chunks.

The machine has two Intel Xeon E5645 (12 cores, 24 HT-threads in total) processors and a 10-Gbps network interface. The disk subsystem is represented by four Western Digital WD1003FBYX hard drives arranged in a RAID10 array. All of this hardware is powered by Ubuntu Server 14.04.1 LTS.

如上所示,为了达到更好的性能,我们调整了几个参数:禁用了loggingaccept_mutex ,同时,启用了sendfile 并设置了sendfile_max_chunk的大小。最后一个指令可以减少阻塞调用sendfile()所花费的最长时间,因为NGINX不会尝试一次将整个文件发送出去,而是每次发送大小为512KB的块数据。

这台测试服务器有2个Intel Xeon E5645处理器(共计:12核、24超线程)和10-Gbps的网络接口。磁盘子系统是由4块西部数据WD1003FBYX 磁盘组成的RAID10阵列。所有这些硬件由Ubuntu服务器14.04.1 LTS供电。

8

Configuration of load generators and NGINX for the benchmark

为基准测试配置负载生成器和NGINX

The clients are represented by two machines with the same specifications. On one of these machines, wrk creates load using a Lua script. The script requests files from our server in a random order using 200 parallel connections, and each request is likely to result in a cache miss and a blocking read from disk. Let’s call this load the random load.

On the second client machine we will run another copy of wrk that will request the same file multiple times using 50 parallel connections. Since this file will be frequently accessed, it will remain in memory all the time. In normal circumstances, NGINX would serve these requests very quickly, but performance will fall if the worker processes are blocked by other requests. Let’s call this load the constant load.

The performance will be measured by monitoring throughput of the server machine using ifstat and by obtaining wrk results from the second client.

Now, the first run without thread pools does not give us very exciting results:

客户端有2台服务器,它们的规格相同。在其中一台上,在wrk中使用Lua脚本创建了负载程序。脚本使用200个并行连接向服务器请求文件,每个请求都可能未命中缓存而从磁盘阻塞读取。我们将这种负载称作随机负载。

在另一台客户端机器上,我们将运行wrk的另一个副本,使用50个并行连接多次请求同一个文件。因为这个文件将被频繁地访问,所以它会一直驻留在内存中。在正常情况下,NGINX能够非常快速地服务这些请求,但是如果工作进程被其他请求阻塞的话,性能将会下降。我们将这种负载称作恒定负载。

性能将由服务器上ifstat监测的吞吐率(throughput)和从第二台客户端获取的wrk结果来度量。

现在,没有使用线程池的第一次运行将不会带给我们非常振奋的结果:

% ifstat -bi eth2
eth2
Kbps in  Kbps out
5531.24  1.03e+06
4855.23  812922.7
5994.66  1.07e+06
5476.27  981529.3
6353.62  1.12e+06
5166.17  892770.3
5522.81  978540.8
6208.10  985466.7
6370.79  1.12e+06
6123.33  1.07e+06

As you can see, with this configuration the server is able to produce about 1 Gbps of traffic in total. In the output from top, we can see that all of worker processes spend most of the time in blocking I/O (they are in a D state):

如上所示,使用这种配置,服务器产生的总流量约为1Gbps。从下面所示的top输出,我们可以看到,工作进程的大部分时间花在阻塞I/O上(它们处于top的D状态):

top - 10:40:47 up 11 days,  1:32,  1 user,  load average: 49.61, 45.77 62.89
Tasks: 375 total,  2 running, 373 sleeping,  0 stopped,  0 zombie
%Cpu(s):  0.0 us,  0.3 sy,  0.0 ni, 67.7 id, 31.9 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem:  49453440 total, 49149308 used,   304132 free,    98780 buffers
KiB Swap: 10474236 total,    20124 used, 10454112 free, 46903412 cached Mem

  PID USER     PR  NI    VIRT    RES     SHR S  %CPU %MEM    TIME+ COMMAND
 4639 vbart    20   0   47180  28152     496 D   0.7  0.1  0:00.17 nginx
 4632 vbart    20   0   47180  28196     536 D   0.3  0.1  0:00.11 nginx
 4633 vbart    20   0   47180  28324     540 D   0.3  0.1  0:00.11 nginx
 4635 vbart    20   0   47180  28136     480 D   0.3  0.1  0:00.12 nginx
 4636 vbart    20   0   47180  28208     536 D   0.3  0.1  0:00.14 nginx
 4637 vbart    20   0   47180  28208     536 D   0.3  0.1  0:00.10 nginx
 4638 vbart    20   0   47180  28204     536 D   0.3  0.1  0:00.12 nginx
 4640 vbart    20   0   47180  28324     540 D   0.3  0.1  0:00.13 nginx
 4641 vbart    20   0   47180  28324     540 D   0.3  0.1  0:00.13 nginx
 4642 vbart    20   0   47180  28208     536 D   0.3  0.1  0:00.11 nginx
 4643 vbart    20   0   47180  28276     536 D   0.3  0.1  0:00.29 nginx
 4644 vbart    20   0   47180  28204     536 D   0.3  0.1  0:00.11 nginx
 4645 vbart    20   0   47180  28204     536 D   0.3  0.1  0:00.17 nginx
 4646 vbart    20   0   47180  28204     536 D   0.3  0.1  0:00.12 nginx
 4647 vbart    20   0   47180  28208     532 D   0.3  0.1  0:00.17 nginx
 4631 vbart    20   0   47180    756     252 S   0.0  0.1  0:00.00 nginx
 4634 vbart    20   0   47180  28208     536 D   0.0  0.1  0:00.11 nginx
 4648 vbart    20   0   25232   1956    1160 R   0.0  0.0  0:00.08 top
25921 vbart    20   0  121956   2232    1056 S   0.0  0.0  0:01.97 sshd
25923 vbart    20   0   40304   4160    2208 S   0.0  0.0  0:00.53 zsh

In this case the throughput is limited by the disk subsystem, while the CPU is idle most of the time. The results from wrk are also very low:

在这种情况下,吞吐率受限于磁盘子系统,而CPU在大部分时间里是空闲的。从wrk获得的结果也非常低:

Running 1m test @ http://192.0.2.1:8000/1/1/1
  12 threads and 50 connections
  Thread Stats   Avg    Stdev     Max  +/- Stdev
    Latency     7.42s  5.31s   24.41s   74.73%
    Req/Sec     0.15    0.36     1.00    84.62%
  488 requests in 1.01m, 2.01GB read
Requests/sec:      8.08
Transfer/sec:     34.07MB

And remember, this is for the file that should be served from memory! The excessively large latencies are because all the worker processes are busy with reading files from the drives to serve the random load created by 200 connections from the first client, and cannot handle our requests in good time.

It’s time to put our thread pools in play. For this we just add the aio threads directive to the location block:

请记住,文件是从内存送达的!第一个客户端的200个连接创建的随机负载,使服务器端的全部的工作进程忙于从磁盘读取文件,因此产生了过大的延迟,并且无法在合理的时间内处理我们的请求。

现在,我们的线程池要登场了。为此,我们只需在location块中添加aio threads指令:

location / {
    root /storage;
    aio threads;
}

and ask NGINX to reload its configuration.

After that we repeat the test:

接着,执行NGINX reload重新加载配置。

然后,我们重复上述的测试:

% ifstat -bi eth2
eth2
Kbps in  Kbps out
60915.19  9.51e+06
59978.89  9.51e+06
60122.38  9.51e+06
61179.06  9.51e+06
61798.40  9.51e+06
57072.97  9.50e+06
56072.61  9.51e+06
61279.63  9.51e+06
61243.54  9.51e+06
59632.50  9.50e+06

Now our server produces 9.5 Gbps, compared to ~1 Gbps without thread pools!

It probably could produce even more, but it has already reached the practical maximum network capacity, so in this test NGINX is limited by the network interface. The worker processes spend most of the time just sleeping and waiting for new events (they are in S state in top):

现在,我们的服务器产生的流量是9.5Gbps,相比之下,没有使用线程池时只有约1Gbps!

理论上还可以产生更多的流量,但是这已经达到了机器的最大网络吞吐能力,所以在这次NGINX的测试中,NGINX受限于网络接口。工作进程的大部分时间只是休眠和等待新的事件(它们处于topS状态):

top - 10:43:17 up 11 days,  1:35,  1 user,  load average: 172.71, 93.84, 77.90
Tasks: 376 total,  1 running, 375 sleeping,  0 stopped,  0 zombie
%Cpu(s):  0.2 us,  1.2 sy,  0.0 ni, 34.8 id, 61.5 wa,  0.0 hi,  2.3 si,  0.0 st
KiB Mem:  49453440 total, 49096836 used,   356604 free,    97236 buffers
KiB Swap: 10474236 total,    22860 used, 10451376 free, 46836580 cached Mem

  PID USER     PR  NI    VIRT    RES     SHR S  %CPU %MEM    TIME+ COMMAND
 4654 vbart    20   0  309708  28844     596 S   9.0  0.1  0:08.65 nginx
 4660 vbart    20   0  309748  28920     596 S   6.6  0.1  0:14.82 nginx
 4658 vbart    20   0  309452  28424     520 S   4.3  0.1  0:01.40 nginx
 4663 vbart    20   0  309452  28476     572 S   4.3  0.1  0:01.32 nginx
 4667 vbart    20   0  309584  28712     588 S   3.7  0.1  0:05.19 nginx
 4656 vbart    20   0  309452  28476     572 S   3.3  0.1  0:01.84 nginx
 4664 vbart    20   0  309452  28428     524 S   3.3  0.1  0:01.29 nginx
 4652 vbart    20   0  309452  28476     572 S   3.0  0.1  0:01.46 nginx
 4662 vbart    20   0  309552  28700     596 S   2.7  0.1  0:05.92 nginx
 4661 vbart    20   0  309464  28636     596 S   2.3  0.1  0:01.59 nginx
 4653 vbart    20   0  309452  28476     572 S   1.7  0.1  0:01.70 nginx
 4666 vbart    20   0  309452  28428     524 S   1.3  0.1  0:01.63 nginx
 4657 vbart    20   0  309584  28696     592 S   1.0  0.1  0:00.64 nginx
 4655 vbart    20   0  30958   28476     572 S   0.7  0.1  0:02.81 nginx
 4659 vbart    20   0  309452  28468     564 S   0.3  0.1  0:01.20 nginx
 4665 vbart    20   0  309452  28476     572 S   0.3  0.1  0:00.71 nginx
 5180 vbart    20   0   25232   1952    1156 R   0.0  0.0  0:00.45 top
 4651 vbart    20   0   20032    752     252 S   0.0  0.0  0:00.00 nginx
25921 vbart    20   0  121956   2176    1000 S   0.0  0.0  0:01.98 sshd
25923 vbart    20   0   40304   3840    2208 S   0.0  0.0  0:00.54 zsh

There are still plenty of CPU resources.

The results of wrk:

如上所示,基准测试中还有大量的CPU资源剩余。

wrk的结果如下:

Running 1m test @ http://192.0.2.1:8000/1/1/1
  12 threads and 50 connections
  Thread Stats   Avg      Stdev     Max  +/- Stdev
    Latency   226.32ms  392.76ms   1.72s   93.48%
    Req/Sec    20.02     10.84    59.00    65.91%
  15045 requests in 1.00m, 58.86GB read
Requests/sec:    250.57
Transfer/sec:      0.98GB

The average time to serve a 4-MB file has been reduced from 7.42 seconds to 226.32 milliseconds (33 times less), and the number of requests per second has increased by 31 times (250 vs 8)!

The explanation is that our requests no longer wait in the events queue for processing while worker processes are blocked on reading, but are handled by free threads. As long as the disk subsystem is doing its job as best it can serving our random load from the first client machine, NGINX uses the rest of the CPU resources and network capacity to serve requests of the second client from memory.

服务器处理4MB文件的平均时间从7.42秒降到226.32毫秒(减少了33倍),每秒请求处理数提升了31倍(250 vs 8)!

对此,我们的解释是请求不再因为工作进程被阻塞在读文件,而滞留在事件队列中,等待处理,它们可以被空闲的进程处理掉。只要磁盘子系统能做到最好,就能服务好第一个客户端上的随机负载,NGINX可以使用剩余的CPU资源和网络容量,从内存中读取,以服务于上述的第二个客户端的请求。

5. 依然没有银弹(Still Not a Silver Bullet)

After all our fears about blocking operations and some exciting results, probably most of you already are going to configure thread pools on your servers. Don’t hurry.

The truth is that fortunately most read and send file operations do not deal with slow hard drives. If you have enough RAM to store the data set, then an operating system will be clever enough to cache frequently used files in a so-called “page cache”.

The page cache works pretty well and allows NGINX to demonstrate great performance in almost all common use cases. Reading from the page cache is quite quick and no one can call such operations “blocking.” On the other hand, offloading to a thread pool has some overhead.

So if you have a reasonable amount of RAM and your working data set isn’t very big, then NGINX already works in the most optimal way without using thread pools.

Offloading read operations to the thread pool is a technique applicable to very specific tasks. It is most useful where the volume of frequently requested content doesn’t fit into the operating system’s VM cache. This might be the case with, for instance, a heavily loaded NGINX-based streaming media server. This is the situation we’ve simulated in our benchmark.

It would be great if we could improve the offloading of read operations into thread pools. All we need is an efficient way to know if the needed file data is in memory or not, and only in the latter case should the reading operation be offloaded to a separate thread.

Turning back to our sales analogy, currently the salesman cannot know if the requested item is in the store and must either always pass all orders to the delivery service or always handle them himself.

The culprit is that operating systems are missing this feature. The first attempts to add it to Linux as the fincore() syscall were in 2010 but that didn’t happen. Later there were a number of attempts to implement it as a new preadv2() syscall with the RWF_NONBLOCK flag (see Non-blocking buffered file read operations and Asynchronous buffered read operations at LWN.net for details). The fate of all these patches is still unclear. The sad point here is that it seems the main reason why these patches haven’t been accepted yet to the kernel is continuous bikeshedding.

On the other hand, users of FreeBSD don’t need to worry at all. FreeBSD already has a sufficiently good asynchronous interface for reading files, which you should use instead of thread pools.

在抛出我们对阻塞操作的担忧并给出一些令人振奋的结果后,可能大部分人已经打算在你的服务器上配置线程池了。先别着急。

实际上,最幸运的情况是,读取和发送文件操作不去处理缓慢的硬盘驱动器。如果我们有足够多的内存来存储数据集,那么操作系统将会足够聪明地在被称作“页面缓存”的地方,缓存频繁使用的文件。

“页面缓存”的效果很好,可以让NGINX在几乎所有常见的用例中展示优异的性能。从页面缓存中读取比较快,没有人会说这种操作是“阻塞”。而另一方面,卸载任务到一个线程池是有一定开销的。

因此,如果内存有合理的大小并且待处理的数据集不是很大的话,那么无需使用线程池,NGINX已经工作在最优化的方式下。

卸载读操作到线程池是一种适用于非常特殊任务的技术。只有当经常请求的内容的大小,不适合操作系统的虚拟机缓存时,这种技术才是最有用的。至于可能适用的场景,比如,基于NGINX的高负载流媒体服务器。这正是我们已经模拟的基准测试的场景。

我们如果可以改进卸载读操作到线程池,将会非常有意义。我们只需要知道所需的文件数据是否在内存中,只有不在内存中时,读操作才应该卸载到一个单独的线程中。

再回到售货员那个比喻的场景中,这回,售货员不知道要买的商品是否在店里,他必须要么总是将所有的订单提交给配货服务,要么总是亲自处理它们。

人艰不拆,操作系统缺少这样的功能。第一次尝试是在2010年,人们试图将这一功能添加到Linux作为fincore()系统调用,但是没有成功。后来还有一些尝试,是使用RWF_NONBLOCK标记作为preadv2()系统调用来实现这一功能(详情见LWN.net上的非阻塞缓冲文件读取操作异步缓冲读操作)。但所有这些补丁的命运目前还不明朗。悲催的是,这些补丁尚没有被内核接受的主要原因,貌似是因为旷日持久的撕逼大战(bikeshedding)。

另一方面,FreeBSD的用户完全不必担心。FreeBSD已经具备足够好的异步读取文件接口,我们应该用这个接口而不是线程池。

6. 配置线程池(Configuring Thread Pools)

So if you are sure that you can get some benefit out of using thread pools in your use case, then it’s time to dive deep into configuration.

The configuration is quite easy and flexible. The first thing you should have is NGINX version 1.7.11 or later, compiled with the --with-threads configuration parameter. In the simplest case, the configuration looks very plain. All you need is to include the aio threads directive in the http, server, or location context:

所以,如果你确信在你的场景中使用线程池可以带来好处,那么现在是时候深入了解线程池的配置了。

线程池的配置非常简单、灵活。首先,获取NGINX 1.7.11或更高版本的源代码,使用--with-threads配置参数编译。在最简单的场景中,配置看起来很朴实。我们只需要在httpserver,或者location上下文中包含aio threads指令即可:

aio threads;

This is the minimal possible configuration of thread pools. In fact, it’s a short version of the following configuration:

这是线程池的最简配置。实际上的精简版本示例如下:

thread_pool default threads=32 max_queue=65536;
aio threads=default;

It defines a thread pool called default with 32 working threads and a maximum length for the task queue of 65536 requests. If the task queue is overloaded, NGINX logs this error and rejects the request:

这里定义了一个名为“default”,包含32个线程,任务队列最多支持65536个请求的线程池。如果任务队列过载,NGINX将输出如下错误日志并拒绝请求:

thread pool "NAME" queue overflow: N tasks waiting

The error means it’s possible that the threads aren’t able to handle the work as quickly as it is added to the queue. You can try increasing the maximum queue size, but if that doesn’t help, then it indicates that your system is not capable of serving so many requests.

As you already noticed, with the thread_pool directive you can configure the number of threads, the maximum length of the queue, and the name of a specific thread pool. The last implies that you can configure several independent thread pools and use them in different places of your configuration file to serve different purposes:

错误输出意味着线程处理作业的速度有可能低于任务入队的速度了。你可以尝试增加队列的最大值,但是如果这无济于事,那么这说明你的系统没有能力处理如此多的请求了。

正如你已经注意到的,你可以使用 thread_pool指令,配置线程的数量、队列的最大值,以及线程池的名称。最后要说明的是,可以配置多个独立的线程池,将它们置于不同的配置文件中,用做不同的目的:

http {
    thread_pool one threads=128 max_queue=0;
    thread_pool two threads=32;

    server {
        location /one {
            aio threads=one;
        }

        location /two {
            aio threads=two;
        }
    }
…
}

If the max_queue parameter isn’t specified, the value 65536 is used by default. As shown, it’s possible to set max_queue to zero. In this case the thread pool will only be able to handle as many tasks as there are threads configured; no tasks will wait in the queue.

Now let’s imagine you have a server with three hard drives and you want this server to work as a «caching proxy» that caches all responses from your back ends. The expected amount of cached data far exceeds the available RAM. It’s actually a caching node for your personal CDN. Of course in this case the most important thing is to achieve maximum performance from the drives.

One of your options is to configure a RAID array. This approach has its pros and cons. Now with NGINX you can take another one:

如果没有指定max_queue参数的值,默认使用的值是65536。如上所示,可以设置max_queue为0。在这种情况下,线程池将使用配置中全部数量的线程,尽可能地同时处理多个任务;队列中不会有等待的任务。

现在,假设我们有一台服务器,挂了3块硬盘,我们希望把该服务器用作“缓存代理”,缓存后端服务器的全部响应信息。预期的缓存数据量远大于可用的内存。它实际上是我们个人CDN的一个缓存节点。毫无疑问,在这种情况下,最重要的事情是发挥硬盘的最大性能。

我们的选择之一是配置一个RAID阵列。这种方法毁誉参半,现在,有了NGINX,我们可以有其他的选择:

# 我们假设每块硬盘挂载在相应的目录中:/mnt/disk1、/mnt/disk2、/mnt/disk3

proxy_cache_path /mnt/disk1 levels=1:2 keys_zone=cache_1:256m max_size=1024G
                 use_temp_path=off;
proxy_cache_path /mnt/disk2 levels=1:2 keys_zone=cache_2:256m max_size=1024G
                 use_temp_path=off;
proxy_cache_path /mnt/disk3 levels=1:2 keys_zone=cache_3:256m max_size=1024G
                 use_temp_path=off;

thread_pool pool_1 threads=16;
thread_pool pool_2 threads=16;
thread_pool pool_3 threads=16;

split_clients $request_uri $disk {
    33.3%     1;
    33.3%     2;
    *         3;
}

location / {
    proxy_pass http://backend;
    proxy_cache_key $request_uri;
    proxy_cache cache_$disk;
    aio threads=pool_$disk;
    sendfile on;
}

n this configuration three independent caches are used, dedicated to each of the disks, and three independent thread pools are dedicated to the disks as well.

The split_clients module is used for load balancing between the caches (and as a result between the disks), which perfectly fits this task.

The use_temp_path=off parameter to the proxy_cache_path directive instructs NGINX to save temporary files into the same directories where the corresponding cache data is located. It is needed to avoid copying response data between the hard drives when updating our caches.

All this together allows us to get maximum performance out of the current disk subsystem, because NGINX through separate thread pools interacts with the drives in parallel and independently. Each of the drives is served by 16 independent threads with a dedicated task queue for reading and sending files.

I bet your clients like this custom-tailored approach. Be sure that your hard drives like it too.

This example is a good demonstration of how flexibly NGINX can be tuned specifically for your hardware. It’s like you are giving instructions to NGINX about the best way to interact with the machine and your data set. And by fine-tuning NGINX in user space, you can ensure that your software, operating system, and hardware work together in the most optimal mode to utilize all the system resources as effectively as possible.

在这份配置中,使用了3个独立的缓存,每个缓存专用一块硬盘,另外,3个独立的线程池也各自专用一块硬盘。

缓存之间(其结果就是磁盘之间)的负载均衡使用split_clients模块,split_clients非常适用于这个任务。

split_clients指令中设置use_temp_path=off,表示NGINX会将临时文件保存在缓存数据的同一目录中。这是为了避免在更新缓存时,磁盘之间互相复制响应数据。

这些调优将带给我们磁盘子系统的最大性能,因为NGINX通过单独的线程池并行且独立地与每块磁盘交互。每块磁盘由16个独立线程和读取和发送文件专用任务队列提供服务。

我敢打赌,你的客户喜欢这种量身定制的方法。请确保你的磁盘也持有同样的观点。

这个示例很好地证明了NGINX可以为硬件专门调优的灵活性。这就像你给NGINX下了一道命令,让机器和数据用最佳姿势来搞基。而且,通过NGINX在用户空间中细粒度的调优,我们可以确保软件、操作系统和硬件工作在最优模式下,尽可能有效地利用系统资源。

7. 总结(Conclusion)

Summing up, thread pools is a great feature that pushes NGINX to new levels of performance by eliminating one of its well-known and long-time enemies – blocking – especially when we are speaking about really large volumes of content.

And there is even more to come. As previously mentioned, this brand-new interface potentially allows offloading of any long and blocking operation without any loss of performance. NGINX opens up new horizons in terms of having a mass of new modules and functionality. Lots of popular libraries still do not provide an asynchronous non-blocking interface, which previously made them incompatible with NGINX. We may spend a lot of time and resources on developing our own non-blocking prototype of some library, but will it always be worth the effort? Now, with thread pools on board, it is possible to use such libraries relatively easily, making such modules without an impact on performance.

Stay tuned.

综上所述,线程池是一个伟大的功能,将NGINX推向了新的性能水平,除掉了一个众所周知的长期危害——阻塞——尤其是当我们真正面对大量内容的时候。

甚至,还有更多的惊喜。正如前面提到的,这个全新的接口,有可能没有任何性能损失地卸载任何长期阻塞操作。NGINX在拥有大量的新模块和新功能方面,开辟了一方新天地。许多流行的库仍然没有提供异步非阻塞接口,此前,这使得它们无法与NGINX兼容。我们可以花大量的时间和资源,去开发我们自己的无阻塞原型库,但这么做始终都是值得的吗?现在,有了线程池,我们可以相对容易地使用这些库,而不会影响这些模块的性能

jack.zh 标签:Nginx 继续阅读

2082 ℉

15.06.17

2015总决赛结局:勇士登顶,以及,我们正在观赏伟大这个词的全部含义

第六场前,科林斯和杰伦-罗斯围着韦德讨论:如果勒布朗输了,能不能拿到总决赛MVP呢?大家的口气,仿佛韦德是勒布朗的亲爸爸:

“看着儿子单飞了,有什么看法呀老先生?”

韦德有点不爽了,打断罗斯,提醒他先别讨论勒布朗输了怎么办,“他满有机会赢总冠军呢!”

然后他回忆起三年前的光荣岁月,对凯尔特人那传奇的第六场前。他说当时但见勒布朗表情木然,不知是忧是喜。直到开赛前,勒布朗做了一个表情,韦德便知道没问题了——那就是勒布朗半场30分、全场45分、震服波士顿花园之夜。

韦德学了一下,那表情是这样的:

而今天比赛前,勒布朗的表情是这样的:

哦对了。最后一件事。

韦德今天解说时,说到“我以前在迈阿密的时候……”

他和迈阿密的合同问题,众所周知。这是他要离开迈阿密了么?


业已进行的战局,我们都知道了:

  • 第一场战得难解难分,然后欧文受伤。
  • 第二、三场,骑士放任巴恩斯、格林和博古特不管,夹击库里,放慢节奏,干掉了勇士。
  • 第四场,勇士上伊戈达拉首发,替下博古特,全小球阵容。再利用库里+格林挡拆联动,干掉骑士。骑士用双内线居高临下冲了12个前场篮板合计25投16中,无效。
  • 第五场,勇士继续伊戈达拉首发,骑士也用小球阵容应对,但关键时刻,依然被库里——格林——伊戈达拉连线击败。
  • 第六场了。

八年前,凤凰城太阳与马刺西部半决赛第二场,被迫用上科特-托马斯首发,放弃全小球速度。理由?第一场,太阳上纳什+贝尔+詹姆斯-琼斯+马里昂+小斯塔德迈尔。结果:邓肯33分16篮板,帕克突破随心所欲32分。

反过来,达拉斯小牛首轮战勇士,最后勇士排了巴朗-戴维斯+理查德森+巴恩斯+杰克船长+别德林斯+皮特鲁斯+埃利斯七人轮换,只有别德林斯一个内线。小牛不敌,因为德克那时还没练出无敌的背身后仰金鸡独立,根本无法搞定勇士内线。

第四场,骑士用内线攻勇士,未遂;第五场,骑士陪勇士跑,未遂。第六场,布拉特教练决定,内线。

骑士开局的攻防:香波特、莫兹科夫、特里斯坦、汤普森,人人都往内线去强攻篮下,趁乱抓篮板;防守端,强侧施压,弱侧则尽力切断传球线路。赌注下得不小:伊戈达拉、库里、克雷-汤普森们都有空位机会,但投不进。骑士一波7比2。那是他们全场气势最盛的时候。

然后,伊戈达拉出来了:

造了勒布朗的进攻犯规。挡住莫兹科夫让格林上篮。抄掉勒布朗的球让库里反击上篮,阻绝球。骑士半节5次失误,因为伊戈达拉对勒布朗的组织思路很理解。

反过来,伊戈达拉的组织就没人管。他在弧顶呼风唤雨,莫兹科夫只好在禁区呆看。伊戈达拉中投、找库里、投三分。到库里——巴恩斯——伊戈达拉一条连线完成后,勇士23比15领先。

到这里,骑士赌输了:他们继续不管伊戈达拉,希望他的手冷,然而伊戈达拉连投篮带组织,随心所欲。前十分钟,勇士投进10个球10个助攻,骑士则多达7个失误。第一节勇士28比15后,骑士的双内线攻势,算是失败了。

第二节,骑士换套路,勒布朗试图多找两个内线挡拆来突破分球,防守端尽量阻绝传球后单打。莫兹科夫在禁区送出3个封盖。骑士追分时,又是伊戈达拉:反击中回传巴恩斯三分球,37比29。

然后就是他的第三次犯规。下场休息。然后就是骑士一口气追到43比45,半场只差2分。伊戈达拉下场的3分钟里,勒布朗得到6分。实际上,勒布朗半场14投6中。

半场休息时,杰伦-罗斯又自得其乐嚷嚷“勒布朗继续突破!你会得到哨子的!不要跳投!”——半场骑士21次罚球,勇士4次。

韦德听不下去了,插嘴:“勒布朗是个组织者!他是试图让每个人融入进来!”

的确如此。下半场一开始,勒布朗继续找双内线,莫兹科夫和特里斯坦-汤普森各一个上篮后,骑士47比45反超。

那是今天他们最好的机会,但是:

  • 伊戈达拉抄球,巴恩斯追身三分。
  • 巴恩斯断球,伊戈达拉反击扣篮。

骑士只好放弃莫兹科夫。注意:骑士每次替下莫兹科夫,都意味着“我们防守端就是拿伊戈达拉没办法!” 伊戈达拉突破,伊戈达拉扣篮,加上利文斯顿的妖异、库里的反击,骑士勇士领先到61比51。

此后是漫长的胶着,直到第四节,勒布朗弱侧抄球突击前场奋力扣篮,重新点燃球场。但之后:

常规赛MVP出现。库里三分球,利文斯顿补扣,JR史密斯三分球,但库里——格林——伊戈达拉连线还以三分球。库里和克雷-汤普森再补两个三分球,加上库里——格林——伊戈达拉连线的三分球,勇士领先到92比77。

以及:勇士的每一次小球阵容快速缩放:

勇士输了无数篮板,但他们同样造了无数失误。这场赌局,他们赢了。

在每一个骑士可能起势的瞬间,伊戈达拉都稳稳地梗住了骑士——也梗住了勒布朗的咽喉。最后时刻,JR史密斯的连续三分球,仿佛两年前马刺vs热第六场超级翻盘的阴影重现,但伊戈达拉又跳出来,抓了两个后场篮板。

然后,金州勇士四十年的等待,得到了回报。


克里夫兰骑士拼到了最后时刻。就在JR史密斯迟来的三分球不断坠入篮筐时,解说员布林老师诧异地道: “骑士还在战斗?”

——因为早五分钟前,他已经和范甘迪聊起“明年骑士会不会变得很强扒拉扒拉”之类话题了。

JR史密斯在第五场上半场与本场最后几秒钟,让系列赛显得刀光剑影。

詹姆斯-琼斯和迈克-米勒作为勒布朗的私人武装……做得可以了。

德拉维多瓦已经靠第二和第三场一举成名,而且伊戈达拉应该给他写封感谢信。

特里斯坦-汤普森本系列赛场均10分13篮板。会有球队愿意付他三年4500万的。

莫兹科夫证明了,在一个博古特都站不稳的舞台上,他站稳了。

巴博萨在第五场为太阳队挣回了面子,今天第四节他一个底线上篮时,布林老师诧异地:“啊居然是巴博萨在屠杀骑士”——嘿,有那么让人奇怪吗?

——这倒让人想起一个话题:巴博萨、迪奥和马里昂这些太阳旧将,都从勒布朗身上得到了戒指;当然,论这方面,谁都不如詹姆斯-琼斯。

利文斯顿是这个系列赛的暗影游魂,每次勇士吊不过气来,他便拯救。

克雷-汤普森……好吧,总决赛第二场他打得不错。

德雷蒙-格林用一个三双结束了总决赛。会有球队朝他舞顶薪合同的了。

巴恩斯在第五和第六场终于醒过来了。

艾泽利今天的那记补扣打三分熄灭了速贷球馆的呼声。

大卫-李代替博古特成为系列赛的转折点之一。


布拉特教练做了他能够做的一切。在欧文受伤后,选择合适的轮换,激励士气,变换防守方式。一个细节:

总决赛前五场,骑士只放给勇士17个大空位(最近的对手在两米开外),反过来,骑士获得了27个大空位。

勇士在对手距离一米二到两米之间的准空位下,46投25中。而骑士在对手离自己起码两米的大空位,27投只有10中。

这是骑士防守的胜利,以及射术的大失败——当然,那就是另一回事了。

关于斯蒂夫-科尔教练,举个例子。

有个球员,叫做卢-阿尔辛多。在两年的NBA生涯里,他拿了一个常规赛MVP、一个NBA年度新人、一个得分王、一个总冠军、一个总决赛MVP、一次NBA二阵、一次NBA一阵。如果到此退役,他也必然是史上最伟大球员之一了——嗯,直到他1971年二年级时改名贾巴尔。

同理。斯蒂夫-科尔教练如果现在辞职不干,也已经名垂青史了。常规赛67胜,新秀年总冠军,完美的人生。哪怕现在就退休,他也会因为勇士这支史上最伟大三分球队之一而被铭记,他会因为总决赛第四场开始伊戈达拉首发这个神话般的举措而被铭记。他真正做到了“五小球首发”,居然还能夺冠。虽然是天时地利人和,但老尼尔森、丹东尼、莱利、道格-莫们未能完成的伟大革命,他完成了。

许多人因为勇士常规赛67胜,觉得他们夺冠理所当然,忘记了一些其他事。就在2014年10月,美国诸家媒体是这么说的:Bleachr预测勇士47胜35负;CBS的三位专家分别猜勇士会是西部第三、五和六;FOX体育预测勇士西部第五。那时节,大家可没有觉得勇士有那么堆山填海的重磅天分哟!实际上,那会儿大家还在担心大卫-李受伤、小奥尼尔远去,勇士的内线怎么办呢。

他们的无私和团队精神成就了这一切,而科尔,当然是关键的关键。

季前写的:

暂时看来,科尔是库里能找到的,最好老师。射手和射手是能彼此理解的;科尔有五枚戒指;在历史上最好的射手球队里(太阳,嗯)担当过管家;他懂战术(而库里是个愿意遵循战术的好孩子)。相比于马克-杰克逊那样更注重球员情绪激发的教练,科尔能给库里一些其他的提示。

以及这件事:

“我知道这话马上就会上推特,但我还是得说,斯蒂芬的高尔夫打得比迈克尔好。”科尔如是说。

嗯。现在,他有资格说许多其他话了。


斯蒂芬-库里这个总决赛MVP丢得有些可惜。他依然是球队第一得分手兼发动机,数据无法形容他对球队胜利的意义。我们只能一再回放库里——格林——任何一个空位球员的录像。连续四场比赛的第四节,他都让骑士胆战心惊。他是勇士的创造者,只差没有像勒布朗似的,将每个球喂到队友嘴里。格林的每次助攻,都该分他一半才是。

但他好好歹歹,完成了一个伟业。虽然去年马刺夺冠,帕克是马刺首席得分手+助攻王,但没人会说马刺是帕克的球队;但从今以后,我们都知道,2015年总冠军是斯蒂芬-库里的球队。常规赛MVP和总决赛数据照耀着这一切。这是基德与纳什始终未能完成、只有魔术师与刺客完成过的伟业。当然,他不是一个纯控卫,但那是另一个话题了。


安德烈-伊戈达拉。当总裁说“总决赛MVP是个一直没打上首发的人”时,我就想:呃。

季前写的,勇士十点里,我把他放在了第二个。

伊戈达拉是上季勇士首席“光看数据根本不知道他干了啥”的家伙。他场均32分钟里9分5篮板4助攻,但是:

他出赛的63场比赛,勇士41胜22负。他没打的19场比赛,勇士10胜9负。

他的存在,以及博古特多打的1000分钟,让勇士的防守排名提高了十位。

虽然他有哈里森这个顶尖替补,但他上季的+/-值达到了全队最高的17.7,甚至超过库里。

当然,如果你愿意多提一句的话……失去了他的掘金,防守也跌到了谷底……但那是另一个话题了。

总而言之,离开了费城两年,伊戈达拉的数据连创新低,但在丹佛,他让掘金完成了队史纪录的57胜;在勇士,他让球队自1992年后再次达到51胜。顺便:勇士队史一共也才五次50胜。长此以往,他真的开始要变成翼侧的基德了:“我们不在乎自己的得分;我们什么都能干一点,不过也不想多刷;我们就想跑起来,让球队赢球,然后深藏功与名”。这很怪异,因为他大概是联盟里仅有的“场均得不到二位数分数或篮板或助攻却领着千万年薪还倍受赞誉”的人了。

天晓得。入行时就被认为是未来明星,二年级开始被当做未来皮彭,2008-09季仿佛迷你勒布朗,但之后越来越证明他长了明星身子却只想当个角色球员,甚至让泰德斯-杨成为球队首席持球攻击手;当然,因为他的防守与全面从来不坐板凳,本季之前漫长的十年职业生涯全部首发——然后本季一直坐替补到总决赛第三场。

忽然间,就天摇地动了。

他的首发意义,不再赘言。他对勒布朗的防守,全世界都看到了。连续第二年,总决赛MVP给了一个“大家都觉得你有些像蓝领,然而你还是忽然爆发了,在总决赛最后三场跟勒布朗对着干,然后拿到冠军”的小前锋。

而且,他是靠若干个定点三分球来刷分的——六年前,费城球迷都在念叨他和安德烈-米勒这俩货不投三分,实在是球队进攻的顽疾来着。

可以很安全的说,不算莱纳德(他还有未来呢),伊戈达拉是NBA史上,第二草根的总决赛MVP,只有1981年的塞德里克-马斯威尔比他更草根。

他一直是个好球员,但不够伟大;他只进过一次全明星,以后也未必能进。他得总决赛MVP,一半是他的卓越表现,一半是……他的出镜,太戏剧性了。

当然,还有一个重要理由是:他对位的是勒布朗-詹姆斯。


勒布朗-詹姆斯在2015年总决赛,完成了NBA总决赛史上最伟大的败者表演,可能没有之一。

——当然,1974年总决赛,天钩七场场均33分12篮板5助攻2封盖,第六场送出传奇绝杀。1969年韦斯特场均38分,第七场三双。1967年巴里场均41分9篮板3助攻抵抗了张伯伦六场。1977年J博士场均30+6+5。其他包括1993年的巴克利、2001年的艾弗森,都上演了惨烈的对抗。

但天钩身边有丹德里奇和奥斯卡(虽然老了);韦斯特身边有贝勒和张伯伦;J博士身边有麦金尼斯。勒布朗,自从第一场后,确实只有他自己。他也确实做了一切。实际上,你可以问一个篮球手要的:持球、组织、远射、突破、罚球、单防、协防、篮板、封盖、长传、推反击、助攻空位队友、寻找空切队友、抛射、上篮、背身——他都做了一遍。

这个系列赛在欧文倒下后,纸面悬念便已消失;奇妙的是,越是这类场合,勒布朗却似乎越能负隅顽抗,仿佛角斗士面对野兽般血战到底。韦德今天在赛前,重新提起那个话题:

2011年总决赛,勒布朗需要思考这里要不要打,那里要不要打,然后就出问题了……当他不需要特意去思考这里那里时,他便所向无敌,“我就是保证他可以不用多思考”。

很奇怪。勒布朗2006年与活塞、2007年与马刺、2008年与凯尔特人、2009年与魔术,都血战到了最后。2010年,他选择了那条众所周知的道路,但在2011年有所选择时,却输给了小牛。2013年,他在总决赛前五场患得患失,然后在第七场毫无退路时打出了不朽表现(单场37分);今年亦然:在欧文倒下后,当他发觉自己无可依靠,必须独自解决一切时,勒布朗反而会打出他最好的表现——是否赢球那是结果,但过程方面,他处理得无可挑剔。包括今天,从开场让全队融入进攻,到自己的接管时机,到下半场悬念已经丢失,还在奋力挽回。

系列赛前,他说现在的自己是最强的自己。如果看过2009年或2013年的他,便不难明白,如今他的面筐突击、远射与防守,都已不如巅峰期;2011年之后,他一直是在用自己日渐长进的投篮、大局观和背身来弥补爆发力的流逝,但这个系列赛,你可以理解“最强的勒布朗”的含义。这是我们所见过的,心志上——聪慧、斗志、热血——最完美的勒布朗。2011年,你可以说,他是明明可以做到却不做,那么今年确实,他的心尽到了,许多时候,只是短一点力。当然,你没法要求那么多:你可以说狮子捕不到鹰是因为没有翅膀,但那样要求就太违背自然了。

但他好歹让克里夫兰骑士打出队史最好的一年,也让自己打出了职业生涯最好的一个系列赛。最重要的是,就像1998年的乔丹带着老去的双腿似的,我们一直想看“勒布朗如果真把自己压榨到极限,会是什么样子?”我们看到了。

结果并不美妙,但就像2001年的艾弗森、1993年的巴克利、1988年的刺客、1981年的摩西-马龙似的,我们有了一个新的英雄故事。1988年那著名的第七场,伯德末节20分,威尔金斯全场47分时,解说员说:“你们在观赏伟大这个词的全部含义!”伟大巨星就是能把自己榨得一点不剩,这也是为什么2007年之前的KG、艾弗森、巴克利这些名字,伟大到能够让人们忽略他们结果是否得到戒指。

勒布朗-詹姆斯的2015年总决赛之旅。很伟大,也因此让金州勇士的2014-15季之旅有一个精彩收尾。许多年后,我们不会说“勇士真正的考验在对灰熊就结束了”,类似于2003年马刺击败湖人后便仿佛一马平川了似的。勒布朗榨干了自己,上演了一场壮烈拼搏。有点残忍,但作为篮球迷,我们要的也就是这个了。

“我们正在观赏伟大这个词的全部含义”,差不多就是这样。

Note:

我不惮以最坏的恶意来揣测那些评FMVP的媒体,他们就是感觉LBJ没有实现英雄的胜利而打压库里(我这里没相对一哥不敬,他值得起FMVP),他们媒体没有更好的理由来书写总决赛报道,没有能够把写好的《LBJ2015传奇》出版而怀恨在心才会给Curry零票的。

我不惮以最坏的恶意来书写LBJ的,他也有老去的时候,命中率在科铁的40%徘徊。

我不惮以最坏的恶意来评价LBJ带队的方式的,他总是能把角色垃圾代成中产,当然,也会把全明星联盟前十带成中产(前者参照07前骑士,热火老将们,骑士新锐们,后者参考韦德,波什,欧文,LOVE)。

我相信,骑士明年最坏的结果,是打进总决赛。

祝愿他们来年一路顺风,勇夺第二。

jack.zh 标签:NBA 继续阅读

990 ℉

15.06.15

2015总决赛第五场:这样的英雄故事,就是篮球运动存在的意义

天正十年暨1582年夏天,织田信长焚没于本能寺大火中。羽柴秀吉星夜回师,五日奔走二百公里,先踞天王山,得了先机,一战破明智光秀。

至于一年后贱岳之战破柴田胜家平定家内,再一年后小牧长久手与德川家康扯平,五年后围小田原……嗯那是《太阁立志传》的事……

天王山,得之得先手。之所以叫这鬼名字,是因为山腰有山崎天王社,祭祀牛头天王。

战争打的是士气。数量优势与质量优势,都是士气。

双方甲兵陈列时,靠的是将帅筹谋。三千铁甲竟不能挡,那是武侠小说。

但倘若进了散兵乱战,猛将就有机会,策马刺敌将于万众中,百万军中取上将首级——当然,还是士气。十万敌军,项羽砍一年都砍不完。

篮球也是这样——偶尔会有科比三节62分比小牛全队还多的事儿,偶尔而已。


总决赛第四场,勇士变阵,大胜骑士。变阵的意义,并不在于让伊戈达拉首发,还在于让博古特下场。实际上,全场比赛,博古特只打了3分钟而已,今天索性没打。

去年马刺总决赛打到半途,用迪奥代替斯普利特;2013年总决赛打到第六场,热用迈克-米勒替下哈斯勒姆。联盟的趋势,是大前锋日益摇摆化,各队通常只留一个巨人在禁区。当然,这个巨人,通常断不可少: 勇士得留着博古特,快船得留着小乔丹,火箭得留着德怀特,公牛得留着诺阿,每个联盟强队,无论打得多华丽,禁区里一定要站一个防守擎天柱。防守端不碍事就行。

科尔教练的疯狂处,在于他撤下所有的传统长人,放全摇摆阵容。这种疯狂程度,老尼尔森与丹东尼都未曾有过。骑士的策略,是疯狂攻击内线,结果全场比赛,勇士抵死不退,任凭莫兹科夫得到28分,骑士双内线合计25投16中刷到12个前场篮板,勇士要自己的节奏。

这就好像,勇士撤下了大盾甲士,让轻骑出阵乱箭纷发;骑士这边,勒布朗中军指挥,让重甲士莫兹科夫冲阵取中路——虽然自己这边被射得不善。

第五场开始,也是如此:

勇士继续摆小球阵容,当骑士给双内线喂球时,快速夹击强侧;谁拿球谁推反击。一开场,骑士被闪电战打到2比8落后。勇士就是欺负骑士慢,靠速度强侧夹击,快速轮转,靠速度追击。打到8比2时,骑士第一次变阵。

5-0

JR史密斯替下莫兹科夫。以一大四小对勇士的五人快打旋风。

但还嫌不够,于是,布拉特教练玩出更绝的一招:第一节剩3分40秒,骑士11比16落后,詹姆斯-琼斯替下特里斯坦-汤普森,骑士场上体格最大的,只剩下了勒布朗——骑士以五人小球阵容还击勇士,三分线里基本空无一人,勒布朗可以肆意突破分球。

结果:一波11比6后,双方22平进入第二节。

5-1

5-2

上一场,骑士坚持巨人阵容,想用前场篮板和内线攻势压垮勇士;本场,骑士便是以小对小,散兵阵容对决。

于是:前几场命中率低迷的两队,本场半场打出51比50的不低比分。库里9投6中15分,勒布朗15投8中20分8篮板8助攻。

威斯布鲁克喜欢坎特+伊巴卡的锋线,因为坎特+伊巴卡可以给他腾空间。

当年科比每逢奥多姆替下拜纳姆,就会手感顺滑,呼风唤雨。

勒布朗希望所有的内线搭档都像波什似的,老老实实练定点三分。

都是为了这个,仿佛许褚裸衣战马超的意思:盔甲卸了,大家战个痛快。

经过四场总决赛,经过第二、三场博古特被放空后,科尔决定打小球;经过第四场重甲内线冲阵不利的教训后,布拉特教练决定小球奉陪。终于在今天,骑士与勇士将各自的重型内线搁在一边。战场清空,勒布朗和库里不用在后面发号施令指挥了。

决个胜负吧。

是的,终于勒布朗突破时,不用考虑对方后面还有博古特;终于库里空切时,不用考虑有两个人追着他。一直当牵制,当诱饵,当引子,彼此的锋刃拿来吓唬人,他们也腻了吧?

莫兹科夫9分钟(他在场的6分钟里骑士4次失误);博古特0分钟,艾泽利3分钟。

战个痛快吧。

勒布朗今天后半段的角色,就是1980年总决赛第六场,魔术师代替天钩出场那个位置。汤普森不在,他经常是场上个子最大的,却无所不能。虽然冲锋陷阵,但他时刻注意着本方人的手感。比如JR史密斯第一节手感好,他便着意给JR叫战术。而且,他防利文斯顿那段,是骑士的妙招:利文斯顿没有三分球,所以,勒布朗可以时不时放开利文斯顿,游弋扫荡。

第二节开始,多丽丝-伯克问布拉特教练,“JR史密斯射中跳投对全队有啥影响”,布拉特教练闷了会儿,说:“全队都很高兴……”

但勇士也不遑多让。没有博古特后,格林的内切和分球骁勇无比,妙在他也知道自己袭篮除了抛射别无他招,所以经常径直加速找犯规——他那个速率,明显就不打算上篮了。

JR史密斯远射,巴博萨妖异的单挑。利文斯顿持球伊戈达拉空切,勒布朗突破撤步投篮。

第二节中,勒布朗和库里开始第一回合对决:

勒布朗用火车加速震开伊戈达拉中投,然而库里持球绕掩护逼得汤普森换防后,起手三分球。勒布朗的中投一度让骑士领先6分,但库里下一秒还一个撤步三分。

第二节后半段,骑士稳定了阵容,常让特里斯坦-汤普森在场:勒布朗突破,需要有个人补前场篮板、在禁区里接球袭篮。但伊戈达拉的一条龙游骑兵突破和巴博萨出出入入的反复穿刺,骑士也挡不住——当然,他们谁都挡不住彼此。战局僵持,需要一点火焰——打架、打仗和打球都是如此:你得让对方觉得“这家伙好厉害,今天他好像很无敌”。哪怕是虚的,谁犹豫了就谁输。勇士低迷了一个系列赛的巴恩斯,给了一点火焰:半场结束前补扣得分。

外加罚球。

下半场,依然是小球阵容对决。骑士下半场的首发就没上莫兹科夫,直接带JR史密斯玩儿。骑士再次祭出夹击,针对库里对球夹击,然后靠快速轮转针对格林。反过来进攻端,骑士不断找特里斯坦-汤普森——这是上一场战术的微调:

防守端,用准小个阵容(不上莫兹科夫)跟勇士的速度。 进攻端,用汤普森的前场篮板和高空作业袭勇士的篮——他是全场最后一个纯内线重步兵了。

但骑士这里,赌输了。

库里左翼三分球得手,然后被夹击后助攻伊戈达拉,连续两个三分球,勇士领先。这时,骑士没变阵。

第四节,勒布朗全力出阵。抛射、突分、双方75平。勇士是靠着巴恩斯的一记怒扣——本届总决赛迄今最佳入球——稳住分差的。

5-3

于是又到大将出阵时间。库里中投,勇士77比75;勒布朗上篮,77平。

之后,勒布朗不可思议的长距离三分球得手,骑士80比79领先。那是勒布朗本场最闪耀的瞬间。

5-4

甚至在库里和克雷-汤普森连续还击后,他还是可以后仰跳投得分,锁住。

5-5

5-6

但是:

在最后时刻,正常的套路,是双方都用无限换防,逼对方单打,但骑士还是针对库里用了夹击——他们太忌惮库里了,不想让他单挑。但他们忘了勇士最危险的家伙: 库里——格林——伊戈达拉连线得手,勇士89比84领先5分。这样的空位,你放不放都是错,只好赌,骑士赌输了。伊戈达拉再施展一条龙,勇士91比84领先。

5-7

5-8

到此为止,大局已定。之后库里夭矫如龙的两个三分球,只是MVP的余兴节目,给勇士敲钉子而已。

不能说骑士赌输了。让库里单挑?他例不虚发,把德拉维多瓦和特里斯坦给封喉了。夹击他?库里——格林——某射手(伊戈达拉或其他人)的连线,从第三场第四节开始,已经所向无敌了。

双方都是脱下了铠甲对轰,以攻对攻。布拉特教练一定也明白,与勇士拼速度斗往返实是险招,但骑士只有这么多套路可用。勒布朗找到了JR史密斯,找到了香波特,找到了汤普森,亲身突阵之余指挥若定,但勇士无所谓了:他们不在意被骑士打成什么样,他们只是逼得骑士跟他们拼速度,然后就拿下了天王山。

伊戈达拉打得,就像是个半场进攻弱化版的勒布朗——14分8篮板7助攻,最后时刻一个三分球一个加罚锁定比赛,虽然也被不断押上法场去罚球,居然出了个“砍伊战术”,也真笑话奇谈。但今天他第二节一条龙上篮后,范甘迪说了这么句话: > “有了伊戈达拉,勇士好像多了一个组织后卫似的。” > 这话他去年说过。范甘迪词真少。 > “有了迪奥,马刺好像多了一个组织后卫似的。”

反过来:骑士今天17次助攻,11次来自勒布朗;投中32球,15球来自勒布朗——意味着什么,不言自明。

虽然败北,这依然可能是勒布朗打过最好的一场总决赛,甚至可能是他打过最好的比赛:2012年对步行者第四战、对凯尔特人第六战,也不过如此了。40分14篮板11助攻,淋漓尽致,命中率44%也比前三场高。但如上所述,勇士不介意他发威;博古特始终不上,就是勇士对勒布朗的回答。你纵横疆埸去吧,我们只要全队联动——勒布朗今天没输给任何人,所当者破所击者服,哪怕跟库里玩接管比赛也没落下风。然而,勇士不在意。他们不在乎骑士如何打出勒布朗反插内切,如何打出弱侧特里斯坦+JR的挡切。骑士将勇士逼得下博古特,是骑士的胜利;勇士将骑士逼得放弃莫兹科夫,是勇士的胜利。

“他持球许多。我们坚定于计划。他投中球也不要丧气,反正他会投中。48分钟,我们希望我们可以把他累倒。” 库里赛后如是说。

天王神威,也只能到此。勒布朗可以独自得到40分外加助攻11次,但他到底是一个人,无法一个人打赢一场战争。上一场闪现在他面前的是伊戈达拉和利文斯顿,今天是伊戈达拉、格林和巴博萨,以及跗骨随身、不断用三分球回答他的库里。

战争打的是士气。数量优势与质量优势,都是士气。

双方甲兵陈列时,靠的是将帅筹谋。三千铁甲竟不能挡,那是武侠小说。

但倘若进了散兵乱战,猛将就有机会,策马刺敌将于万众中,百万军中取上将首级——当然,还是士气。十万敌军,项羽砍一年都砍不完。

篮球也是这样——偶尔会有科比三节62分比小牛全队还多的事儿,偶尔而已。今天第四节,勒布朗那记超远距离三分得手,骑士80比79领先时,天下真的已在他掌中。那是他独自奋战、鼓励队友、寻找战机、策划、筹谋、冲阵斩将,得出的一缕光芒。

然后就被库里的百步穿杨、伊戈达拉们的乱箭给射散了。

当然,幸运的是我们。MVP和前任MVP在总决赛天王山,合计得到77分?我们得追溯到乔丹大战邮差、乔丹大战巴克利、乔丹大战魔术师、魔术师大战伯德、J博士大战天钩那些岁月,才能看到类似的玩意了。双方你来我往勾心斗角,战到最后,逼得超级巨星终于出来,使劲气力筹谋,抖出压箱底招式,奋死血战,将个人的能力压榨到极限,将时代作为赌注放在命运天平上探测未来——《星球大战》、《海贼王》、《SLAM DUNK》的剧情也不过如此。

这样的比赛,这样的英雄故事,就是篮球运动存在的意义。


来点调剂

jack.zh 标签:NBA 继续阅读

1010 ℉

15.06.15

2015总决赛第四场:你们城里人真会玩

JR史密斯今天到球场时,踩着一双疑似是遥控的灯光旱冰鞋。毕竟是打纽约过来的浪子,城里人真会玩。 勇士开场阵容:191公分的库里,201公分的汤普森,198公分的伊戈达拉,203公分的巴恩斯,201公分的格林。考虑到格林入行时填的第一位置是小前锋,库里大学里还是得分后卫,这简直就是一个摇摆人首发。太好了,城里人真会玩。


这是勇士实际上的生死战,众所周知。科尔摆这阵势,两个故事。

  • 他是凤凰城太阳的前老总。这个阵势,十年前的凤凰城球迷定然看得泪流满面。
  • 本季勇士常规赛有过八套首发,合计二十套不同的五人阵容排布,但这个五人阵,从来没摆过。

老尼尔森、迈克-丹东尼、大范甘迪、莱利等诸位疯狂试验家,到此也不免抱拳拱手:行,你敢赌,行。

所以开场,勇士被骑士揍个7比0也很自然了。勒布朗一定想:“当我是白痴啊!”专门传球找着莫兹科夫,揍。

但一个暂停后,勇士缓过来了。

库里单挑,一个三分球。伊戈达拉一个中投。库里+格林挡拆后,格林上篮。伊戈达拉反击扣篮。库里+格林挡拆后,格林助攻巴恩斯三分球。

勇士的前12分,是这样得的。这也就是他们全场套路的缩影了。

上一场,勇士的套路:

库里每次都能吸引夹击?好,就给其他放空的队友吧。

这其实,可以理解为库里在呐喊:“我吸引他们注意力,你们负责灭门!——勒布朗被夹击队友能投进球,你们也拜托拜托吧!”

而他们的问题:

可以说,伊戈达拉和利文斯顿是全队替补最聪明的二位,但要命的是:他们二位,看得见,想得到,但手上不够稳。利文斯顿是不会三分球的,伊戈达拉是个三分半吊子。

实际上,伊戈达拉自己还做了许多事:他持球时,勒布朗在防守,等于将骑士的协防抽干了筋血,于是伊戈达拉自己投三分、找空切队友。但他三分不够稳。

勇士真正的问题是谁呢?格林和巴恩斯。博古特被放空却不得分,那是能力所限。但格林得到无数空位却无法惩罚对手,巴恩斯今天8投0中,从中投到定点三分一片稀烂。

今天,勇士做到了。

伊戈达拉首发,骑士无人可以防他,只好用莫兹科夫对位,类似于西部半决赛博古特对位托尼-阿伦——结果,伊戈达拉全场都在投莫兹科夫,“你有本事出来呀!”

4-0

以及单枪匹马推这样的快攻:

4-1

格林的进攻侵略性十足,实际上,勇士上半场基本放弃了“库里找掩护后迂回”,而是干脆的“库里+格林挡拆,格林立刻分球或自己投”。这就变成去年总决赛的套路:马刺趁热无限夹击,找空位投篮。

4-2

格林投三分、格林助攻巴恩斯、格林自己造罚球。伊戈达拉欺负莫兹科夫防不出来。勇士的进攻开了。

4-3

4-4

第一节剩4分钟时的暂停里,科尔的叫嚷是勇士本场的核心: “骑士会累的!”

的确如此。

骑士的进攻,也只有那么多戏法可以变。勒布朗单挑,吸引夹击,分球,队友三分球是主轴。今天稍微不一样:勇士放小球阵容,按规矩是得用前场篮板屠宰他们的,如此好逼勇士变阵。莫兹科夫、特里斯坦-汤普森蹲禁区里,欢天喜地:二位今天各6个前场篮板,合计26投15中,莫兹科夫还带12罚10中,真仿佛鲨鱼附体。问题在于,勇士变了一变:

伊戈达拉主防勒布朗,是其一变——前三场,就数他防勒布朗效果好——再一变,是勇士的夹击时机和防守轮转。小球阵容的好处就在这里:内线虚,需要补位,但大家脚下快,轮转也到位。勇士今天对勒布朗的夹击很聪明,要么是提前夹击,逼他将球送走;要么是等他已经无法传球时夹击。像这个就是及时夹击后,利文斯顿迅速返位,让JR史密斯无法轻松空位投篮。

4-5

4-6

所以,今天勒布朗招牌的“突破吸引四双眼睛长传弱侧三分”,没怎么开。第三节初趁着克雷-汤普森走神甩了一下,但就这样了。

当勒布朗无法突破分球,而只是不断给俩内线塞球时,他的破坏力会打些折。总决赛前三场,他连得分带助攻,骑士291分里他包了200分(借个冰球概念);但本场,夹击到位、内线有更好的投篮选择,他的效率下去了。

以及:他毕竟是人类,也会累的。第二节勒布朗脑袋蹭伤后,范甘迪在解说间阴飕飕地说:

“勒布朗本来今天开场就动力不足(low energy)。”

虽然是新阵出场,但科尔的时间凑得很好。对骑士的常规阵容,勇士上小球;一等莫兹科夫下去,立刻上大卫-李,去禁区里倒腾。

但第三节,勇士还是乱了。骑士的防守出招:

  • 全场领防,刹勇士的速度。
  • 阻绝球,不让勇士舒畅传递,逼勇士单打。

第三节前半段,勇士是靠伊戈达拉的三分球、巴恩斯的单打、库里的个人才华撑下来的。之后骑士赌博式破坏传球路线加上勒布朗的突击,追到了62比65。

这是本场真正的生死之际,但是:

巴恩斯和伊戈达拉控制住了局面,确切说,是伊戈达拉个人的活跃。 防守端,伊戈达拉封锁了勒布朗;进攻端,三分、个人强推罚球、中投:莫兹科夫你有本事出来呀!

4-7

勇士的最后一波是这样的:

第三节末和第四节初,库里连续两次借对球掩护逼出换防后三分球,再一个“库里——格林——利文斯顿——伊戈达拉”式的连线,勇士领先到88比74。而那时,骑士只剩下莫兹科夫单挑了。比赛结束。

4-8

上场比赛后说的:

所以勇士需要什么呢?根据下半场的经验,他们得多给库里设立持球掩护,多用用大卫-李(或者斯贝茨),给博古特设计点可以得分的套路,当然,最好的法子还是敲敲巴恩斯和格林的警钟——“该你们俩独的时候,做点狠事儿啊!出来混要讲信用,给你们机会就要杀他们全家啊!”

勇士确实利用了库里的牵制。李也上了。

巴恩斯和格林确实争气了——当然,他俩是要进了快节奏才能热血起来的。今天的节奏,很合他们的脾胃,尤其是格林:果敢了。

4-10

但勇士最大的功臣,是两个人。

本场MVP,或者说,勇士总决赛四场以来的MVP,是伊戈达拉。

单防勒布朗;射中关键三分;个人连续快攻;单挑莫兹科夫——在这一切背后,是他的首发带起了勇士的节奏。

过去三场,骑士的防守胜利,在于他们放任博古特、格林和巴恩斯,死守库里,“你们是好汉的,剁我一刀!”

而今天伊戈达拉出阵,勇士等于放弃了后场篮板——全场骑士16个前场篮板——但拼命跟骑士搏跳投、搏速度,“我先拉自己一刀,您随意!”

以及,肖恩-利文斯顿。

今天利文斯顿抓了6个防守篮板,大部分是“格林和汤普森抢成一团,利文斯顿过去拣了,就地推快攻”。他给伊戈达拉送出击地传球,助攻了快攻扣篮;吸引夹击后找到大卫-李,诸如此类。当然,他还担当了接应点,让格林的传球有终结点。伊戈达拉和利文斯顿交替防守勒布朗推快攻,在弱侧轮转得严丝合缝,没让骑士的射手们快活,捎带回收了无数后场篮板。最重要的是,他俩加上格林的果决,让勇士找回了先前纵横天下的转移球。

当然,话题还是得回到勒布朗与库里。

勒布朗打得没什么可说的。依然是准三双数据,命中率也并不比前三场差太多,依然指挥若定,在进攻端的选择正确。唯一的问题是罚球。勇士成功的地方,在于让勒布朗犹豫了。伊戈达拉的防守,弱侧时不时的夹击,放小球阵容后的站位,勒布朗没法再简洁地“突击吸引夹击分球或者自己来”了。以及,他的体力,确实现出问题来了——没法子,他担负着NBA史上空前沉重的进攻任务呢。

而库里,则是不犹豫。本场他的思路很清晰:找掩护,吸引夹击,立刻给格林传空位;要么找掩护,逼出换防来直接投。减少了运球找队友,出球更快,进攻也更迅疾。第三和第四节那两个神来之笔的三分球,对气势大有帮助。此外,小个阵容也便于他发动“被夹击后给格林,连续传球找空位”。比如,下面这个库里——格林——利文斯顿——伊戈达拉四重奏。

说来说去,无非那句:超级巨星们是不能犹豫的。在这个绝少一对一防守、大家拼换防和轮转速率的时代,谁快一步,谁敢赌,谁就先一招。布拉特教练之前赌了勇士诸将投不进三分球,科尔今天就赌了小球阵容。

“你有本事砍我一刀!” “我自己拉一刀了,你敢跟着来吗?!”

作为两个第一次打总决赛的教练,他俩到现在为止的对局,都算极大胆了。

你们城里人真会玩。

哦对了。

2011年总决赛前,NBA的总决赛,任何前两场打到1比1的,谁赢第三场谁冠军——依照这定律,今年骑士冠军。

但2011年总决赛和2013年总决赛,迈阿密热赢了2011年第三场却丢了总冠军,2013年输了第三场却赢了总冠军,所以……似乎这定律近年又不准了。

而今年季后赛,所有打到1比1,之后赢第三场的,结果都糟糕了。比如公牛对骑士,比如马刺队快船,比如快船对火箭,比如勇士对灰熊——按照这定律,今年勇士冠军。

说来说去,一切定律都是待推翻的,只有科比永远硬气。第二场比赛结束后,鉴于勒布朗前三场107发投篮,第三场34次投篮,有了这么条推特:

今天,科比可以很自傲地说:“勒布朗今天没赢,是他没有投足够多的‘科比式投篮’啊!”

最后,利文斯顿第一节差点完成本届总决赛最漂亮一球——底线起跳,滞空,被拽住犯规,依然将球砸到了篮筐边缘。如果再顺一点,就是一个扣篮了。每次利文斯顿做出类似动作,我总想有个谁录下来,然后给保罗-乔治去看看。

*“你看,把腿伤养好了,照样可以在总决赛飞的!”


来点调剂

jack.zh 标签:NBA 继续阅读

1 ℃

1145 ℉

15.06.15

2015总决赛第三场:勒布朗和库里很伟大,但本场属于德拉维多瓦

武力威慑,以牙还牙,《古惑仔》里阿坤说:出来混要讲信用,说杀他全家就杀他全家。

邓布舍欠了贝多芬的钱,贝多芬去讨债。邓布舍问:“非如此不可吗?”贝多芬答:“非如此不可!”——还把这句音韵写成了《命运》开头的重音。

篮球里,也是如此:

有债必偿。你放空我,我就得让你付出代价。


说杀他全家就杀他全家=西部半决赛,灰熊用兰多夫守格林,被格林+库里挡拆打得摸不着门。

或者更极端的例子:九年前,猛龙坚持全场单防科比,不夹击,然后科比81分。

反例呢?四年前的总决赛,小牛用基德甚至巴里亚,去挡勒布朗的背身,收缩阵线让勒布朗和韦德跳投: “您请您请,这个我豁出去不要了……哟?您客气啊?那我得罪了。”

第二场库里被锁死,是因为他每次无球走位,都遭遇德拉维多瓦;每次挡出机会,骑士换防;每次他持球,骑士送两个人过来。

骑士当然付了代价:博古特放空了,格林放空了,巴恩斯半放空了——然而博古特投不进,格林投不进,巴恩斯单打德拉维多瓦居然也不成。这就算是降住了。

反过来,勇士对付勒布朗,是每次他突破都要两三人夹击。结果?勒布朗后22投4中,最后7投0中,但他依靠不断突破分球,给队友塞三分球助攻;而且,特里斯坦-汤普森趁勇士全体围剿勒布朗,统治了前场篮板。

骑士的进攻端策略,并没什么好说。勒布朗持球单打,队友散开等着投空位;勒布朗背身单挑,队友散开等着投空位;德拉维多瓦在弱侧趁乱挡拆抛射;JR史密斯风骚地单挑;特里斯坦-汤普森冲前场篮板;莫兹科夫弱侧内切袭篮。

前两场,骑士全队命中率37%,三分率31%,勒布朗两场投篮72次,场均41.5分,送出全队33次助攻中的17个。今天用了34次投篮和12次罚球得到40分8助攻(全队助攻15次)。就是这样了:

反正,你一对一防勒布朗,他能得分;你夹击,他分球;你不中,他追身反击。效率高不到哪儿去,但也不低。跟哈登西部决赛五场比赛投90个篮得到142分比,勒布朗三场比赛投106个篮得到123分,那肯定没法比;但勒布朗担负的要沉重得多。反正无论勇士怎么防,他总有法子就是。进攻端,骑士不会神到哪里去,但有勒布朗这个变形金刚万能输出机,随时变身突破/背身/分球机器/快攻启动,怎么着都是这样。勒布朗近三场的真正秘诀,是他的坚决。投不进还是投,突破累还是突。今天上半场虽然17投6中,但有13次投篮是在禁区里——就是撞破南墙不回头的气势。

近两场比赛,骑士的秘密,在防守。

第三场,骑士的防守策略,依然如此:

  • 库里无球走位,德拉维多瓦死缠到底。不行就换防。
  • 克雷-汤普森,限制他投三分,允许他中投。
  • 格林的三分,放空;巴恩斯的三分,可以放空;博古特,可以不把他当人看。
  • 伊戈达拉,可以放空;利文斯顿,三分线外不理他。

如此这般,绞杀库里。

勇士的应对手段?

库里每次都能吸引夹击?好,就给其他放空的队友吧。

这其实,可以理解为库里在呐喊:“我吸引他们注意力,你们负责灭门!——勒布朗被夹击队友能投进球,你们也拜托拜托吧!”

问题来了:

从一开场,库里便随时被夹击,但格林被放空到这程度,却还是投不进。

3-1

库里想推反击追身?骑士全队都会放弃格林或博古特,来刹车,甚至可以一过半场,就给他来个夹击。

3-2

3-3

格林第一次有效捕捉空位,是第一节过半,面筐突破打三分那一下。但自那之后,他的进攻哑火了——前两场,格林命中率30%,今天10投2中。

懂得利用库里空位的人是有的。比如:伊戈达拉第一节那个扣篮,就是利文斯顿看着库里空切、吸引了勒布朗+德拉维多瓦的注意,于是直塞过去。

3-4

可以说,伊戈达拉和利文斯顿是全队替补最聪明的二位,但要命的是:他们二位,看得见,想得到,但手上不够稳。利文斯顿是不会三分球的,伊戈达拉是个三分半吊子。

实际上,伊戈达拉自己还做了许多事:他持球时,勒布朗在防守,等于将骑士的协防抽干了筋血,于是伊戈达拉自己投三分、找空切队友。但他三分不够稳。

勇士真正的问题是谁呢?格林和巴恩斯。博古特被放空却不得分,那是能力所限。但格林得到无数空位却无法惩罚对手,巴恩斯今天8投0中,从中投到定点三分一片稀烂。

类似这样的机会还8投0中……而且还犹豫不决:

3-5

3-6

加上利文斯顿进不了三分球、伊戈达拉三分球不稳,忽然间,历史最顶级三分球勇士,变成一支投不进三分的队伍了。

骑士这个思路,和当年禅师是暗通的。禅师很爱玩这个:坦胸露怀恶狠狠夹击,“你有本事投死我,投不死我我就拍死你”。斗狠,骑士胜了一筹。

下半场开始,库里自己被夹击,于是连找两个博古特的高位挡拆,让博古特得了今天全部的4分,但这也只是饮鸩止渴。

3-7

反过来德拉维多瓦找着库里挡拆、绕过掩护揍汤普森,很得意。

第三节中段,艾泽利上场是勇士的大痛点:骑士肆意夹击勇士,艾泽利一拿球就被钳死;勒布朗再接管比赛——连续的右腰启动到禁区背身后仰投篮——勇士就被甩开。 3-8

到最后,科尔终于掏出了一个法宝。

第三节下半段,库里开始接管比赛。强行三分、绕掩护中投、吸引夹击助攻巴博萨。

以及:找全队年薪第一人出场:1500万的大卫-李先生。

大卫-李的防守不及博古特,运动不及艾泽利,全能不及格林,但他有两个好处:接到球能传球——比如,库里被夹击,他接球后,还能找到伊戈达拉。

3-9

3-10

或者,自己接到库里被夹击后的传球,也能完成上篮——和罚球。

勇士找到这招后,一度落后20分的局面被扳回。骑士比赛后半段,再次显得办法不多。勒布朗右腿疑似抽筋,已经跑不动,只能靠反击和罚球得分。当库里神话般的三分追到80比81时,骑士眼看要垮了。

然后,真英雄再次出来了。

骑士开场,德拉维多瓦单挑克雷-汤普森两个抛射得分,让骑士10比5领先。

第一节最后时刻,德拉维多瓦在死角把球往天上一抛,“特里斯坦,我知道你接得住啊!”

下一个回合,他和迈克-米勒为了个球权滚得一地鸡毛。

下半场一开始,德拉维多瓦找着库里打挡拆投中三分,再绕掩护一个抛射;之后接勒布朗传球三分得手让骑士领先10分。

还有一个神奇的空中补篮得手,骑士63比48领先。

说回比赛最后时刻。库里三分得手,勇士80比81落后,然后:德拉维多瓦倒地,球出手打板得分,加罚。骑士重新领先4分。那是本场比赛最关键的瞬间。

至于之后不到两分钟的比赛,他一共为了球权摔地四次,反而不在话下了。

勒布朗尽力了,不多提。本场他真正的亮点,是第三节的封盖和一波引领反击。至于其他,于他而言只是中规中矩——哦对了,总决赛前三场得到123分,是迄今为止的NBA记录。之前的记录是1967年的里克-巴里。当然,勒布朗三场出手106次投中43个是不太好看,不过巴里在1967年第一场43次投篮和第三场48次投篮也真够瞧的……那会儿巴里还没开发出组织前锋属性,1967年总决赛第三场他48投得到55分,之后非官方统计说他全场一共传球11次……好吧,这是闲篇。

3-11

库里也尽力了。全场比赛他27分6助攻,以及二位数的“库里吸引夹击——分球给队友——队友再分球给其他人”的二次策动。最后一节的追杀无愧天下第一神射手。

所以这其实可以概括为:勒布朗被夹击时,他的队友把球投进了;库里被夹击时,他的队友没把球投进——确切说,就是格林和巴恩斯。科尔教练在最后找到了大卫-李,但稍微晚了点。当然,最后那几下,是德拉维多瓦的个人传奇。

值得一提的细节倒是:

当库里被锁死后,勇士居然没有持球攻击点。本来巴恩斯的持球单打、克雷-汤普森的背身、格林的面筐、利文斯顿的单挑和伊戈达拉的一条龙都是有的,但克雷被针对性对待,利文斯顿遭遇收缩后,巴恩斯和格林的单挑莫名其妙消失了。反过来,今天JR史密斯摇曳生姿的单挑和德拉维多瓦的绕掩护抛射,恰好是勇士需要的——勇士居然输在了定点三分球和单挑上,说来也讽刺。

所以勇士需要什么呢?根据下半场的经验,他们得多给库里设立持球掩护,多用用大卫-李(或者斯贝茨),给博古特设计点可以得分的套路,当然,最好的法子还是敲敲巴恩斯和格林的警钟——“该你们俩独的时候,做点狠事儿啊!出来混要讲信用,给你们机会就要杀他们全家啊!”

但在此之前,到现在为止,布拉特教练是赌赢了的。

凯撒在《高卢战记》里说过个经验:当双方都紧张时,能克服紧张的一方往往能够先制,所以临场犹疑实为自己真正的大敌。搁现代战场上就是“你越怕子弹越打你,你越不怕子弹越打不中你”的思路。骑士的诸位射手们,也就是赢在了“不怕”上面。当然,这信心不是凭空而来的。有一个超级王牌,信心的确不一样。

最后一个问题:

以德拉维多瓦迄今为止的攻防两端表现,以及他与香波特类似的、为球队注入的韧劲和狠劲……假设欧文这时没受伤,他和欧文该谁首发呢?这个问题如果总决赛前提出来简直荒诞,但现在想一想的话……

所以,这场比赛前三节属于勒布朗,第四节属于库里,但归根结底,属于德拉维多瓦,属于他那些抛射、追防和无数次橄榄球达阵般倒地扑球。这出来混就不回头的热血与不屈不挠。

勇士恰好,就输在这里。


来点调剂

jack.zh 标签:NBA 继续阅读

1 2 3 4 5 6 7 8 9 10
Fork me on GitHub