Shared posts

13 Mar 00:47

每个程序员都应当知道的编译器优化知识

by alexxxx

高级编程语言提供的函数、条件语句和循环这样的抽象编程构造极大地提高了编程效率。然而,这也潜在地使性能显著下降成为了用高级编程语言写程序的一大劣势。在理想条件下,在不以性能为妥协的情况下,你应该写出易读并且易维护的代码。因此,编译器尝试自动优化代码以提高其性能,当今的编译器都深谙其道。编译器可以转化循环、条件语句和递归函数、消除整块代码和利用目标指令集的优势让代码变得高效而简洁。所以对程序员来说,写出可读性高的代码要比因为手工优化而使代码变得神秘且难以维护更加可贵。事实上,手工优化的代码反而可能会让编译器难以进行额外和更加有效的优化。

比起手工优化代码,你更应该考虑关于设计的各个方面,比如使用更快的算法,引入线程级并行机制和利用框架特性(比如move构造函数)。

这篇文章是关于Visual C++ 编译器优化的。为了便于应用,我将会讨论编译器采取的最重要的优化技巧和决策。我的目的不是告诉你如何手工优化代码,而是向你展示为什么你可以信赖编译器来优化你写出的代码。这篇文件绝不是对Visual C++ 编译器优化工作的全面考察。但是将会给你展示那些你真正想要了解的优化工作和怎样与你的编译器沟通来应用它们。

有一些重要的优化是超出所有现有编译器能力的——比如,用高效的算法代替低效的,或者改变数据结构的排列以优化其在内存中的布局。但是这些优化话题超出了本文的范围。

定义编译器优化

优化工作涉及到的一个方面,是把一行代码转化成同等效果的另一行代码,在这个过程中提升它的一项或多项性能。最重要的两项性能(指标)是代码的执行速度和长度。其他一些特性包括代码执行开销,代码编译所需时间,如果代码需要通过即时编译机制(Just-in-Time (JIT))进行编译,那么JIT所需的编译时间也是指标之一。

编译器经常会依据它们所使用的技术优化代码。虽然并不完美,但是比起花时间手工苦苦推敲一个程序,利用编译器提供的特有功能和让编译器来优化代码要高效得多。

这里有4种方法让你的编译器更加高效地优化代码:

  1. 书写可读、高效的代码。不要把Visual C++ 面向对象的特性当作性能的敌人。最新版本的C++可以让这些开销保持到最低甚至消除这些开销。

2.使用编译器声明。例如让编译器使用比默认情况更快的函数调用约定。

3.使用编译器内置函数(compiler-intrinsic functions)。内在函数是其实现由编译器自动提供的特殊函数。编译器对其很熟悉并且会用极其高效的指令序列来代替函数调用,以充分利用目标指令集的优势。当前Microsoft .NET Framework不支持编译器内置函数,因此其下的语言都不支持。但是Visual C++ 对这一特性有外在支持。注意,虽然使用内置函数能够提升代码性能,但是会降低可读性和可移植性。

4. 使用性能分析引导优化(profile-guided optimization)。使用这一技术,可以让编译器搜集更多关于代码的运行时行为,并且以此来作为优化依据。

本文的目的是通过证明编译器可以在低效但是可读性强的代码上应用优化(应用第一条方法),从而向你展示为什么你可以信任编译器。当然我也会提供一些对性能分析引导优化(profile-guided optimization)的简短说明,和提到一些可以微调代码的编译器声明。

编译器有许多优化技巧,从像常量折叠这样简单的变换,直到像指令重排(instruction scheduling)这样极其复杂的变换。然而在这篇文章中我只有限地讨论了一些最重要的优化——那些可以显著地提升性能(两位数的百分数来衡量)和减少代码长度的优化:内联函数(function inlining)、COMDAT优化(COMDAT optimizations)和循环优化。我将会在下一部分讨论前两个话题,然后展示你如何控制Visual C++实现优化。最后会有.NET Framework优化的简略说明。通篇我都将会采用Visual Studio 2013来构建代码。

链接时代码生成

链接时代码生成(LTCG)是一项应用在C/C++代码上的程序全局优化(WPO)技术。C/C++编译器独立地编译每个源文件然后产生出相应的目标文件。这意味着编译器只能在单个源文件上应用优化技术,而无法照顾到整个程序。但是,一些重要的优化却只能浏览全部程序后才能产生。所以你只能在链接时(link time)应用这些优化,而非编译时(compile time),因为链接器可以完整地看到程序。

当LTGC被打开时(通过指定编译器开关/GL),编译器驱动程序(cl.exe)将只调用编译器前端(c1.dll or c1xx.dll),并把后端调用(c2.dll)推迟到链接时间。产出的目标文件包含通用中间语言(Common Intermediate Language——CIL)代码,而不是依赖机器的汇编代码。然后,当链接器(link.exe)被调用,它就能看到包含C中间语言的目标文件,并调用编译器后端,依次进行程序全局优化,生成二进制目标文件,再返回链接器把所有目标文件链接在一起,最后生成可执行文件。

编译器前端实际上进行了一些优化,比如无论优化启用还是禁用,都会进行常量折叠。但是所有重要的优化工作都是在编译器后端进行的,并且可以使用编译器开关控制。

链接时代码生成(LTCG)能让后端积极地执行许多优化(通过指定/GL与/O1或/O2,以及/Gw编译器开关,和/OPT:REF 与 /OPT:ICF链接器开关)。在本文中,讨论仅限于内联函数(function inlining)和COMDAT优化(COMDAT optimizations)。关于完整的链接时代码生成优化,请参考相关文档。注意链接器可以在本地目标文件,本地/托管混合目标文件,纯托管目标文件,安全托管目标文件和安全.net模块上执行链接时代码生成。

我编写了一个包含两个源文件(source1.c 和 source2.c)和一个头文件(source2.h)的程序。source1.c 和 source2.c分别在Figure 1 and Figure 2中。由于头文件中非常简单地包含了source2.c中的函数原型, 所以并没有列出。

Figure 1 The source1.c File

#include <stdio.h> // scanf_s and printf.
#include "Source2.h"
int square(int x) { return x*x; }
main() {
  int n = 5, m;
  scanf_s("%d", &m);
  printf("The square of %d is %d.", n, square(n));
  printf("The square of %d is %d.", m, square(m));
  printf("The cube of %d is %d.", n, cube(n));
  printf("The sum of %d is %d.", n, sum(n));
  printf("The sum of cubes of %d is %d.", n, sumOfCubes(n));
  printf("The %dth prime number is %d.", n, getPrime(n));
}

Figure 2 The source2.c File

#include <math.h> // sqrt.
#include <stdbool.h> // bool, true and false.
#include "Source2.h"
int cube(int x) { return x*x*x; }
int sum(int x) {
  int result = 0;
  for (int i = 1; i <= x; ++i) result += i;
  return result;
}
int sumOfCubes(int x) {
  int result = 0;
  for (int i = 1; i <= x; ++i) result += cube(i);
  return result;
}
static
bool isPrime(int x) {
  for (int i = 2; i <= (int)sqrt(x); ++i) {
    if (x % i == 0) return false;
  }
  return true;
}
int getPrime(int x) {
  int count = 0;
  int candidate = 2;
  while (count != x) {
    if (isPrime(candidate))
      ++count;
  }
  return candidate;
}

source1.c文件包含两个函数,有一个参数并返回这个参数的平方的square函数,以及程序的main函数。main函数调用source2.c中除了isPrime之外的所有函数。source2.c有5个函数。cube返回一个数的三次方;sum函数返回从1到给定数的和;sumOfcubes返回1到给定数的三次方的和;isPrime用于判断一个数是否是质数;getPrime函数返回第x个质数。我省略掉了容错处理因为那并非本文的重点。

这些代码简单但是很有用。其中一些函数只进行简单的运算,一些需要简单的循环。getPrime是当中最复杂的函数,包含一个while循环且在循环内部调用了也包含一个循环的isPrime函数。我将会利用这些函数证实被称作内联函数的优化,和一些其他的优化,其中内联函数这是编译器最重要的优化之一。

我会在三种不同的配置下生成代码并且检验结果来验证代码是如何被编译器转化的。如果你也照做的话,你需要汇编生成文件(由编译器开关/FA[s]生成)来检验生成的汇编代码以及映像文件(由链接器开关/MAP生成)来检验初始化数据优化是否被执行(如果你指定了/verbose:icf 和 /verbose:ref开关,链接器也可以汇报这一项)。因此你需要确保在接下来的配置中指定了上述开关。我也会使用C编译器(/TC)以让生成的代码容易检验。但是这篇文章中所有我讨论的东西对于C++一样适用。

Debug配置

之所以使用Debug配置,是因为在你打开了编译器/Od开关而没有打开/GL开关时,所有的后端优化都是禁用的。当在这项配置下构建代码时,生成的目标文件将包含和源代码完全对应的二进制代码。你可以通过生成的汇编输出文件和映像文件来确认这一点。这项配置相当于Visual Studio中的调试配置。

编译时代码生成Release配置

这项配置和优化被启用的配置(通过指定/O1,/O2或/Ox编译器开关)非常相似,但是不指定/GL编译器开关。在这项配置下,生成目标文件将包含优化过的二进制代码。但是没有整个程序级别的优化。

通过查看source1.c生成的汇编代码文件,你会看到执行了两项优化。首先,通过在编译时的评估计算把square函数的第一次调用完全删去了。这是如何发生的呢?编译器发现square函数很小,它应该被作为内联函数。将它作为内联函数之后,编译器发现本地变量n的值是已知的并且在给它赋值和调用函数之间没有发生改变。因此,编译器总结出执行乘法和用25替代结果是安全的。第二项优化,对于square的第二次调用square(m),也被当作内联函数。但是,因为m的值在编译时是未知的,所以编译器不能对计算估值,所以事实上代码被保留了。

现在我会检查source2.c的汇编代码文件,这将会更有趣。在函数sumOfCubes内对cube的调用被作为内联函数。这会让编译器启用了对循环来说意义重大的一些优化(如你在“循环优化”部分将看到的)。此外,SSE2指令集被用于在isPrime函数中,当调用了sqrt函数时把int转化为double而在sqrt返回值时又把double转化为int。并且sqrt只在循环开始前调用了一次。注意如果/arch编译器开关没有被打开,x86编译器将会默认使用SSE2。大多数x86处理器以及所有x86-64处理器,都支持SSE2。

链接时代码生成Release配置

链接时代码生成(LTCG) Relase配置与Visual Studio中的Release配置相同。在这项配置中,优化被启用并且/GL编译器开关被打开。这个开关隐含的指定了使用/O1或者/O2。这告诉编译器生成通用中间语言(Common Intermediate Language——CIL)目标文件而不是汇编目标文件。这样,链接器像之前所说那样调用编译器的后端来执行整个程序的优化。现在我将会讨论一些程序全局优化来展示链接时代码生成带来的巨大好处。这项配置所生成的汇编代码列表可以在网络上得到。

只要允许函数被内联(/Ob控制,不论何时,只要需要优化就可以打开),不论/Gy开关(稍后讨论)是否打开,/GL开关都允许把其他翻译单元中定义的函数作为内联函数。/LTCG链接器开关是可选的并且只为链接器提供指导。

通过查看source1.c的汇编代码,你会看到除了scanf_s之外的所有函数都被作为了内联函数。因此,编译器被允许执行函数cube,sum和sunOfCubes的计算。只有isPrime函数没有被作为内联函数。但是,如果它被我们手动在getPrime中写为内联函数,编译器仍然会在main函数中把getPrime作为内联函数。

正如你所见,将函数内联很重要不仅仅是因为它总是优化函数调用,而且它可以允许编译器进行许多其他优化。将函数内联通常会以代码量增加为代价来提升性能。过度地使用这一优化会导致我们熟知的代码膨胀现象。在每一次调用函数的地方,编译器都会分析这样做的利弊来决定是否将一个函数作为内联函数。

由于内联的重要性,Visual C++编译器提供了比对内联的标准规定控制更多的支持。你可以通过使用auto_inline编译控制编译器不将一段范围内的函数内联。你可以通过标记为__declspec(noinline)控制编译器不把特定的函数或方法内联。你可以用关键字inline标记一个函数来给编译器提示将这个函数作为内联函数(虽然编译器可能选择忽略这一标记如果这次内联带来的是净损失)。inline关键字从C++的第一个版本——C99,就可以使用了。你可以同时在C或者C++中使用微软特有的关键字_inline,这在你使用不支持inline的老式C版本时是很有用的。并且,你可以使用__forceinline关键字(C和C++)来强制编译器将任何可以内联的函数内联。最后但是很重要的一点是,你可以告诉编译器以确定或者不确定的深度拆开一个递归函数,这可以通过使用inline_recursion编译指令来达成。注意编译器当下没有提供任何特性可以让你在函数调用时控制内联,一切都只能在函数定义时控制。

默认情况下生效的/Ob0开关会完全禁用内联功能。你应该在调试代码时使用这一开关(它在Visual Studio Debug配置下是自动打开的)。/Ob1开关让编译器只在函数被定义为inline,__inline 或者__forceinline时,才考虑将函数内联。/Ob2开关在指定了/O[1|2|x]时生效,编译器将会考虑所有的函数是否可以内联。在我看来,只有在/Ob1控制内联时考虑是否使用inline或_inline才是有意义的。

在一些特定的条件下,编译器是不能将函数内联的。举个例子,当虚调用一个虚函数时,因为编译器不知道哪个函数将会被调用,所以这个函数不能被内联。另一个例子是当通过指针调用一个函数而不是通过函数名时。你应该尽力避免这些条件来使得函数可以被内联。具体请参考MSDN文档,那里列出了不能被内联的完整条件列表。

某些优化,当其作用于整个程序级别时,往往比其作用于局部时更加有效,函数内联就是这种类型的优化之一。事实上,大多数优化都在整体级别更加有效。在这一部分余下的内容中,我将会讨论被称作COMDAT优化的一类特定优化。

默认情况下,当编译翻译单元时,所有的代码都被存储到结果目标文件的一个单独区块。链接器在单独区块的范畴上进行操作:也就是对这些区块进行移除、合并或者重新排序。(但是)这种会妨碍链接器进行三项优化工作,而这三项优化工作对显著减少可执行代码量和提升性能又非常重要。第一项是消除未被引用的函数和全局变量;第二项是合并相同的函数和全局常量;第三项是重新对函数和全局变量排序,使得那些在同一路径上执行的函数和被一起访问的变量在物理内存中离得更近,这会让程序有更好的局部性。

为了能让这些链接器优化生效,你可以通过分别打开/Gy(函数级别链接)和/Gw(全局数据优化)来分别让编译器对位于在不同区块的函数和变量进行打包操作。这些区块被称为COMDATs。你也可以用__declspec( selectany)标记特定的全局数据变量来告诉编译器把这个变量加入COMDAT。然后,通过指定/OPT:REF链接器开关,链接器就会删去未被引用的函数和全局变量。你也可以通过指定/OPT:ICF开关,链接器就会合并相同的函数和全局常数变量。(ICF代表Identical COMDAT Folding。)通过/ORDER链接器开关,你可以让链接器把COMDAT以特定的顺序放入生成镜像。注意所有的这些优化都是链接器优化所以不需要/GL开关。如果是要对程序进行调试,并且目的明确,那么/OPT:REF和/OPT:ICF开关应当关闭。

你应该尽可能使用链接时代码生成(LTCG)。唯一不使用的原因是当你想要分发生成的目标文件和二进制文件时。记得这些文件包含通用中间语言(CIL)而不是汇编语言,通用中间语言只能被生成它的特定版本的编译器和链接器识别,这将会明显限制目标文件的使用,因为开发者必须使用相同版本的编译器以使用这些文件。这种情况下,除非你愿意为每个版本的编译器都分发一份目标文件,否则你应该使用编译时代码生成。除了限制使用,这些目标文件通常比相应的汇编目标文件更加庞大。但是记得CIL目标文件带来的巨大好处,那就是可以进行程序全局优化(WPO)。

循环优化

Visual C++支持多种循环优化,但是我只讨论其中的3种:循环展开,自动向量化和循环不变量代码移动。如果你修改了Figure1中的代码让m代替n作为sumOfCubes的参数,编译器将不能推断出参数的值,所以必须让函数可以处理任何参数。生成函数被高度优化并且尺寸很大,所以编译器不会将它作为内联函数。

用/O1生成汇编代码,会在空间尺寸上进行优化。在这种情况下,不会对sumOfCubes函数实行任何优化操作。用/O2生成代码针对执行速度进行优化。生成代码的长度会很长但是执行效率显著提高,因为sumOfCubes内部的循环被展开并且向量化了。有一个概念很重要,必须理解:如果不把cube函数内联就不能进行向量化。而且,不进行内联的话循环展开并不会变得高效。Figure3 显示了生成的汇编代码的流程图。这个流程图对x86和x86-64架构都适用。

图3 sumOfCubes流程图

在Figure3中,绿色的菱形代表开始点,红色矩形代表结束点。蓝色菱形代表在运行时作为sumOfCubes函数中一部分而被执行的条件。如果处理器支持SSE4并且x大于等于8,就会使用SSE4指令同时执行四个乘法指令。同时把同一操作在多个值上执行的过程被称为向量化。编译器也会将循环展开,就是说循环体将会把每次迭代循环重复一次。这样做的最终效果就是八次乘法在每次迭代都会被执行。当x的值小于8时,传统的指令将会被用于执行余下的运算。注意到编译器放出了结合了三个独立结尾的循环结束点而不是一个。这将会减少跳转次数。

循环展开是重复执行循环体的过程,展开后的循环每次把未展开循环内的循环体执行不止一次。这样做的原因是可以通过减少循环控制指令的执行频率来提升性能。也许更重要的是,这样可以允许编译器进行许多其他优化工作,比如向量化。循环展开的弊端是会增加代码量和寄存器的压力。但是这可能使性能达到两位百分数级别的提升,当然这是和具体的循环体有关的。

不同于x86处理器,所有的x86-64处理器都支持SSE2.不仅如此,你可以在最新的x86-64微处理器架构上(包括Intel和AMD)通过打开/arch开关来利用AVX/AVX2指令集。打开/architecture:AVX2也会允许编译器使用FMA和BMI指令集。

当前的Visual C++编译器不支持控制循环展开。但是你可以通过使用模版结合__ forceinline关键字来模仿这一技术。你可以通过使用no_vector选项来禁用对于某个函数的自动向量化。

通过观察生成的汇编代码,如果你有足够敏锐的眼睛的话你会注意到代码还有少许优化空间。但是,编译器已经做了很多工作了,并且不会再花更多的时间分析代码和进行一些无关紧要的优化。

SumOfCubes(原文是someOfCubes,应该是写错了——译者注)不是唯一一个循环被展开的函数。如果你修改代码让m作为参数而不是n,编译器将不能对代码进行估计,因此必须放出其代码。在这种情况下,循环被展开了两次。

最后我要讨论的优化是循环不变量代码移动(loop-invariant code motion)。考虑如下代码:

  int sum(int x) {
  int result = 0;
  int count = 0;
  for (int i = 1; i &lt;= x; ++i) {
    ++count;
    result += i;
  }
  printf("%d", count);
  return result;
}

这里唯一的改变是增加了一个变量并且在每次循环进行自增,然后打印。不难看出这段代码可以通过把变量count的自增移出循环来优化。也就是说,我可以直接把x的值赋给变量count。这种优化被称为循环不变量代码移动(loop-invariant code motion)。循环不变量部分清楚的表明这项技术只能用于其代码不依赖于任何循环之前的表达式的情况。

那么这里有一个问题:如果你自己来进行这项优化,生成的代码可能在某些情况下会导致性能下降。能发现为什么吗?考虑x为非正数的情况。循环将不被执行,这意味着未被手动优化的代码中count不会被访问。但是,在我们手动优化过的代码中在循环外进行了一次不必要的赋值操作,把x赋给了count。更甚者,如果x是负数,count就会拥有错误的值。程序员和编译器都容易受到这种陷阱的影响。所幸Visual C++编译器足够聪明地在赋值之前加上了循环条件,这样可以对所有x的值都生成性能有所提升的代码。

综上所述,如果你既不是编译器也不是编译器优化方面的专家,你应该避免仅仅因为想让代码更快而进行手工修改。管住你的手并且相信编译器将会优化你的代码。

控制优化

除了/O1,/O2,和/Ox编译开关,你还可以使用控制优化编译来达到让某个函数优化的目的,其形式如下:

#pragma optimize( "[optimization-list]", {on | off} )

[optimization-list]可以为空或者一个或多个紧跟的值:g,s,t和y。分别对应编译器开关/Og,/Os,/Ot和/Oy.

空列表和off参数会让所有的优化都被关闭,不管之前的编译器开关是否被打开。空列表和on参数会让之前打开的编译器开关生效。

/Og开关启用全局优化,全局优化只作用域那些通过表面分析就可以被优化的函数上,而这些函数内部调用的其他函数则不会被优化。如果(链接时代码生成)LTCG被启用,/Og允许代码全局优化(WPO)。

当你需要让不同的函数进行不同的优化时,比如一些进行空间尺寸优化而另一些进行执行速度优化,那么优化编译参数就很有用了。但是如果真的想达到那种粒度的控制,你应该考虑性能分析引导优化(PGO),就是通过对运行测量代码时的行为信息进行记录,然后使用这一纪录对代码进行优化的过程。编译器使用性能分析来决定怎样优化代码。Visual Studio提供了必要的工具,来将这一技术同时应用于本机代码和托管代码上。

.NET中的优化

在.NET的编译模型中没有链接器。但是有一个源代码编译器(C# compiler)和即时编译器(JIT compiler),源代码编译器只进行很小的一部分优化。比如它不会执行函数内联和循环优化。而这些优化是由即时编译器执行的。在4.5以前的所有.NET Framework JIT都不支持SIMD指令集。但是.NET Framework 4.5.1和之后的版本都装有支持SIMD的即时编译器,被称为RyuJIT。

从优化能力上来讲RyuJIT和Visual C++有什么不同呢?因为RyuJIT是在运行时完成其工作的,所以它可以完成一些Visual C++不能完成的工作。比如在运行时,RyuJIT可能会判定,在这次程序的运行中一个if语句的条件永远不会为true,所以就可以将它移除。RyuJIT也可以利用他所运行的处理器的能力。比如如果处理器支持SSE4.1,即时编译器就会只写出sumOfCubes函数的SSE4.1指令,让生成打的代码更加紧凑。但是它不能花更多的时间来优化代码,因为即时编译所花的时间会影响到程序的性能。另一方面,Visual C++编译器可以花更多的时间寻找和利用更多恰当的优化机会。微软新推出了一项称为.NET Native的全新技术,允许你使用Visual C++编译器后端对托管代码(Managed Code)进行编译和优化,并形成自包含的独立可执行程序。当下这项技术只支持Windows Store apps。

在当前控制托管代码的能力是很有限的。C#和VB编译器只允许使用/optimize编译器开关打开或者关闭优化功能。为了控制即时编译优化,你可以在方法上使用System.Runtime.Compiler­Services.MethodImpl属性和MethodImplOptions中指定的选项。NoOptimization选项可以关闭优化,NoInlining阻止方法被内联,AggressiveInlining (.NET 4.5)选项推荐(不仅仅是提示)即时编译器将一个方法内联。

结语

本文中提到的所有优化功能都会显著地将你的代码效率提升两位百分数级别,并且Visual C++编译器支持所有这些优化。重要的是这些技术能够在应用之后,带来其他更多的优化。本文绝不敢奢望能够对Visual C++编译器的优化工作进行一次综合全面的讨论。但是我希望通过本文可以让你领会编译器的精妙。Visual C++可以做比这多得多的事情,所以敬请期待Part2。


作者介绍:Hadi Brais是印度德里理工大学(IITD)的一名博士生,他的主要研究课题是编译器优化和下一代内存技术。他花费了很多时间使用C/C++/C#语言来编写程序,并对CLR和CRT做深入的研究。他的博客地址是:hadibrais.wordpress.com , 邮箱为:hadi.b@live.com .

每个程序员都应当知道的编译器优化知识,首发于博客 - 伯乐在线

11 Mar 00:41

JAVA面试700问(六)

by yhfolive

原文地址 译者:olive(yhfolive@gmail.com)

1.wait(),notify()和notifyAll()的作用是什么?

  • wait():使当前线程停止,直到另一个线程调用notify()方法或notifyAll()方法.。
  • notify():唤醒这个对象的监视器上等待的一个线程。
  • notifyAll():将引发wait()状态的所有的线程变为就绪状态,所有的线程继续执行。这些线程将基于优先级以及基于JVM选择来执行。
  • 注意:这三个方法在被调用之前必须获得对象的锁。

2.File类与RandomAccessFile类之间的区别是什么?

基于操作系统的文件系统服务,比如文件、文件夹的创建,权限的核实,改变文件名等,都由java.io.File类提供。java.io.RandomAccessFile类提供对文件的随机访问。使用这个类同样可以读、写,操作数据,同时更为方便的是,这个类可以读写基本类型,这有助于以结构化的方式处理数据文件。

3.什么是同步方法和同步代码块?

同步方法用于控制在多线程下对对象的访问。获得对象的锁后,线程才能执行同步方法。
同步代码块是类似于同步方法。不同之处在于同步方法是类的一部分,并明确地被另外一个方法调用。而一个同步代码块是由其被包含的方法执行,而这个包含同步代码块的方法还要被另外一个方法调用。

4.什么是Java类加载器?什么又是动态类加载?

类可以被JVM访问并通过名称来引用。JVM启动后,这些类被JVM读取。

所有的类加载器以一定的层次关系被JVM识别。第一个被加载的类是所谓的“初始化类”,也就是包含main方法的类。这个类是由JVM本身加载。所有其他后续类,按照它们在 ‘class chaining’中出现的顺序被加载。每一个类加载器都会创建一个命名空间来唯一识别它们。每个JVM至少包含一个引导类加载器。

5.抽象类和接口之间的区别是什么?什么时候使用抽象类?什么时候使用接口?

一个抽象类除了具体方法外还有一个或多个抽象方法。所有抽象方法必须被它的子类覆盖。一个抽象类可以没有抽象方法。而接口只包含方法签名。

默认情况下,一个接口中的的所有方法都是public的和abstract的。需要的时候应该为接口中的方法提供访问说明符。接口的实现类必须提供所有接口中声明的方法的实现。

抽象类的声明以关键字abstract开始,接口以interface开始。
一个接口可以继承任意数量的接口,一个抽象类只能继承一个抽象类。
接口的成员变量都是public static 的,抽象类的成员变量可以有任意声明。

当一个类继承自另一个类后,它还可以实现多个接口。当一个类需要实现接口所有的方法时,接口就很有用。
覆盖抽象类的具体方法不是强制要求的。当值需要实现若干方法,并想利用继承的优势的时候,就可以使用抽象类

6.ArrayList和Vector的主要区别?HashMap和HashTable的主要区别?

ArrayList和Vector的区别:
Vector是同步的,ArrayList不是。
Vector默认大小为10,ArrayList没有默认大小。
Vector有一个方法,可以指定Vector的大小,ArrayList不行。
Vector扩容的时候成倍增长,而ArrayList以50%增长。

HashMap和Hashtable的区别:
Hashtable是同步的,Hashmap不是。
Hashtable的值不能为空,Hashmap可以。
HashMap不能保证map中元素的顺序不变。
HashMap通过Map接口来实现。哈希表通过继承Dictionary 类实现。

7.解释Java集合框架?Java集合框架带来什么好处?

集合是由一组对象组成的一个对象。集合框架,让应用程序更有效率,更少的硬编码,不同的对象由类似的实现。使用集合框架,主要是为了集合框架的标准性和简单性。

集合框架的又一个好处是,一组对象,没有顺序或类型类型也可以通过一个单一的对象来表示。
Java集合框架提供了以下好处:
•减少编程工作量
•提高程序的速度和质量
•允许无关的API之间协同工作
•减少学习和使用的成本
•不要去设计新的API
•促进软件重用

8.对象的浅克隆和深克隆的主要区别?

Shallow cloning and deep cloning in Java – April 02, 2009 at 17:00 PM by Vidya Sagar

浅克隆只克隆了对象的引用。
深克隆克隆对象本身。
两者的区别用Java实例不好理解,但如果用数据库来想会更好理解:
如果实体需要“级联删除”被删除那就就要深克隆。如果“级联删除”是没有必要删除就需要浅克隆。

Shallow cloning and deep cloning in Java – July 31, 2009 at 14:00 PM by Amit Satpute

当对象被浅复制的时候,结果是复制的结果和原来的引用指向相同的对象。由于这个原因,原来的引用对对象的改变会映射到复制的结果。
当对象被深克隆时,对象的值也被复制,由于两个对象维持独立的引用,对对象的改变不会相互影响。

9.final,finally finalize()的区别?

Final, finally and finalize() in Java – April 02, 2009 at 17:00 PM by Vidya Sagar

• final

final成员变量的值不能被改变。
final方法不能被重写。
final类不能被继承。

• finally

这是一个语句块,咱try /catch块后这个语句块一定会被执行。

• finalize()

当对象要被GC回收时,这个方法会执行。

Final, finally and finalize() in Java – April 02, 2009 at 17:00 PM by Vidya Sagar
• final – 一个关键字/访问修饰符用于定义常量。

• finally – 除了try块调用了System.exit(0), finally块总是与try和catch块一起。这是确保在发生error/exception的情况下,finally块都能执行。finally不仅用于异常处理,还可以用于清理代码,比如返回值,continue或break语句的使用,I/O流的关闭等,在finally块中执行清理代码是一种良好的编程习惯。

• finalize()  -此方法与垃圾回收相关。在系统清理资源之前,这个方法会被调用。

10.什么是类型转换?什么是向上转型和向下转型?什么情况下会抛出ClassCastException异常?

Type casting and down casting in Java – April 02, 2009 at 17:00 PM by Vidya Sagar

“类型转换”:把对象从一种类型转换为另一种类型的表达式。
向上转型是把子类转换成父类,例如把子类的引用赋予父类的引用。
向下转型是把父类转换为子类,例如把父类的引用赋予子类的引用。
无效的类型转换会抛出ClassCastException。它表示该应用程序试图将对象转换为子类,但这个子类而不是对象的实例。当试图向容器中插入不兼容的对象时也会抛出这个异常。

Type casting and down casting in Java – July 31, 2009 at 14:00 PM by Amit Satpute

将类型的值从一种类型到另一种类型叫做类型转换。
如:

int i =10;
float x = (float) i;

向上和向下转型和类有关。
如:

Parent p = new Child();

11.有几种内部类,分别是什么?

Different types of inner classes in Java – April 02, 2009 at 17:00 PM by Vidya Sagar

有4种类型的内部类 – 成员内部类,局部内部类,静态内部类和匿名内部类。
1.成员内部类-类(外部类)的成员。
2.局部内部类-这是一个块中定义的内部类。
3.静态内部类-类似于静态成员,这个类本身是静态的。
4.匿名内部类-类没有名称,只能实现一个接口或继承自一个抽象类。

外部类不能任意访问内部类的成员。要做到这一点,外部类必须创建其内部类的对象(静态内部类除外)。内部类可以访问所有的外部类的成员,因为内部类就像是一个类的成员。
匿名内部类是使用的场景如下:一个对象创建后, 调用单一的方法,调用完成后立即回收对象。多用于事件驱动编程。
嵌套类用于AWT,Swing和applet。动作事件发生后匿名内部类或被调用。

12.什么是Java包?

当项目的规模变大后,项目文件的管理变得非常繁琐。较小的项目可以将相关的文件存储在单个位置。
包可以有效地用来管理需要编写很多代码的问题。
不同功能的文件可以保存在不同的包中,有类似或相关功能的文件可以保存在同一包中。
例如:文件中的java.io包中的文件包含I / O相关的功能,而在java.net包中的文件包含与网络相关的功能。
包是类的集合,可以避免命名空间的冲突。
多个有相同名字的文件不能放在同一个包中。
一个类层次结构可以创建和保存在一个包中。

包也可以被看作是一个类的容器类。

13.什么是Java本地方法?

Native methods in Java – posted by Amit SatputeJava

应用程序可以调用C,C ++或汇编代码。这样做有时是为了性能,有时为了使用底层操作系统的系统服务或者操作系统的GUI API。具体步骤为:

* 编写和编译java代码
* 然后创建一个C头文件
* 创建Ç存根文件
* 编写C代码
* 创建共享代码库(或DLL)
* 运行应用程序
Native methods in Java – posted by Vidya Sagar

可以用java调用c或者c++的代码。这个过程是处于性能的考虑,或访问底层操作系统。JNI框架就是为了执行这样的程序而设计的。

14.什么是Java反射API?

Reflection API in Java – posted by Amit Satpute

用反射API可以在运行获取类或者对象的相关信息。反射类可以在运行时动态调用另一个类的方法。使用反射类你可以获得一个实例的字段,并且改变字段的内容。反思API包含的java.lang.Class类和java.lang.reflects中的类:Field, Method, Constructor, Array, 和Modifier.

Reflection API in Java – posted by Vidya Sagar

反射是一个用于检查在JVM中运行的应用程序的运行时行为的一个过程。反射API允许运行时创建一个不知道类名的类的实例。定制的处理类可以在没有编译的情况下嵌入系统。所有这些处理类都是继承基类“MassageProcessor’而得到的。

15.解释私有构造器的作用。

Java private constructor – August 11, 2008, 19:00 pm by Amit Satpute

私有构造函数不能被任何派生类访问也不能被其他任何类访问。所以,如果对象尚未初始化,你必须提供一个public 的方法来调用私有构造函数,或者如果它已经被初始化,你就用这个public方法返回对象的实例。
这种方法对不能被被实例化的对象很有用。

Java private constructor – Feb 27, 2009, 17:35 pm by Vidya Sagar

私有构造器可以阻止其包含类和子类的外部实例化。对象可以被创建,但创造是在内部完成。
私有构造可在下列情况下使用:
类只有static的字段以及方法
一个只有static final 字段的类
单例模式
工厂方法
类型安全的枚举类

16.什么是静态初始化块?

Static Initializers in Java – posted by Amit Satpute

静态初始化块可以被看做一个,没有名字,没有参数,没有返回值的成员。没有必要从类定义之外引用它。语法:

static
{
//CODE
}

静态初始化块中的代码是由JVM进行类加载时执行。
因为它是在类加载时自动执行,参数对它来说没有任何意义,所以静态初始化块没有一个参数列表。

Static Initializers in Java – posted by Vidya Sagar

静态初始化块是一个名为’static’的代码块。一个类可以包含一个或多个静态初始化块。静态初始化块中的代码会先于构造器中的代码执行,并且按照各静态初始化快在类中的顺序执行。当类被加载的时候,静态初始化中的代码会被执行。
例如:

class Test
{
static int stNumber;
int number;
Test()
{
number = 10;
}
static
{
stNumber=30;
}
…… // other methods here

}

在上面的例子中,静态初始化块中的代码先执行,然后用才是构造器中的代码。

17.&和&&的区别?

Java – Boolean & operator and the && operator – Feb 28, 2010 at 16:16 PM by Vidya Sagar

&&用于两个关系式的逻辑与运算
&用于两个数的按位与运算

18.什么是任务的优先级和它是如何被调度的?

Java – task’s priority – Feb 28, 2010 at 16:16 PM by Vidya Sagar

一个任务的优先级是一个整数值。这个值用于各个任务之间执行的相对顺序。调度是任务执行的顺序它被任务调度器控制。任务调度器会尝试让具有较高优先级的任务在优先级较低的任务之前执行。

Java – task’s priority – Jan 15, 2009 at 8:10 am by Amit Satpute
优先级是一个整数值。
任务调度器会根据这个值来确定何时执行这个任务。

19.抢占式调度和时间分片之间的区别是什么?

Java – Preemptive scheduling and time slicing – Feb 28, 2010 at 16:16 PM by Vidya Sagar

抢占式调度:具有最高优先级的任务将被执行,直到它进入等待或死亡状态。
时间分片:任务执行特定的和预先确定的时间片,然后重新进入就绪任务池。调度器的任务根据优先级以及其他因素来确定下一个要执行的任务。

Java – Preemptive scheduling and time slicing – Jan 15, 2009 at 8:10 am by Amit Satpute

如果某一个任务正在运行并且所使用的调度方法是抢占式调度,那么如果存在比正在执行的任务更高优先级的另一个任务,那么正在执行的任务将会被具有更高优先级的任务抢占。
在时间分片的调度方法中,任务会执行特定的时间片。
任务执行完后,如果存在具有更高优先级的另一项任务,调度器要执行的下一个任务取决于任务的优先级以及其他因素。

20.什么是同步的,为什么它很重要?

Java – synchronization – Feb 28, 2010 at 16:16 PM by Vidya Sagar

在多线程中访问共享资源时,只有一个线程可以在同一时间访问一个指定的资源,称为同步。
同步用于防止多线程造成的数据的不一致。
对于多线程,同步具有控制多个线程对共享资源访问的能力。在非同步时,可能有一个线程真在修改一个共享变量,同时另一个线程在更新相同的共享数据。

Java – synchronization – Jan 15, 2009 at 8:10 am by Amit Satpute

线程通过访问共享字段和对象实现通信。
然而,有线程冲突和内存不一致的可能性。
同步是用来防止这种情况。
在同步中,如果一个对象对多个线程是可见的,所有读取或写入该对象的变量,通过同步的方式进行。

21.如何使用Observer类和Observable接口?

Java – Observer and Observable – Jan 15, 2009 at 8:10 am by Amit Satpute

Observable 类代表一个可观察的对象。
要被观察的对象可以用Observable 类的子类来表示。
当Observable实例发生改变时 ,应用程序调用Observable对象的notifyObservers方法,通知它的所有Observer,这些Observer会调用它们的update()方法响应通知。

22.什么是ResultSetMetaData的?

Java – What is ResultSetMetaData? – Jan 15, 2009 at 8:10 am by Amit Satpute

ResultSetMetaData的是一个对象,用于获取ResultSet对象的类型和列的属性的信息。
如下:

ResultSet resultSet1 = statement1.executeQuery(“SELECT a, b, c FROM TABLE2″);
ResultSetMetaData rsmd = rs.getMetaData();

23.解释CacheRowset,JdbcRowSet的和的WebRowSet。

Java – CacheRowset, JDBCRowset and WebRowset – Jan 15, 2009 at 8:10 am by Amit Satpute

JdbcRowSet是一种保持连接的rowset ,它使用JDBC驱动保持和数据源的连接。
CachedRowSet和WebRoeSet是不保持连接的结果集,仅当它们读取或者写入数据的时候才和数据源连接。

如下几个例子:
Example 1:

JdbcRowSet j = new JdbcRowSetImpl();
j.setCommand(“SELECT * FROM TABLE_NAME);
j.setURL(“jdbc:myDriver:myAttribute”);
j.setUsername(“XXX”);
j.setPassword(“YYYo”);
j.execute();
Example 2:
ResultSet rs = stmt.executeQuery(“SELECT * FROM AUTHORS”);
CachedRowSet crset = new CachedRowSetImpl();
crset.populate(rs);
Example 3:
WebRowSet wrs = new WebRowSetImpl();
wrs.populate(rs);
wrs.absolute(2)
wrs.updateString(1, “stringXXX”);

24.解释一类的静态和非静态成员之间的差异。

Java – difference between Static and Non-Static fields – Feb 22, 2009 at 10:10 am by Vidya Sagar

静态成员属于类。该类的对象不能有这些字段的副本。这些字段由类来直接引用。例如:Employee. company。无需创建该类的一个实例,这些字段也可被访问。

非静态字段由类的所有对象访问。每个对象有非静态字段的一个副本。要使用非静态字段,必须先创建类的实例。

25.instanceof关键字的作用。

Java – Describe the use of “instanceof” keyword. – with answers posted on July 22, 2008 at 8:10 am

“instanceof” 用于检查一个对象是什么类型。

26.垃圾回收机制有什么缺点?

Java – What is the disadvantage of garbage collector? – with answers posted on July 22, 2008 at 8:10 am

尽管垃圾收集器在它自己的线程中运行,但仍然对性能有影响。它增加了开销,因为JVM必须跟踪所有未被引用的对象,然后释放这些未引用的对象。
垃圾收集器可使用“的System.gc”或“Runtime.gc”来强制执行。

27.资源清理(Finalization)的作用?

Java – What is the purpose of finalization? – July 06, 2009 at 10:00 AM

Finalization通过调用finalized()实现。Finalization的目的是为了对象得到清理之前执行某些操作。此方法在GC之前执行必要的清理工作,也就是消除’orphan objects’.

28.定义类和对象并使用Java的例子解释。

Java – Define class and object. – Feb 14, 2009 at 8:10 am by Vidya Sagar

类:类是一个封装了数据和在数据上的操作的程序结构。在面向对象的编程中,类可以看成对象的蓝图。
对象:一个对象是属于某个类的程序结构,具有状态和行为。
例如 Employee 是一个类
具有的唯一标识的一个Employee 是一个对象的一个​​例子。

class Employee
{
// instance variables declaration
// Methods definition
}

一个employee 对象是一个特定的employee

Employee vismay =new Employee();

vismay 是此对象的引用。

29.解释类和实例?

Java – Explain class vs. instance with example using java. – Feb 16, 2009 at 19:50 am by Vidya Sagar

一类是封装了数据和操作上的操作的程序结构。它是一个对象的蓝图。
一个对象可以被称为一个类的一个“实例”。
类中声明的变量和方法,除方法中的变量外都被称为实例变量,他们在每个对象中都有一份拷贝。一旦对象被创建,可以说,创建了一个类的实例。
类变量和方法创建一次,而每次创建一个对象都会创建实例变量和成员

30.什么是方法?说出几种方法的签名。

Java – What is a method? Provide several signatures of the methods – July 06, 2009 at 10:00 AM

java方法是一组执行任务的语句。一个方法要被放在类中。
方法签名:方法的名字,返回类型和参数的数目组成方法签名。

一个成员可以有以下方法签名:
访问说明符- public, private, protected 等
访问修饰符- static, synchronized等
返回类型- void, int, String 等
方法的名字-
参数

31.创建类实例的方法有几种?分别给出描述。

Java – Explain how to create instance of a class by giving an example. – Feb 18, 2009 at 9:10 am by Vidya Sagar

Java 有三种创建类实例的方法:

* 用 new 操作符

* 用 Class.forName(classname).newInstance() method

* 用 clone() 方法

32.什么是单例类?有什么用途?

Java – What is singleton class? Where is it used? – Feb 18, 2009 at 9:10 am by Vidya Sagar

一个类可以定义为单例类,当且仅当它只能创建一个类的实例。当应用中想只适用类的一个实例时单例类会很有用。
以下是一些单例类的用途。
1.对对象的访问进行控制。
2.当全局范围需要”just-in-time”实例化的时候。

原创文章,转载请注明: 转载自并发编程网 – ifeve.com

本文链接地址: JAVA面试700问(六)

11 Mar 00:39

文章: Kafka剖析(一):Kafka背景及架构介绍

by 郭俊

Kafka是由LinkedIn开发的一个分布式的消息系统,使用Scala编写,它以可水平扩展和高吞吐率而被广泛使用。目前越来越多的开源分布式处理系统如Cloudera、Apache Storm、Spark都支持与Kafka集成。InfoQ一直在紧密关注Kafka的应用以及发展,“Kafka剖析”专栏将会从架构设计、实现、应用场景、性能等方面深度解析Kafka。

背景介绍

Kafka创建背景

Kafka是一个消息系统,原本开发自LinkedIn,用作LinkedIn的活动流(Activity Stream)和运营数据处理管道(Pipeline)的基础。现在它已被多家不同类型的公司 作为多种类型的数据管道和消息系统使用。

活动流数据是几乎所有站点在对其网站使用情况做报表时都要用到的数据中最常规的部分。活动数据包括页面访问量(Page View)、被查看内容方面的信息以及搜索情况等内容。这种数据通常的处理方式是先把各种活动以日志的形式写入某种文件,然后周期性地对这些文件进行统计分析。运营数据指的是服务器的性能数据(CPU、IO使用率、请求时间、服务日志等等数据)。运营数据的统计方法种类繁多。

近年来,活动和运营数据处理已经成为了网站软件产品特性中一个至关重要的组成部分,这就需要一套稍微更加复杂的基础设施对其提供支持。

Kafka简介

Kafka是一种分布式的,基于发布/订阅的消息系统。主要设计目标如下:

  • 以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能保证常数时间复杂度的访问性能。
  • 高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒100K条以上消息的传输。
  • 支持Kafka Server间的消息分区,及分布式消费,同时保证每个Partition内的消息顺序传输。
  • 同时支持离线数据处理和实时数据处理。
  • Scale out:支持在线水平扩展。

为何使用消息系统

  • 解耦

    在项目启动之初来预测将来项目会碰到什么需求,是极其困难的。消息系统在处理过程中间插入了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口。这允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。

  • 冗余

    有些情况下,处理数据的过程会失败。除非数据被持久化,否则将造成丢失。消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的"插入-获取-删除"范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。

  • 扩展性

    因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。不需要改变代码、不需要调节参数。扩展就像调大电力按钮一样简单。

  • 灵活性 & 峰值处理能力

    在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见;如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。

  • 可恢复性

    系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。

  • 顺序保证

    在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。Kafka保证一个Partition内的消息的有序性。

  • 缓冲

    在任何重要的系统中,都会有需要不同的处理时间的元素。例如,加载一张图片比应用过滤器花费更少的时间。消息队列通过一个缓冲层来帮助任务最高效率的执行———写入队列的处理会尽可能的快速。该缓冲有助于控制和优化数据流经过系统的速度。

  • 异步通信

    很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。

常用Message Queue对比

  • RabbitMQ

    RabbitMQ是使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正因如此,它非常重量级,更适合于企业级的开发。同时实现了Broker构架,这意味着消息在发送给客户端时先在中心队列排队。对路由,负载均衡或者数据持久化都有很好的支持。

  • Redis

    Redis是一个基于Key-Value对的NoSQL数据库,开发维护很活跃。虽然它是一个Key-Value数据库存储系统,但它本身支持MQ功能,所以完全可以当做一个轻量级的队列服务来使用。对于RabbitMQ和Redis的入队和出队操作,各执行100万次,每10万次记录一次执行时间。测试数据分为128Bytes、512Bytes、1K和10K四个不同大小的数据。实验表明:入队时,当数据比较小时Redis的性能要高于RabbitMQ,而如果数据大小超过了10K,Redis则慢的无法忍受;出队时,无论数据大小,Redis都表现出非常好的性能,而RabbitMQ的出队性能则远低于Redis。

  • ZeroMQ

    ZeroMQ号称最快的消息队列系统,尤其针对大吞吐量的需求场景。ZeroMQ能够实现RabbitMQ不擅长的高级/复杂的队列,但是开发人员需要自己组合多种技术框架,技术上的复杂度是对这MQ能够应用成功的挑战。ZeroMQ具有一个独特的非中间件的模式,你不需要安装和运行一个消息服务器或中间件,因为你的应用程序将扮演这个服务器角色。你只需要简单的引用ZeroMQ程序库,可以使用NuGet安装,然后你就可以愉快的在应用程序之间发送消息了。但是ZeroMQ仅提供非持久性的队列,也就是说如果宕机,数据将会丢失。其中,Twitter的Storm 0.9.0以前的版本中默认使用ZeroMQ作为数据流的传输(Storm从0.9版本开始同时支持ZeroMQ和Netty作为传输模块)。

  • ActiveMQ

    ActiveMQ是Apache下的一个子项目。 类似于ZeroMQ,它能够以代理人和点对点的技术实现队列。同时类似于RabbitMQ,它少量代码就可以高效地实现高级应用场景。

  • Kafka/Jafka

    Kafka是Apache下的一个子项目,是一个高性能跨语言分布式发布/订阅消息队列系统,而Jafka是在Kafka之上孵化而来的,即Kafka的一个升级版。具有以下特性:快速持久化,可以在O(1)的系统开销下进行消息持久化;高吞吐,在一台普通的服务器上既可以达到10W/s的吞吐速率;完全的分布式系统,Broker、Producer、Consumer都原生自动支持分布式,自动实现负载均衡;支持Hadoop数据并行加载,对于像Hadoop的一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka通过Hadoop的并行加载机制统一了在线和离线的消息处理。Apache Kafka相对于ActiveMQ是一个非常轻量级的消息系统,除了性能非常好之外,还是一个工作良好的分布式系统。

Kafka架构

Terminology

  • Broker

    Kafka集群包含一个或多个服务器,这种服务器被称为broker

  • Topic

    每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处)

  • Partition

    Parition是物理上的概念,每个Topic包含一个或多个Partition.

  • Producer

    负责发布消息到Kafka broker

  • Consumer

    消息消费者,向Kafka broker读取消息的客户端。

  • Consumer Group

    每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定group name则属于默认的group)。

Kafka拓扑结构

如上图所示,一个典型的Kafka集群中包含若干Producer(可以是web前端产生的Page View,或者是服务器日志,系统CPU、Memory等),若干broker(Kafka支持水平扩展,一般broker数量越多,集群吞吐率越高),若干Consumer Group,以及一个Zookeeper集群。Kafka通过Zookeeper管理集群配置,选举leader,以及在Consumer Group发生变化时进行rebalance。Producer使用push模式将消息发布到broker,Consumer使用pull模式从broker订阅并消费消息。

Topic & Partition

Topic在逻辑上可以被认为是一个queue,每条消费都必须指定它的Topic,可以简单理解为必须指明把这条消息放进哪个queue里。为了使得Kafka的吞吐率可以线性提高,物理上把Topic分成一个或多个Partition,每个Partition在物理上对应一个文件夹,该文件夹下存储这个Partition的所有消息和索引文件。若创建topic1和topic2两个topic,且分别有13个和19个分区,则整个集群上会相应会生成共32个文件夹(本文所用集群共8个节点,此处topic1和topic2 replication-factor均为1),如下图所示。

每个日志文件都是一个log entrie序列,每个log entrie包含一个4字节整型数值(值为N+5),1个字节的"magic value",4个字节的CRC校验码,其后跟N个字节的消息体。每条消息都有一个当前Partition下唯一的64字节的offset,它指明了这条消息的起始位置。磁盘上存储的消息格式如下:

message length : 4 bytes (value: 1+4+n)
"magic" value : 1 byte 
crc : 4 bytes 
payload : n bytes 

这个log entries并非由一个文件构成,而是分成多个segment,每个segment以该segment第一条消息的offset命名并以“.kafka”为后缀。另外会有一个索引文件,它标明了每个segment下包含的log entry的offset范围,如下图所示。

因为每条消息都被append到该Partition中,属于顺序写磁盘,因此效率非常高(经验证,顺序写磁盘效率比随机写内存还要高,这是Kafka高吞吐率的一个很重要的保证)。

对于传统的message queue而言,一般会删除已经被消费的消息,而Kafka集群会保留所有的消息,无论其被消费与否。当然,因为磁盘限制,不可能永久保留所有数据(实际上也没必要),因此Kafka提供两种策略删除旧数据。一是基于时间,二是基于Partition文件大小。例如可以通过配置$KAFKA_HOME/config/server.properties,让Kafka删除一周前的数据,也可在Partition文件超过1GB时删除旧数据,配置如下所示。

  
# The minimum age of a log file to be eligible for deletion
log.retention.hours=168
# The maximum size of a log segment file. When this size is reached a new log segment will be created.
log.segment.bytes=1073741824
# The interval at which log segments are checked to see if they can be deleted according to the retention policies
log.retention.check.interval.ms=300000
# If log.cleaner.enable=true is set the cleaner will be enabled and individual logs can then be marked for log compaction.
log.cleaner.enable=false

这里要注意,因为Kafka读取特定消息的时间复杂度为O(1),即与文件大小无关,所以这里删除过期文件与提高Kafka性能无关。选择怎样的删除策略只与磁盘以及具体的需求有关。另外,Kafka会为每一个Consumer Group保留一些metadata信息——当前消费的消息的position,也即offset。这个offset由Consumer控制。正常情况下Consumer会在消费完一条消息后递增该offset。当然,Consumer也可将offset设成一个较小的值,重新消费一些消息。因为offet由Consumer控制,所以Kafka broker是无状态的,它不需要标记哪些消息被哪些消费过,也不需要通过broker去保证同一个Consumer Group只有一个Consumer能消费某一条消息,因此也就不需要锁机制,这也为Kafka的高吞吐率提供了有力保障。

Producer消息路由

Producer发送消息到broker时,会根据Paritition机制选择将其存储到哪一个Partition。如果Partition机制设置合理,所有消息可以均匀分布到不同的Partition里,这样就实现了负载均衡。如果一个Topic对应一个文件,那这个文件所在的机器I/O将会成为这个Topic的性能瓶颈,而有了Partition后,不同的消息可以并行写入不同broker的不同Partition里,极大的提高了吞吐率。可以在$KAFKA_HOME/config/server.properties中通过配置项num.partitions来指定新建Topic的默认Partition数量,也可在创建Topic时通过参数指定,同时也可以在Topic创建之后通过Kafka提供的工具修改。

在发送一条消息时,可以指定这条消息的key,Producer根据这个key和Partition机制来判断应该将这条消息发送到哪个Parition。Paritition机制可以通过指定Producer的paritition. class这一参数来指定,该class必须实现kafka.producer.Partitioner接口。本例中如果key可以被解析为整数则将对应的整数与Partition总数取余,该消息会被发送到该数对应的Partition。(每个Parition都会有个序号,序号从0开始)

import kafka.producer.Partitioner;
import kafka.utils.VerifiableProperties;

public class JasonPartitioner<T> implements Partitioner {

    public JasonPartitioner(VerifiableProperties verifiableProperties) {}

    @Override
    public int partition(Object key, int numPartitions) {
        try {
            int partitionNum = Integer.parseInt((String) key);
            return Math.abs(Integer.parseInt((String) key) % numPartitions);
        } catch (Exception e) {
            return Math.abs(key.hashCode() % numPartitions);
        }
    }
}

如果将上例中的类作为partition.class,并通过如下代码发送20条消息(key分别为0,1,2,3)至topic3(包含4个Partition)。

public void sendMessage() throws InterruptedException{
  for(int i = 1; i <= 5; i++){
        List messageList = new ArrayList<KeyedMessage<String, String>>();
        for(int j = 0; j < 4; j++){
            messageList.add(new KeyedMessage<String, String>("topic2", j+"", "The " + i + " message for key " + j));
        }
        producer.send(messageList);
    }
  producer.close();
}

则key相同的消息会被发送并存储到同一个partition里,而且key的序号正好和Partition序号相同。(Partition序号从0开始,本例中的key也从0开始)。下图所示是通过Java程序调用Consumer后打印出的消息列表。

Consumer Group

(本节所有描述都是基于Consumer hight level API而非low level API)。

使用Consumer high level API时,同一Topic的一条消息只能被同一个Consumer Group内的一个Consumer消费,但多个Consumer Group可同时消费这一消息。

这是Kafka用来实现一个Topic消息的广播(发给所有的Consumer)和单播(发给某一个Consumer)的手段。一个Topic可以对应多个Consumer Group。如果需要实现广播,只要每个Consumer有一个独立的Group就可以了。要实现单播只要所有的Consumer在同一个Group里。用Consumer Group还可以将Consumer进行自由的分组而不需要多次发送消息到不同的Topic。

实际上,Kafka的设计理念之一就是同时提供离线处理和实时处理。根据这一特性,可以使用Storm这种实时流处理系统对消息进行实时在线处理,同时使用Hadoop这种批处理系统进行离线处理,还可以同时将数据实时备份到另一个数据中心,只需要保证这三个操作所使用的Consumer属于不同的Consumer Group即可。下图是Kafka在Linkedin的一种简化部署示意图。

下面这个例子更清晰地展示了Kafka Consumer Group的特性。首先创建一个Topic (名为topic1,包含3个Partition),然后创建一个属于group1的Consumer实例,并创建三个属于group2的Consumer实例,最后通过Producer向topic1发送key分别为1,2,3的消息。结果发现属于group1的Consumer收到了所有的这三条消息,同时group2中的3个Consumer分别收到了key为1,2,3的消息。如下图所示。

Push vs. Pull

作为一个消息系统,Kafka遵循了传统的方式,选择由Producer向broker push消息并由Consumer从broker pull消息。一些logging-centric system,比如Facebook的Scribe和Cloudera的Flume,采用push模式。事实上,push模式和pull模式各有优劣。

push模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的。push模式的目标是尽可能以最快速度传递消息,但是这样很容易造成Consumer来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而pull模式则可以根据Consumer的消费能力以适当的速率消费消息。

对于Kafka而言,pull模式更合适。pull模式可简化broker的设计,Consumer可自主控制消费消息的速率,同时Consumer可以自己控制消费方式——即可批量消费也可逐条消费,同时还能选择不同的提交方式从而实现不同的传输语义。

Kafka delivery guarantee

有这么几种可能的delivery guarantee:

  • At most once 消息可能会丢,但绝不会重复传输
  • At least one 消息绝不会丢,但可能会重复传输
  • Exactly once 每条消息肯定会被传输一次且仅传输一次,很多时候这是用户所想要的。

    当Producer向broker发送消息时,一旦这条消息被commit,因数replication的存在,它就不会丢。但是如果Producer发送数据给broker后,遇到网络问题而造成通信中断,那Producer就无法判断该条消息是否已经commit。虽然Kafka无法确定网络故障期间发生了什么,但是Producer可以生成一种类似于主键的东西,发生故障时幂等性的重试多次,这样就做到了Exactly once。截止到目前(Kafka 0.8.2版本,2015-03-04),这一Feature还并未实现,有希望在Kafka未来的版本中实现。(所以目前默认情况下一条消息从Producer到broker是确保了At least once,可通过设置Producer异步发送实现At most once)。

    接下来讨论的是消息从broker到Consumer的delivery guarantee语义。(仅针对Kafka consumer high level API)。Consumer在从broker读取消息后,可以选择commit,该操作会在Zookeeper中保存该Consumer在该Partition中读取的消息的offset。该Consumer下一次再读该Partition时会从下一条开始读取。如未commit,下一次读取的开始位置会跟上一次commit之后的开始位置相同。当然可以将Consumer设置为autocommit,即Consumer一旦读到数据立即自动commit。如果只讨论这一读取消息的过程,那Kafka是确保了Exactly once。但实际使用中应用程序并非在Consumer读取完数据就结束了,而是要进行进一步处理,而数据处理与commit的顺序在很大程度上决定了消息从broker和consumer的delivery guarantee semantic。

  • 读完消息先commit再处理消息。这种模式下,如果Consumer在commit后还没来得及处理消息就crash了,下次重新开始工作后就无法读到刚刚已提交而未处理的消息,这就对应于At most once

  • 读完消息先处理再commit。这种模式下,如果在处理完消息之后commit之前Consumer crash了,下次重新开始工作时还会处理刚刚未commit的消息,实际上该消息已经被处理过了。这就对应于At least once。在很多使用场景下,消息都有一个主键,所以消息的处理往往具有幂等性,即多次处理这一条消息跟只处理一次是等效的,那就可以认为是Exactly once。(笔者认为这种说法比较牵强,毕竟它不是Kafka本身提供的机制,主键本身也并不能完全保证操作的幂等性。而且实际上我们说delivery guarantee 语义是讨论被处理多少次,而非处理结果怎样,因为处理方式多种多样,我们不应该把处理过程的特性——如是否幂等性,当成Kafka本身的Feature)

  • 如果一定要做到Exactly once,就需要协调offset和实际操作的输出。精典的做法是引入两阶段提交。如果能让offset和操作输入存在同一个地方,会更简洁和通用。这种方式可能更好,因为许多输出系统可能不支持两阶段提交。比如,Consumer拿到数据后可能把数据放到HDFS,如果把最新的offset和数据本身一起写到HDFS,那就可以保证数据的输出和offset的更新要么都完成,要么都不完成,间接实现Exactly once。(目前就high level API而言,offset是存于Zookeeper中的,无法存于HDFS,而low level API的offset是由自己去维护的,可以将之存于HDFS中)

总之,Kafka默认保证At least once,并且允许通过设置Producer异步提交来实现At most once。而Exactly once要求与外部存储系统协作,幸运的是Kafka提供的offset可以非常直接非常容易得使用这种方式。

作者简介

郭俊(Jason),硕士,从事大数据平台研发工作,精通Kafka等分布式消息系统及Storm等流式处理系统。

联系方式:新浪微博: 郭俊_Jason 微信:habren 博客:http://www.jasongj.com

下篇预告

下一篇将深入讲解Kafka是如何做Replication和Leader Election的。在Kafka0.8以前的版本中,如果某个broker宕机,或者磁盘出现问题,则该broker上所有partition的数据都会丢失。而Kafka0.8以后加入了Replication机制,可以将每个Partition的数据备份多份,即使某些broker宕机也能保证系统的可用性和数据的完整性。


感谢郭蕾对本文的策划和审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。

10 Mar 09:31

Java开发者写SQL时常犯的10个错误

by zer0Black

我十分惊讶的发现,我最近的一篇文章——《Java开发者写SQL时常犯的10个错误》——最近在我的博客我的合作伙伴DZone上非常的受欢迎。(这篇博客)的流行程度说明了几件事:

  • SQL在专业的Java开发中多么重要。
  • 基本的SQL知识被忘掉(的情况)普遍存在。
  • 通过embracing SQL,你就能了解像 jOOQMyBatis这样的以SQL为中心的库正好反应了市场的需要。 令人惊喜的是有用户提到了我博客上贴的一篇“SLICK’s mailing list”,SLICK是Scala中的一个不以SQL为中心的数据库访问库,像LINQ(还有LINQ-to-SQL),它侧重语言整合,而不是SQL语句的产生。

无论如何,我之前仓促列出的常见错误还没列完。因此我为你另外准备了10个没那么常见的,但Java开发者在写SQL语句时同样爱犯的错误。

1、不用PreparedStatements

有意思的是,在JDBC出现了许多年后的今天,这个错误依然出现在博客、论坛和邮件列表中,即便要记住和理解它是一件很简单的事。开发者不使用PreparedStatements的原因可能有如下几个:

  • 他们对PreparedStatements不了解
  • 他们认为使用PreparedStatements太慢了
  • 他们认为写PreparedStatements太费力

来吧,我们来破除上面的谣言。96%的案例中,用PreparedStatement比静态声明语句更好。为什么呢?就是下面这些简单的原因:

  • 使用内联绑定值(inlining bind values)可以从源头上避免糟糕的语句引起语法错误。
  • 使用内联绑定值可以避免糟糕的语句引起的SQL注入漏洞。
  • 当插入更多“复杂的”数据类型(比如时间戳、二进制数据等等)时,可以避免边缘现象(edge-cases)。
  • 你若保持PreparedStatements的连接开启状态而不马上关闭,只要重新绑定新值就可以进行复用。
  • 你可以在更多复杂的数据库里使用adaptive cursor sharing——自适应游标共享(Oracle的说法)。这可以帮你在每次新设定绑定值时阻止SQL语句硬解析。

(译者注:硬解析的弊端。硬解析即整个SQL语句的执行需要完完全全的解析,生成执行计划。而硬解析,生成执行计划需要耗用CPU资源,以及SGA资源。在此不得不提的是对库缓存中 闩的使用。闩是锁的细化,可以理解为是一种轻量级的串行化设备。当进程申请到闩后,则这些闩用于保护共享内存的数在同一时刻不会被两个以上的进程修改。在 硬解析时,需要申请闩的使用,而闩的数量在有限的情况下需要等待。大量的闩的使用由此造成需要使用闩的进程排队越频繁,性能则逾低下)

某些特殊情况下你需要对值进行内联绑定,这是为了给基于成本的性能优化器提示该查询将要涉及的数据集。典型的情况是用“常量”判断:

  • DELETED = 1
  • STATUS = 42

而不应该用一个“变量”判断:

  • FIRST_NAME LIKE “Jon%”
  • AMOUNT > 19.95

要注意的是,现代数据库已经实现了绑定数据窥探(bind-variable peeking)。因此,默认情况下,你也可以为你所有的查询参数使用绑定值。在你写嵌入的JPQL或嵌入的SQL时,用JPA CriteriaQuery或者jOOQ这类高层次的API可以很容易也很清晰的帮你生成PreparedStatements语句并绑定值。

更多的背景资料:

解决方案:

默认情况下,总是使用PreparedStatements来代替静态声明语句,而永远不要在你的SQL语句嵌入内联绑定值。

2、返回太多列

这个错误发生的非常频繁,它不光会影响你的数据库执行计划,也会对你的Java应用造成不好的影响。让我们先看看对后者的影响:

对Java程序的不良影响:

如 果你为了满足不同DAO层之间的数据复用而select *或者默认的50个列,这样将会有大量的数据从数据库读入到JDBC结果集中,即使你不从结果集读取数据,它也被传递到了线路上并被JDBC驱动器加载到 了内存中。如果你知道你只需要2-3列数据的话,这就造成了严重的IO和内存的浪费。

这个(问题的严重性)都是显而易见的,要小心……

对数据库执行计划的不良影响:

这 些影响事实上可能比对Java应用的影响还要严重。当复杂的数据库要针对你的查询请求计算出最佳执行计划时,它会进行大量的SQL转换(SQL transformation )。还好,请求中的一部分可以被略去,因为它们对SQL连映射或过滤条件起不了什么作用。我最近写了一篇博客来讲述这个问题:元数据模式会对Oracle查询转换产生怎样的影响

现在,给你展示一个错误的例子。想一想有两个视图的复杂查询:

SELECT *
FROM  customer_view c
JOIN  order_view o
ON  c.cust_id = o.cust_id

每个关联了上述关联表引用的视图也可能再次关联其他表的数据,像 CUSTOMER_ADDRESS、ORDER_HISTORY、ORDER_SETTLEMENT等等。进行select * 映射时,你的数据库除了把所有连接表都加载进来以外别无选择,实际上,你唯一感兴趣的数据可能只有这些:

SELECT c.first_name, c.last_name, o.amount
FROM  customer_view c
JOIN  order_view o
ON  c.cust_id = o.cust_id

一个好的数据库会在转换你的SQL语句时自动移除那些不需要的连接,这样数据库就只需要较少的IO和内存消耗。

解决方案:

永远不要用select *(这样的查询)。也不要在执行不同请求时复用相同的映射。尽量尝试减少映射到你所真正需要的数据。

需要注意的是,想在对象-关系映射(ORMs)上达成这个目标有些难。

3、把JOIN当做了SELECT的子句

对于性能或SQL语句的正确性来说,这不算错。但是不管如何,SQL开发者应该意识到JOIN子句不是SELECT语句的一部分。SQL standard 1992 定义了表引用:

6.3 <table reference>

<table reference> ::=
<table name> [ [ AS ] <correlation name>
[ <left paren> <derived column list> <right paren> ] ]
| <derived table> [ AS ] <correlation name>
[ <left paren> <derived column list> <right paren> ]
| <joined table>

7.4 <from clause>

<from clause> ::=
FROM <table reference> [ { <comma> <table reference> }... ]

7.5 <joined table>

<joined table> ::=
<cross join>
| <qualified join>
| <left paren> <joined table> <right paren>

<cross join> ::=
<table reference> CROSS JOIN <table reference>

<qualified join> ::=
<table reference> [ NATURAL ] [ <join type> ] JOIN
<table reference> [ <join specification> ]

关联数据库是以表为中心的。许多的操作的某方面都是执行在物理表、连接表或派生表上的。为了有效的写出SQL语句,理解SELECT … FROM子句是以“,”分割表引用是非常重要的。

基于表引用(table references)的复杂性,一些数据库也接受其它类型的复杂的表引用(table references),像INSERT、UPDATE、DELETE、MERGE。看看Oracle实例手册,里面解释了如何创建可更新的视图。

解决方案:

一定要考虑到,一般说来,FROM子句也是一个表引用(table references)。如果你写了JOIN子句,要考虑这个JOIN子句是这个复杂的表引用的一部分:

SELECT c.first_name, c.last_name, o.amount
FROMcustomer_view c
JOIN order_view o
ON c.cust_id = o.cust_id

4、使用ANSI 92标准之前连接语法

我 们已经说清了表引用是怎么工作的(看上一节),因此我们应该达成共识,不论花费什么代价,都应该避免使用ANSI 92标准之前的语法。就执行计划而言,使用JOIN…ON子句或者WHERE子句来作连接谓语没有什么不同。但从可读性和可维护性的角度看,在过滤条 件判断和连接判断中用WHERE子句会陷入不可自拔的泥沼,看看这个简单的例子:

SELECT c.first_name, c.last_name, o.amount
FROM  customer_view c,
order_view o
WHERE  o.amount > 100
AND    c.cust_id = o.cust_id
AND    c.language = 'en'

你能找到join谓词么?如果我们加入数十张表呢?当你使用外连接专有语法的时候会变得更糟,就像Oracle的(+)语法里讲的一样。

解决方案:

一定要用ANSI 92标准的JOIN语句。不要把JOIN谓词放到WHERE子句中。用ANSI 92标准之前的JOIN语法没有半点好处。

5、使用LIKE判定时忘了ESCAPE

SQL standard 1992 指出like判定应该如下:

8.5 <like predicate>

<like predicate> ::=
<match value> [ NOT ] LIKE <pattern>
[ ESCAPE <escape character> ]

当允许用户对你的SQL查询进行参数输入时,就应该使用ESCAPE关键字。尽管数据中含有百分号(%)的情况很罕见,但下划线(_)还是很常见的:

SELECT *
FROM  t
WHERE  t.x LIKE 'some!_prefix%' ESCAPE '!'

解决方案:

使用LIKE判定时,也要使用合适的ESCAPE

6、认为 NOT (A IN (X, Y)) 和 IN (X, Y) 的布尔值相反

对于NULLs,这是一个举足轻重的细节!让我们看看 A IN (X, Y) 真正意思吧:

A IN (X, Y)
is the same as    A = ANY (X, Y)
is the same as    A = X OR A = Y

When at the same time, NOT (A IN (X, Y)) really means:

同样的,NOT (A IN (X, Y))的真正意思:

NOT (A IN (X, Y))
is the same as    A NOT IN (X, Y)
is the same as    A != ANY (X, Y)
is the same as    A != X AND A != Y

看起来和之前说的布尔值相反一样?其实不是。如果X或Y中任何一个为NULL,NOT IN 条件产生的结果将是UNKNOWN,但是IN条件可能依然会返回一个布尔值。

或者换种说话,当 A IN (X, Y) 结果为TRUE或FALSE时,NOT(A IN (X, Y)) 结果为依然UNKNOWN而不是FALSE或TRUE。注意了,如果IN条件的右边是一个子查询,结果依旧。

不信?你自己看SQL Fiddle 去。它说了如下查询给不出结果:

SELECT 1
WHERE    1 IN (NULL)
UNION ALL
SELECT 2
WHERE NOT(1 IN (NULL))

更多细节可以参考我的上一篇博客,上面写了在同区域内不兼容的一些SQL方言。

解决方案:

当涉及到可为NULL的列时,注意NOT IN条件。

7、认为NOT (A IS NULL)和A IS NOT NULL是一样的

没错,我们记得处理NULL值的时候,SQL实现了三值逻辑。这就是我们能用NULL条件来检测NULL值的原因。对么?没错。

但在NULL条件容易遗漏的情况下。要意识到下面这两个条件仅仅在行值表达式(row value expressions)为1的时候才相等:

NOT (A IS NULL)
is not the same as A IS NOT NULL

如果A是一个大于1的行值表达式(row value expressions),正确的表将按照如下方式转换:

  • 如果A的所有值为NUll,A IS NULL为TRUE
  • 如果A的所有值为NUll,NOT(A IS NULL) 为FALSE
  • 如果A的所有值都不是NUll,A IS NOT NULL 为TRUE
  • 如果A的所有值都不是NUll,NOT(A IS NOT NULL)  为FALSE

在我的上一篇博客可以了解到更多细节。

解决方案:

当使用行值表达式(row value expressions)时,要注意NULL条件不一定能达到预期的效果。

8、不用行值表达式

行值表达式是SQL一个很棒的特性。SQL是一个以表格为中心的语言,表格又是以行为中心。通过创建能在同等级或行类型进行比较的点对点行模型,行值表达式让你能更容易的描述复杂的判定条件。一个简单的例子是,同时请求客户的姓名

SELECT c.address
FROM  customer c,
WHERE (c.first_name, c.last_name) = (?, ?)

可以看出,就将每行的谓词左边和与之对应的右边比较这个语法而言,行值表达式的语法更加简洁。特别是在有许多独立条件通过AND连接的时候就特别有效。行值表达式允许你将相互联系的条件放在一起。对于有外键的JOIN表达式来说,它更有用:

SELECT c.first_name, c.last_name, a.street
FROM  customer c
JOIN  address a
ON  (c.id, c.tenant_id) = (a.id, a.tenant_id)

不幸的是,并不是所有数据库都支持行值表达式。但SQL标准已经在1992对行值表达式进行了定义,如果你使用他们,像Oracle或Postgres这些的复杂数据库可以使用它们计算出更好的执行计划。在Use The Index, Luke这个页面上有解析。

解决方案

不管干什么都可以使用行值表达式。它们会让你的SQL语句更加简洁高效。

9、不定义足够的限制条件(constraints

我又要再次引用Tom Kyte 和 Use The Index, Luke 了。对你的元数据使用限制条件不能更赞了。首先,限制条件可以帮你防止数据质变,光这一点就很有用。但对我来说更重要的是,限制条件可以帮助数据库进行SQL语句转换,数据库可以决定。

  • 哪些值是等价的
  • 哪些子句是冗余的
  • 哪些子句是无效的(例如,会返回空值的语句)

有些开发者可能认为限制条件会导致(数据库)变慢。但相反,除非你插入大量的数据,对于大型操作是你可以禁用限制条件,或用一个无限制条件的临时“载入表”,线下再把数据转移到真实的表中。

解决方案:

尽可能定义足够多的限制条件(constraints)。它们将帮你更好的执行数据库请求。

10、认为50ms是一个快的查询速度

NoSQL的炒作依然在继续,许多公司认为它们像Twitter或Facebook一样需要更快、扩展性更好的解决方案,想脱离ACID和关系模型横向扩展。有些可能会成功(比如Twitter或Facebook),而其他的也许会走入误区:

看这篇文章:https://twitter.com/codinghorror/status/347070841059692545

对于那些仍被迫(或坚持)使用关系型数据 库的公司,请不要自欺欺人的认为:“现在的关系型数据库很慢,其实它们是被天花乱坠的宣传弄快的”。实际上,它们真的很快,解析20Kb查询文档,计算 2000行执行计划,如此庞大的执行,所需时间小于1ms,如果你和数据管理员(DBA)继续优化调整数据库,就能得到最大限度的运行。

它们会变慢的原因有两种:一是你的应用滥用流行的ORM;二是ORM无法针对你复杂的查询逻辑产生快的SQL语句。遇到这种情况,你就要考虑选择像 JDBCjOOQ 或MyBatis这样的更贴近SQL核心,能更好的控制你的SQL语句的API。

因此,不要认为查询速度50ms是很快或者可以接受的。完全不是!如果你程序运行时间是这样的,请检查你的执行计划。这种潜在危险可能会在你执行更复杂的上下文或数据中爆发。

总结

SQL很有趣,同时在各种各样的方面也很微妙。正如我的关于10个错误的博客所展示的。跋山涉水也要掌握SQL是一件值得做的事。数据是你最有价值的资产。带着尊敬的心态对待你的数据才能写出更好的SQL语句。

可能感兴趣的文章

09 Mar 07:56

读懂Java中的Socket编程

by importnewzz

Socket,又称为套接字,Socket是计算机网络通信的基本的技术之一。如今大多数基于网络的软件,如浏览器,即时通讯工具甚至是P2P下载都是基于Socket实现的。本文会介绍一下基于TCP/IP的Socket编程,并且如何写一个客户端/服务器程序。

餐前甜点

Unix的输入输出(IO)系统遵循Open-Read-Write-Close这样的操作范本。当一个用户进程进行IO操作之前,它需要调用Open来指定并获取待操作文件或设备读取或写入的权限。一旦IO操作对象被打开,那么这个用户进程可以对这个对象进行一次或多次的读取或写入操作。Read操作用来从IO操作对象读取数据,并将数据传递给用户进程。Write操作用来将用户进程中的数据传递(写入)到IO操作对象。 当所有的Read和Write操作结束之后,用户进程需要调用Close来通知系统其完成对IO对象的使用。

在Unix开始支持进程间通信(InterProcess Communication,简称IPC)时,IPC的接口就设计得类似文件IO操作接口。在Unix中,一个进程会有一套可以进行读取写入的IO描述符。IO描述符可以是文件,设备或者是通信通道(socket套接字)。一个文件描述符由三部分组成:创建(打开socket),读取写入数据(接受和发送到socket)还有销毁(关闭socket)。

在Unix系统中,类BSD版本的IPC接口是作为TCP和UDP协议之上的一层进行实现的。消息的目的地使用socket地址来表示。一个socket地址是由网络地址和端口号组成的通信标识符。

进程间通信操作需要一对儿socket。进程间通信通过在一个进程中的一个socket与另一个进程中得另一个socket进行数据传输来完成。当一个消息执行发出后,这个消息在发送端的socket中处于排队状态,直到下层的网络协议将这些消息发送出去。当消息到达接收端的socket后,其也会处于排队状态,直到接收端的进程对这条消息进行了接收处理。

TCP和UDP通信

关于socket编程我们有两种通信协议可以进行选择。一种是数据报通信,另一种就是流通信。

数据报通信

数据报通信协议,就是我们常说的UDP(User Data Protocol 用户数据报协议)。UDP是一种无连接的协议,这就意味着我们每次发送数据报时,需要同时发送本机的socket描述符和接收端的socket描述符。因此,我们在每次通信时都需要发送额外的数据。

流通信

流通信协议,也叫做TCP(Transfer Control Protocol,传输控制协议)。和UDP不同,TCP是一种基于连接的协议。在使用流通信之前,我们必须在通信的一对儿socket之间建立连接。其中一个socket作为服务器进行监听连接请求。另一个则作为客户端进行连接请求。一旦两个socket建立好了连接,他们可以单向或双向进行数据传输。

读到这里,我们多少有这样的疑问,我们进行socket编程使用UDP还是TCP呢。选择基于何种协议的socket编程取决于你的具体的客户端-服务器端程序的应用场景。下面我们简单分析一下TCP和UDP协议的区别,或许可以帮助你更好地选择使用哪种。

在UDP中,每次发送数据报时,需要附带上本机的socket描述符和接收端的socket描述符。而由于TCP是基于连接的协议,在通信的socket对之间需要在通信之前建立连接,因此会有建立连接这一耗时存在于TCP协议的socket编程。

在UDP中,数据报数据在大小上有64KB的限制。而TCP中也不存在这样的限制。一旦TCP通信的socket对建立了连接,他们之间的通信就类似IO流,所有的数据会按照接受时的顺序读取。

UDP是一种不可靠的协议,发送的数据报不一定会按照其发送顺序被接收端的socket接受。然后TCP是一种可靠的协议。接收端收到的包的顺序和包在发送端的顺序是一致的。

简而言之,TCP适合于诸如远程登录(rlogin,telnet)和文件传输(FTP)这类的网络服务。因为这些需要传输的数据的大小不确定。而UDP相比TCP更加简单轻量一些。UDP用来实现实时性较高或者丢包不重要的一些服务。在局域网中UDP的丢包率都相对比较低。

Java中的socket编程

下面的部分我将通过一些示例讲解一下如何使用socket编写客户端和服务器端的程序。

注意:在接下来的示例中,我将使用基于TCP/IP协议的socket编程,因为这个协议远远比UDP/IP使用的要广泛。并且所有的socket相关的类都位于java.net包下,所以在我们进行socket编程时需要引入这个包。

客户端编写

开启Socket

如果在客户端,你需要写下如下的代码就可以打开一个socket。

String host = "127.0.0.1";
int port = 8919;
Socket client = new Socket(host, port);

上面代码中,host即客户端需要连接的机器,port就是服务器端用来监听请求的端口。在选择端口时,需要注意一点,就是0~1023这些端口都已经被系统预留了。这些端口为一些常用的服务所使用,比如邮件,FTP和HTTP。当你在编写服务器端的代码,选择端口时,请选择一个大于1023的端口。

写入数据

接下来就是写入请求数据,我们从客户端的socket对象中得到OutputStream对象,然后写入数据后。很类似文件IO的处理代码。

public class ClientSocket {
  public static void main(String args[]) {
        String host = "127.0.0.1";
        int port = 8919;
        try {
          Socket client = new Socket(host, port);
          Writer writer = new OutputStreamWriter(client.getOutputStream());
          writer.write("Hello From Client");
          writer.flush();
          writer.close();
          client.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
    }

}

关闭IO对象

类似文件IO,在读写数据完成后,我们需要对IO对象进行关闭,以确保资源的正确释放。

服务器端编写

打开服务器端的socket

int port = 8919;
ServerSocket server = new ServerSocket(port);
Socket socket = server.accept();

上面的代码创建了一个服务器端的socket,然后调用accept方法监听并获取客户端的请求socket。accept方法是一个阻塞方法,在服务器端与客户端之间建立联系之前会一直等待阻塞。

读取数据

通过上面得到的socket对象获取InputStream对象,然后安装文件IO一样读取数据即可。这里我们将内容打印出来。

public class ServerClient {
  public static void main(String[] args) {
        int port = 8919;
        try {
            ServerSocket server = new ServerSocket(port);
                Socket socket = server.accept();
            Reader reader = new InputStreamReader(socket.getInputStream());
            char chars[] = new char[1024];
            int len;
            StringBuilder builder = new StringBuilder();
            while ((len=reader.read(chars)) != -1) {
               builder.append(new String(chars, 0, len));
            }
            System.out.println("Receive from client message=: " + builder);
            reader.close();
            socket.close();
            server.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
  }
}

关闭IO对象

还是不能忘记的,最后需要正确地关闭IO对象,以确保资源的正确释放。

附注一个例子

这里我们增加一个例子,使用socket实现一个回声服务器,就是服务器会将客户端发送过来的数据传回给客户端。代码很简单。

import java.io.*;
import java.net.*;
public class EchoServer {
    public static void main(String args[]) {
        // declaration section:
        // declare a server socket and a client socket for the server
        // declare an input and an output stream
        ServerSocket echoServer = null;
        String line;
        DataInputStream is;
        PrintStream os;
        Socket clientSocket = null;
        // Try to open a server socket on port 9999
        // Note that we can't choose a port less than 1023 if we are not
        // privileged users (root)
        try {
           echoServer = new ServerSocket(9999);
        }
        catch (IOException e) {
           System.out.println(e);
        }
        // Create a socket object from the ServerSocket to listen and accept 
        // connections.
        // Open input and output streams
        try {
               clientSocket = echoServer.accept();
               is = new DataInputStream(clientSocket.getInputStream());
               os = new PrintStream(clientSocket.getOutputStream());
               // As long as we receive data, echo that data back to the client.
               while (true) {
                 line = is.readLine();
                 os.println(line);
               }
        } catch (IOException e) {
               System.out.println(e);
            }
        }
}

编译运行上面的代码,进行如下请求,就可以看到客户端请求携带的数据的内容。

15:00 $ curl http://127.0.0.1:9999/?111
GET /?111 HTTP/1.1
User-Agent: curl/7.37.1
Host: 127.0.0.1:9999
Accept: */*

总结

进行客户端-服务器端编程还是比较有趣的,同时在Java中进行socket编程要比其他语言(如C)要简单快速编写。

java.net这个包里面包含了很多强大灵活的类供开发者进行网络编程,在进行网络编程中,建议使用这个包下面的API。同时Sun.*这个包也包含了很多的网络编程相关的类,但是不建议使用这个包下面的API,因为这个包可能会改变,另外这个包不能保证在所有的平台都有包含。

可能感兴趣的文章

05 Mar 06:43

Memcache内部剖析

by 程序员学架构

本文主要对memcache内部Big-O、LRU算法、内存分配(Memory allocation)、一致性哈希(Consistent hashing)等进行了深入剖析,并举例生动形象描述了一致性哈希算法 阅读原文 »

The post Memcache内部剖析 appeared first on 头条 - 伯乐在线.

03 Mar 01:33

关于Pull Request的十个建议

by 李小兵

 

Pull Request是BitbucketGitHub等源代码托管系统为了方便开发者之间协作而提供的一个功能,它提供了一个用户友好的Web界面来帮助审查人员进行代码审查。开发人员可以通过GitHub发出Pull Requests要求请求他人将程序拉下来进行代码审查。一个好的Pull Request不仅仅只是代码的事情,还牵涉到代码审查者对代码的审查,所以开发者不仅要写出好的代码,还必须迎合审查者的审查工作,才能给使得自己贡献的代码顺利通过审查并合并到master分支。现对丹麦的程序员、软件架构师、独立顾问Mark Seemann在自己博客中发布的一篇题为《关于Pull Request的十个建议》的文章进行一个全面的整理,以供读者阅读和参考。具体内容如下:

1. 进行较小的Pull Request

一个小且集中的Pull Request会使得提交的代码更加容易通过审核。据Mark Seemann根据自己的经验透漏,对提交代码进行审查所花费的时间是随着代码大小呈指数增长,而非线性增长;Pull Request多大才合适与Pull Request做了什么相关,最好少于12个文件的改变。如果Pull Request非常大,审查者就需要安排连续、相对比较长的时间进行审查,就会造成审查过程的延迟。

2. 每个Pull Request只做一件事

就如软件设计模式中的单一责任原则所指:一个类只负责一个功能领域中的相应职责,因此一个Pull Request也应只关注一件事情。如果一次Pull Request做了多件事情的话,将会增加审查过程的延迟、审查被拒绝的可能性。

3. 注意代码行的字数,最好少于80个字

代码审查者会使用不同的审查工具来审查提交的代码,并且GitHub和Stash还提供了不同形式的视图,这样就使得代码审查者能通过不同视图非常方便来审查用户的提交。如果代码行比较宽的话,审查者就不能在一个屏幕或者半个屏幕中来阅读代码,不得不拖动滚动条来阅读代码。为了使得代码比较容易审查,每行代码最好少于80个字符。

4. 避免重新格式化代码

就算自己觉得应该改变当前代码的格式以适合自己的风格,但是请不要这么做。在源代码上,用户对每个字节的改变将会在不同的审查视图显示出来。有些审查者会选择忽略空格改变,但是有些审查者会审查这些代码,对这些格式化引起的代码审查属于不必要的审查。如果真需要解决空格问题的话,就需要在其他文件里移动代码、改变格式或者对代码做其他样式改变,并在Pull Request注释中给出相应的说明。

5. 确保代码能够编译通过

在提交Pull Request时,应该首先在自己的电脑上进行编译构建。在编译构建过程中,务必注意编译过程出现的警告,要把警告当作错误来对待,这些警告可能不会阻止编译,就有可能被忽略。然而,当用户Pull Request操作引起了很多编译警告的话,代码审查者就有可能拒绝对应的提交。

6. 确保提交的代码能够通过所有测试

即使问题代码已经做了自动化测试,但是在提交Pull Request时,也要务必保证针对代码的所有测试都必须通过。

7. 添加测试

为代码建立测试规则,即使出现问题的代码已经做过了自动化测试,最好也要为自己提交的代码也做下测试。

8.记录下自己提交的原因

利用文档对代码进行注释、对自己的代码直接进行注释、利用版本控制器提供的提交信息功能备注信息、利用Pull Request 管理系统(如GItHub或者Stash)添加自定义的Pull Request注释信息。

9. 编写符合编码规范的代码

按照代码正确性规则编写代码,并附上有效的代码注释、提交信息、Pull Request信息等。

10. 避免颠簸

不同审查者对Pull Request有可能具有不同的观点,这就会导致颠簸的情况,就需要用户移除冲突的提交和推送修改的分支,并备注有效的信息。


感谢郭蕾对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。

03 Mar 01:32

Pivotal开源其大数据处理的核心组件

by 李小兵

近日,Pivotal宣布将其大数据套件的三个核心组件开源,即基于内存的分布式NoSQL数据库GemFire、基于Hadoop架构的大规模并行SQL 分析处理引擎HAWQ、大规模并行处理分析数据库Greenplum。同时,商业版本仍将继续提供更高级功能和商业支持服务。Pivotal开源这三个核心组件最主要原因是受其成功的Cloud Foundry开源案例所启发。Cloud Foundry是VMware于2011年4月12日推出的业界第一个开源PaaS云平台,它支持多种框架、语言、运行时环境、云平台以及应用服务。同时,Pivotal还宣布其将加入开放数据平台(Open Data Platform),这是一个目前已有14个公司组成的组织,包括Hortonworks通用电器GE、IBM、威瑞森(Verizon)等,该组织主要提供Hadoop的企业版。

Pivotal的大数据套件主要包括Pivotal的企业级Hadoop发行版Pivotal HD、Greenplum、HAWQ、GemFire、开源分布式框架Spring XD、K/V数据库Redis、消息队列RabbitMQCloud Foundry上的大数据套件。其中Greenplum和GemFire主要用来处理结构化数据,其他产品都是用来处理非结构化数据。通过HAWQ能够将Greenplum与Hadoop分布式架构进行紧密地融合。整个套件将从大规模并行处理、内存计算以及Hadoop批处理三方面满足企业对大数据的需求。Pivotal的大数据套件的客户包括国内的中信银行中国铁路总公司以及国外的美国金融服务公司Zions Bancorporation印度尼西亚电信运营商巴克莱电信(Bakrie Telecom)印度国有铁路公司(Indian Railways
美国西南航空公司(Southwest Airlines)
Pivotal大数据套件架构如下图所示:

从Pivotal的官网得知,GemFire的重要特征包括支持基于内存的数据网格、支持ACID事务、高性能、低延迟、高可用性、高扩展性、能够使用多种语言实现数据管理、强大的数据应用功能、易于管理的分布式数据网格管理等。GemFire可用于企业级的数据缓存、弹性的内存计算、大规模的实时交易应用、弹性流数据处理等。作为世界规模最大的实时交易系统之一的中国铁路客户服务中心网站(12306.cn),于2012年6月选择GemFire分布式内存计算平台进行了改造,以解决尖峰高流量并发问题。

HAWQ支持事务处理,它能够将复杂的查询分割成简单的任何大小的处理单元,并分发到并行处理系统中。HAWQ具有高性能的架构、完全支持SQL标准、具有深度分析和机器学习能力、支持本地Hadoop文件格式等重要特征。

开源中国上发布的一篇题为《Greenplum高性能数据引擎探秘》的文章对Greenplum进行了详细介绍。文章指出Greenplum数据库是为新一代数据仓库和大规模分析处理而建立的软件解决方案,其最大的特点是不需要高端的硬件支持仍然可以支撑大规模的高性能数据仓库和商业智能查询。在数据仓库、商业智能的应用上,尤其海量数据的处理方面,Greenplum的性能极其优异。Greenplum的重要特征包括大规模并行处理的架构、超强的并行计算能力、高效的数据载入、具有灵活地存储和分析能力、高效的分析平台、能够无缝集成已有的分析功能栈、最佳的数据管理框架等。

Pivotal是由GE、EMC和VMware 联合组成的专注开源PaaS和大数据应用Cloud Foundry、Greenplum等业务的合资公司。Pivotal原来一直资助着Groovy/Grails项目和主导这些项目的开发,不过今年1月份,Pivotal官方宣布将不再资助Groovy/Grails项目,并重新把精力集中在Cloud Foundry上。


感谢郭蕾对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。

03 Mar 01:01

OpenCV进阶之路:一个简化的视频摘要程序

by Wennn

一、前言

视频摘要又称视频浓缩,是对视频内容的一个简单概括,先通过运动目标分析,提取运动目标,然后对各个目标的运动轨迹进行分析,将不同的目标拼接到一个共同的背景场景中,并将它们以某种方式进行组合。视频摘要在视频分析和基于内容的视频检索中扮演着重要角色。

视频摘要主要运用在对长时间的监控视频的压缩上,它可以将不同时刻场景内目标的运动显示在同一时刻,这样大量减少了整个场景事件的时间跨度。一般的视频摘要的步骤可以总结为:

视频读取→背景建模  → 前景提取→ 目标轨迹跟踪→ 目标的时序与空间规划 → 生成浓缩视频

但是本文并不讨论上面的这些主题,这里我只想通过一个简单的去除视频里非运动帧来实现一个简单的视频压缩的功能。视频摘要不是本文的主题,文章想通过做一个简单的视频摘要程序对OpenCV下面几个功能进行介绍:

1)OpenCV与XML数据通信

2)视频的读取与写入

3)如何在没有OpenCV的环境中运行编译好的程序

二、与XML的交互

很多程序都需要有一个配置文件,可以手动的去调整一些运行中的参数,xml文件格式就是我们常用到的一种配置文件格式。opencv中提供了一个处理xml的类用来与xml文件进行简单的数据存储与读取通信。但这个类的功能有限,如果需要更多的功能可以利用第三方的库,比如libxml等。

我们所设计的视频摘要程序,跟常规的视频摘要不同,这里只是通过删除一些无运动目标的帧来达到视频压缩的目的,所以我们的算法可以设计如下:

1,定义一个目标运动的兴趣区域,作为检测区域。
2,遍历指定目录下的所有视频文件,并逐一的进行视频处理。
3,针对视频的每一帧,在检测区域内运用帧差法检测前景移动。
4,如果检测区域内前景的面积超过区域面积的10%,则说明有运动物体,则此帧进行保留,写入压缩视频。否则,该帧直接舍弃。
5,所有视频处理结束,则程序终止。

么,我们需要一个配置文件,这个文件里需要保存下面几个内容:

1,检测区域的参数

2,视频文件的目录

3,视频文件的后缀格式

4,生存视频的保存目录

 

<?xml version="1.0"?> 
<opencv_storage> 
<roi>  3 460 1250 480</roi> 
<videoReadPath>D:\ExtractKeyImages\video\</videoReadPath> 
<videoSuffix>*.mp4</videoSuffix> 
<videoSavePath>../result.avi</videoSavePath> 
</opencv_storage>

注意所有的节点都保存在opencv_storage节点下。

在OpenCV中定义了一个叫FileStorage的类,提供了一些简单的打开与读取xml文件内容的操作。

我们先来看xml文件数据的读取:

1,用FileStorage的构造函数可以打开一个xml或yml文件,也可以用FileStorage::open()来打开一个数据文件。

FileStorage::FileStorage(); // 默认构造函数
FileStorage::FileStorage(const string& source, int flags, const string& encoding = string());

上面第二个构造函数中有三个参数。

第一个参数source指定读取文件的路径。

第二个参数flag指定操作的模式,可以设置为READ说明以只读的方式打开一个文件,或者设置为WRITE,这种情况下,如果文件不存在,则创建一个文件,如果文件已经存在,则会清空当前文件里的内容。还可以设置为APPEND用来打开一个存在的文件,并且可以在原来基础上写入。

第三个参数用来指定文件的编码格式,一般都为UTF-8。

而open成员函数的接口与第二个构造函数接口一致。

bool FileStorage::open(const string& filename, int flags, const string& encoding=string())

2,读取文件内的数据,FileStorage重载的操作符[],用来获得指定的节点内容。

FileNode FileStorage::operator[](const string& nodename) const
FileNode FileNode::operator[](const string& nodename) const

上面两个操作符都返回FileNode类型,它是一个子节点类型。

比如:我们想读取<book>结点下的<name>结点,则可以:

FileStorage fs("../config.xml", FileStorage::READ); 
string book_name; 
fs["book"] ["name"]>> book_name;

如果要取出A节点下的B结点下的C结点则为fs["A"]["B"]["C"]>>content;要记住所有节点都是在根结点opencv_storage下的,但是访问时忽略它。

而如果需要将数据写入,则简单的写入可以直接用<<运算符,比如增加一个节点为book,内容为theOpenCV:

string book_name=”theOpenCV”;
fs<<”book”<<book_name;

最后给出我们程序中读取配置参数的代码,我们需要4项配置项,上面已经介绍过了:

FileStorage fs("../config.xml", FileStorage::WRITE);
 
string videoPath; 
string videoSuffix; 
Rect roiRect; 
string imgSavePath;

fs["videoReadPath"] >> videoPath; 
fs["videoSuffix"] >> videoSuffix; 
fs["imgSavePath"] >> imgSavePath; 
fs["roi"] >> roiRect;

二、检测区域的运动检测

这里我们要进行简单的视频压缩就是想把完全静止不动的视频帧从原视频里删除,我们的兴趣目标一般是在移动的视频里。所以我们可以用帧差法来检测移动物体,它的原理是利用视频中物体的移动将引起相邻视频帧内容的不同,从而显示出移动的前景。

两帧之间的帧差图像可以这样定义:

其中imgCur代表当前帧的图像,imgPre代表前一帧图像

在得到帧差图像后,我们并不能得到很明显的判断条件,所以我们需要对帧差图像进行二值化,我们设置一个阈值T

然后我们只需遍历图像求出图像中所有白点的个数,即是运动前景的面积,计算一下面积比例即可以确定当前帧是否有物体移动。

当然我们得到的前景目标并不移动的物体的轮廓,而是与前一帧相比目标移动的部分。

下面为这一部分的OpenCV实现,相关的视频读取和写入的操作可以参考OpenCV成长之路中的相关文章。

// 查找文件目录下的所有视频文件 
vector<string> videoPathStr = FindAllFile((videoPath + videoSuffix).c_str(), true); 
// 先读取一个视频文件,用于获取相关的参数 
VideoCapture capture(videoPathStr[0]); 
// 视频大小 
Size videoSize(capture.get(CV_CAP_PROP_FRAME_WIDTH), capture.get(CV_CAP_PROP_FRAME_HEIGHT)); 
// 创建一个视频写入对象 
VideoWriter writer("../result.avi", CV_FOURCC('M', 'J', 'P', 'G'), 25.0, videoSize);

for (auto videoName : videoPathStr) 
{ 
capture.open(videoName); // 读入路径下的视频

Mat preFrame; 
bool stop(false);

double totleFrameNum = capture.get(CV_CAP_PROP_FRAME_COUNT); // 获取视频总帧数

for (int frameNum = 0; frameNum < totleFrameNum; frameNum++) 
{ 
Mat imgSrc; 
capture >> imgSrc; // 读一视频的一帧 
if (!imgSrc.data) 
break; 
Mat frame; 
cvtColor(imgSrc, frame, CV_BGR2GRAY); 
++frameNum; 
if (frameNum == 1) 
{ 
preFrame = frame; 
} 
Mat frameDif; 
absdiff(frame, preFrame, frameDif); // 帧差法 
preFrame = frame;

threshold(frameDif, frameDif, 30, 255, THRESH_BINARY); // 二值化

Mat imgRoi = frameDif(roiRect); 
double matArea = computeMatArea(imgRoi); // 计算区域面积

if (matArea / (imgRoi.rows*imgRoi.cols) > 0.1) // 面积比例大于10% 
{ 
writer << frameDif;// 写入视频 
} 
} 
} 
capture.release(); 
writer.release();

三、在没有OpenCV的环境下运行程序

这里是指基于windows系统下VS平台的程序,很多时候我们编译好的程序需要在别人的电脑上运行,而别人电脑上是没有OpenCV的基本库的,而我们的编译的opencv程序一般是动态链接一些dll的。

有两种方法:一种是拷贝用到的dll到release目录下,另一种是把相关的源文件加入工程中一起编译。

下面主要介绍第一种方法,因为看起来简单,很多人还是运行不了。

我们从openCV的环境配置开始说起:

首先,我们先找到我们下载并解压后的OpenCV目录下的这几个目录:

头文件目录:F:\EvProjects\OpenCV\OpenCV248\build\include

运行库目录:F:\EvProjects\OpenCV\OpenCV248\build\x86\vc12\lib

上面的vc12指定你的vs的版本,这里是vs2013

然后我们在我们新建的工程中找到属性管理器:

然后分别在DeBug和Release下配置属性表:

我们可以新建一个名字为opencv248_debug.props的属性表,以后新建的工程,直接拷贝添加即可。

然后右键配置opencv248_debug.props的属性,在VC目录下配置两项:

一项是包含目录,加入:F:\EvProjects\OpenCV\OpenCV248\build\include

第二项是在库目录下加入:F:\EvProjects\OpenCV\OpenCV248\build\x86\vc12\lib

最后我们需要在链接器->输入->附加依赖项中加入一些常用到的库文件

opencv_core248d.lib 
opencv_imgproc248d.lib 
opencv_highgui248d.lib 
opencv_ml248d.lib 
opencv_video248d.lib 
opencv_features2d248d.lib 
opencv_calib3d248d.lib 
opencv_objdetect248d.lib 
opencv_contrib248d.lib 
opencv_legacy248d.lib 
opencv_flann248d.lib

注意上面的248说明了我的opencv版本,你的可能是246或247。

也可以把F:\EvProjects\OpenCV\OpenCV248\build\x86\vc12\lib目录里的lib文件都加入,注意只加入带d的表示debug库。

这样的话debug下就配置完了,我们按相同方法,在release下配置一个属性表opencv248_release.props,与debug不同的是,在链接器的配置里加入的库名,都是不包含d的。

OK,属性表都配置好后,我们把当前的编译环境改为Release:

在解决方案里,右键项目名->属性->配置管理器

然后把活动解决方案配置改为release即可。

所有的环境配置好后,只需要编译好程序,然后在release下找到exe文件,这个就是我们的可执行文件,但是它不能单独运行,我们需要把它需要依赖的一些dll拷贝过来,dll在opencv的F:\EvProjects\OpenCV\OpenCV248\build\x86\vc12\bin目录下,如果你不确定你的程序里需要哪些库,你就把全部都拷贝过来。或者可以用一个依赖库查看软件查看你的程序所依赖的库,把对应的dll拷贝过来即可。

另外值得注意,如果是VS的较高版本,如VS2012,VS2013你还安装对应的运行库。

 

 

OpenCV进阶之路:一个简化的视频摘要程序,首发于博客 - 伯乐在线

02 Mar 09:37

怎样编写高质量的Java代码

by importnewzz

代码质量概述

怎样辨别一个项目代码写得好还是坏?优秀的代码和腐化的代码区别在哪里?怎么让自己写的代码既漂亮又有生命力?接下来将对代码质量的问题进行一些粗略的介绍。也请有过代码质量相关经验的朋友提出宝贵的意见。

代码质量所涉及的5个方面,编码标准、代码重复、代码覆盖率、依赖项分析、复杂度分析。这5方面很大程序上决定了一份代码的质量高低。我们分别来看一下这5方面:

编码标准:这个想必都很清楚,每个公司几乎都有一份编码规范,类命名、包命名、代码风格之类的东西都属于其中。

代码重复:顾名思义就是重复的代码,如果你的代码中有大量的重复代码,你就要考虑是否将重复的代码提取出来,封装成一个公共的方法或者组件。

代码覆盖率:测试代码能运行到的代码比率,你的代码经过了单元测试了吗?是不是每个方法都进行了测试,代码覆盖率是多少?这关系到你的代码的功能性和稳定性。

依赖项分析:你的代码依赖关系怎么样?耦合关系怎么样?是否有循环依赖?是否符合高内聚低耦合的原则?通过依赖项分析可以辨别一二。

复杂度分析:以前有人写的程序嵌套了10层 if else你信吗?圈复杂度之高,让人难以阅读。通过复杂度分析可以揪出这些代码,要相信越优秀的代码,越容易读懂。

上面解释了代码质量相关的5个方面,在实际开发环境中,已经有很多工具为我们解决以上5个方面的问题,下列5个Eclipse插件分别对这5个问题有很好的支持:

编码标准:CheckStyle  插件URL:http://eclipse-cs.sourceforge.net/update/

代码重复:PMD的CPD  插件URL:http://pmd.sourceforge.net/eclipse/

代码覆盖率:Eclemma 插件URL:http://update.eclemma.org

依赖项分析:JDepend 插件URL:http://andrei.gmxhome.de/eclipse/

复杂度分析:Eclipse Metric  插件URL:http://metrics.sourceforge.net/update

注:某些插件需要科学上网才能更新

编码标准(CheckStyle的使用)

在eclipse上安装好了CheckStyle插件后,我们来建一个类用它跑一下。这个类很简单,一个常见的用户实体,包含了id,用户名、密码、邮件等属性,并包含get set方法,一个标准的POJO。运行CheckStyle检查一下:

一个我们平时再普通不过的一个类,被checkstyle弄出这么多问题,情何以堪,我们来看看究竟是什么情况?

看一下这些警告信息:

line 1、,说缺少package-info.java文件。

line 2、,说第一句注释要以“.”结尾。

line 30、,缺少java doc注释。

line 35、,getId不是继承的方法,必须指定abstract,final或空。另外也缺少java doc注释。

这个类基本就这四类毛病,缺少package-info.java文件,这个文件是做什么的呢?他是用来描述包注释的类,有一定的特殊性,要想详细了解请百度。如果对你的项目没有太大的影响,可以忽略它。配置CheckStyle的方法我们等会再说。第一句注释要以“.”结尾,这看你的习惯,你确定需要这个,你就保留,不需要就忽略。缺少java doc,对于java类的属性来说,注释是必要的,所以这个要保留。不是继承的方法,需要加上final关键字,如果你有这个习惯,就保留,反之忽略。

我们这里只是建立了一个最简单的类用CheckStyle来检查,随着你的类代码越来越多,逻辑越来越复杂,CheckStyle能检查出来的毛病也越来越多。常见的CheckStyle错误有这些:

1.Type is missing a javadoc commentClass
缺少类型说明
2.“{” should be on the previous line
“{” 应该位于前一行
3.Methods is missing a javadoc comment
方法前面缺少javadoc注释
4.Expected @throws tag for “Exception”
在注释中希望有@throws的说明
5.“.” Is preceeded with whitespace “.”
前面不能有空格
6.“.” Is followed by whitespace“.”
后面不能有空格
7.“=” is not preceeded with whitespace
“=” 前面缺少空格
8.“=” is not followed with whitespace
“=” 后面缺少空格
9.“}” should be on the same line
“}” 应该与下条语句位于同一行
10.Unused @param tag for “unused”
没有参数“unused”,不需注释
11.Variable “CA” missing javadoc
变量“CA”缺少javadoc注释
12.Line longer than 80characters
行长度超过80
13.Line contains a tab character
行含有”tab” 字符
14.Redundant “Public” modifier
冗余的“public” modifier
15.Final modifier out of order with the JSL
suggestionFinal modifier的顺序错误
16.Avoid using the “.*” form of import
Import格式避免使用“.*”
17.Redundant import from the same package
从同一个包中Import内容
18.Unused import-java.util.list
Import进来的java.util.list没有被使用
19.Duplicate import to line 13
重复Import同一个内容
20.Import from illegal package
从非法包中 Import内容
21.“while” construct must use “{}”
“while” 语句缺少“{}”
22.Variable “sTest1” must be private and have accessor method
变量“sTest1”应该是private的,并且有调用它的方法
23.Variable “ABC” must match pattern “^[a-z][a-zA-Z0-9]*$”
变量“ABC”不符合命名规则“^[a-z][a-zA-Z0-9]*$”
24.“(” is followed by whitespace
“(” 后面不能有空格
25.“)” is proceeded by whitespace
“)” 前面不能有空格

可以看出CheckStyle检查出来的问题,大多是编码规则以及风格上的问题,这是编写高质量代码最基本的。值得注意的是,我们将一些优秀的开源代码用CheckStyle来检查也会检查出不少问题,这不能不说这些开源不优秀,而是每个公司组织有自己的编写规范度,这个度既可以减少程序员的工作量又可以让代码的可读性合格,但这个度不一样符合CheckStyle的完整标准。所以我们一般使用CheckStyle都不会用他的默认标准,而是通过配置,制定适合自己的编码规则。

自定义CheckStyle规则:

打开CheckStyle配置,新建一个配置,选择外部配置文件。在这之前最好导出一个eclipse自带的checkstyle配置文件(sun_checks.xml),然后重命名作为一个外部的配置导进去,这么做的目的是导入之后可以修改相应的配置,达到自定义配置的目的(因为eclipse自带的配置是加锁的,不能修改)。导入之后,点击右边的“Configure”进行编辑。

先去掉缺少package-info.java文件的提示

再将第一句注释要以“.”结尾这个规则去掉,双击“Style javadoc”,将窗口内“checkFirstSentence”勾选去掉。

对于实体类,属性有了注释,get set方法也不需要注释了,双击“Method javadoc”将allowMissingPropertyJavadoc勾选中。

“getId不是继承的方法,必须指定abstract,final或空”,如果你懒得在方法上加“final”,这条规则也可以去掉。

如果你不想每一个参数都加“final”,还需要把参数的final规则去掉:

另外还有一个错误“’id’ hides a field.”,原因是方法的参数和类里面定义的域重名了,但使用eclipse生成的get set方法都会这样,所以可以忽略此项。

至此我们再使用checkstyle检查一篇,发现仅剩下属性缺少注释这个警告。

对每个属性加上java doc注释,所有问题都清除了。以此类推,解决checkstyle问题的方法就是:1、按规则解决代码问题;2、如果觉得这个问题对你的项目质量影响不大,则可以忽略它。

代码重复(PMD的CPD的使用)

对于多人开发的项目,难以避免出现重复代码的问题,尽管我们尽量对共用的代码进行了封装,但随着需求的增加、人员技术水平差异、沟通不足等原因,重复代码会越来越多。这不仅严重影响代码质量,也无形中增加了代码量。

注:精简的程序和高复用度的代码是我们一直追求的目标。

PMD的CPD工具就是为检查重复代码而生的。右键项目—>PMD—->Find Suspect Cut and Paste,执行重复代码检查:

检查出来的重复代码,可以双击查看。然后根据业务逻辑以及代码特征,决定要不要做封装、怎么封装。

代码覆盖率(Eclemma的使用)

一份质量合格的代码,不仅包含功能程序本身也包含了对应的测试代码,Eclemma插件可以用来统计测试代码覆盖整体代码中的比率,以此来评估代码的功能性和稳定性。

使用Junit编写好测试用例之后,右键Coverage As—>Junit Test,运行测试用例,Eclemma会统计出相关的代码覆盖率:

根据这个结果,你可以看出自己编写的测试用例覆盖到了那些代码,而没有覆盖到的代码,则有可能成为代码质量的盲区。

依赖项分析(JDepend的使用)

随着程序业务逻辑的增加,代码的依赖关系也变的越来越复杂,JDepend插件可以统计包和类的依赖关系,分析出程序的稳定性、抽象性和是否存在循环依赖的问题。右键包—>Run JDepend Analysis:

看一下这几项指标:

CC(Number of Classes)

被分析package的具体和抽象类(和接口)的数量,用于衡量package的可扩展性。如果一个类中实现了其他类,如实现了监听类,则监听类的数目也记录在此。

AC(Abstract classes)

抽象类和接口的数量。

Ca(Afferent Couplings)

依赖于被分析package的其他package的数量,用于衡量pacakge的职责。即有多少包调用了它。

Ce(Efferent Couplings)

被分析package的类所依赖的其他package的数量,用于衡量package的独立性。即它调用了多少其他包。

A(Abstractness)

被分析package中的抽象类和接口与所在package所有类数量的比例,取值范围为0-1。

I(Instability)

I=Ce/(Ce+Ca),用于衡量package的不稳定性,取值范围为0-1。I=0表示最稳定,I=1表示最不稳定。即如果这个类不调用任何其他包,则它是最稳定的。

D(Distance)

被分析package和理想曲线A+I=1的垂直距离,用于衡量package在稳定性和抽象性之间的平衡。理想的package要么完全是抽象类和稳定(x=0,y=1),要么完全是具体类和不稳定(x=1,y=0)。取值范围为0-1,D=0表示完全符合理想标准,D=1表示package最大程度地偏离了理想标准。即你的包要么全是接口,不调用任何其他包(完全是抽象类和稳定),要么是具体类,不被任何其他包调用。

Cycle

循环依赖的数量。

有个这个报告我们就可以有针对性的对代码进行设计和重构

复杂度分析(Metrics的使用)

对于阅读代码的人来说,越简单的代码越好理解和维护,如果你的代码阅读起来很费劲或者你自己过段时间后再来看都看不懂,你就得想办法解决下代码的复杂度问题了。Metrics插件可以帮你做到这点。

首先在Java透视图下右键一个项目—->Properties,选择Metrics,勾选Enble Metrics。

然后Window—>Show View—->Other—->Metrics View

打开Metrics视图,点击右上角运行图标,即可得到复杂度分析的结果:

可以根据复杂度指标,对自己的程序进行优化。

小结

本文介绍了和java代码质量相关的5个方面问题,并介绍对应eclipse插件的用法和作用。在我们实际开发中,尽量根据自己公司和团队的情况来制定一些检查规则,来提高代码质量。并且在大多数情况下,会有两个检查环节,即本地检查和持续集成环境的检查,我们常用的Hudson就可以集成很多插件。

参考资料:

追求代码质量: 软件架构的代码质量
http://www.ibm.com/developerworks/cn/java/j-cq04256/ 
JDepend
http://www.clarkware.com/software/JDepend.html
PMD
http://pmd.sourceforge.net/
CheckStyle
http://sourceforge.net/projects/eclipse-cs/?source=directory
Eclemma
http://www.eclemma.org/
Metrics
http://metrics.codahale.com/

相关文章

02 Mar 09:37

追求代码质量(6): JUnit 4 与 TestNG 的对比

by importnewzz

经过长时间积极的开发之后,JUnit 4.0 于今年年初发布了。JUnit 框架的某些最有趣的更改 —— 特别是对于本专栏的读者来说 —— 正是通过巧妙地使用注释实现的。除外观和风格方面的显著改进外,新框架的特性使测试用例的编制从结构规则中解放出来。使原来僵化的 fixture 模型更为灵活,有利于采取可配置程度更高的方法。因此,JUnit 框架不再强求把每一项测试工作定义为一个名称以 test 开始的方法,并且现在可以只运行一次 fixture,而不是每次测试都需要运行一次。

虽然这些改变令人欣慰,但 JUnit 4 并不是第一个提供基于注释的灵活模型的 Java™ 测试框架。在修改 JUnit 之前很久,TestNG 就已建立为一个基于注释的框架。

事实上,是 TestNG 在 Java 编程中率先 实现了利用注释进行测试,这使它成为 JUnit 的有力竞争对手。然而,自从 JUnit 4 发布后,很多开发者质疑:二者之间还有什么差别吗?在本月的专栏中,我将讨论 TestNG 不同于 JUnit 4 的一些特性,并提议采用一些方法,使得这两个框架能继续互相补充,而不是互相竞争。

表面上的相似

JUnit 4 和 TestNG 有一些共同的重要特性。这两个框架都让测试工作简单得令人吃惊(和愉快),给测试工作带来了便利。二者也都拥有活跃的社区,为主动开发提供支持,同时生成丰富的文档。

两个框架的不同在于核心设计。JUnit 一直 是一个单元测试框架,也就是说,其构建目的是促进单个对象的测试,它确实能够极其有效地完成此类任务。而 TestNG 则是用来解决更高级别的测试问题,因此,它具有 JUnit 中所没有的一些特性。

一个简单的测试用例

初看起来,JUnit 4 和 TestNG 中实现的测试非常相似。为了更好地理解我的意思,请看一下清单 1 中的代码。这是一个 JUnit 4 测试,它有一个 macro-fixture(即仅在所有测试运行前调用一次的 fixture),这个 macro-fixture 由 @BeforeClass 属性表示:

清单 1. 一个简单的 JUnit 4 测试用例
package test.com.acme.dona.dep;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.junit.BeforeClass;
import org.junit.Test;

public class DependencyFinderTest {
 private static DependencyFinder finder;

 @BeforeClass
 public static void init() throws Exception {
  finder = new DependencyFinder();
 }

 @Test
 public void verifyDependencies() 
  throws Exception {
   String targetClss = 
     "test.com.acme.dona.dep.DependencyFind";

   Filter[] filtr = new Filter[] { 
      new RegexPackageFilter("java|junit|org")};

   Dependency[] deps = 
      finder.findDependencies(targetClss, filtr);

   assertNotNull("deps was null", deps);
   assertEquals("should be 5 large", 5, deps.length);	
 }
}

JUnit 用户会立即注意到:这个类中没有了以前版本的 JUnit 中所要求的一些语法成分。这个类没有 setUp() 方法,也不对 TestCase 类进行扩展,甚至也没有哪个方法的名称以 test 开始。这个类还利用了 Java 5 的一些特性,例如静态导入,很明显地,它还使用了注释。

更多的灵活性

在清单 2 中,您可以看到同一个 测试项目。不过这次是用 TestNG 实现的。这里的代码跟清单 1 中的测试代码有个微妙的差别。发现了吗?

清单 2. 一个 TestNG 测试用例
package test.com.acme.dona.dep;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Configuration;
import org.testng.annotations.Test;

public class DependencyFinderTest {
 private DependencyFinder finder;

 @BeforeClass
 private void init(){
  this.finder = new DependencyFinder();
 }

 @Test
 public void verifyDependencies() 
  throws Exception {
   String targetClss = 
     "test.com.acme.dona.dep.DependencyFind";

   Filter[] filtr = new Filter[] { 
      new RegexPackageFilter("java|junit|org")};

   Dependency[] deps = 
      finder.findDependencies(targetClss, filtr);
   
   assertNotNull(deps, "deps was null" );
   assertEquals(5, deps.length, "should be 5 large");		
 }
}

显然,这两个清单很相似。不过,如果仔细看,您会发现 TestNG 的编码规则比 JUnit 4 更灵活。清单 1 里,在 JUnit 中我必须把@BeforeClass 修饰的方法声明为 static,这又要求我把 fixture,即 finder 声明为 static。我还必须把 init() 声明为 public。看看清单 2,您就会发现不同。这里不再需要那些规则了。我的 init() 方法既不是 static,也不是 public

从最初起,TestNG 的灵活性就是其主要优势之一,但这并非它惟一的卖点。TestNG 还提供了 JUnit 4 所不具备的其他一些特性。

依赖性测试

JUnit 框架想达到的一个目标就是测试隔离。它的缺点是:人们很难确定测试用例执行的顺序,而这对于任何类型的依赖性测试都非常重要。开发者们使用了多种技术来解决这个问题,例如,按字母顺序指定测试用例,或是更多地依靠 fixture 来适当地解决问题。

如果测试成功,这些解决方法都没什么问题。但是,如果测试不成功,就会产生一个很麻烦的后果:所有 后续的依赖测试也会失败。在某些情况下,这会使大型测试套件报告出许多不必要的错误。例如,假设有一个测试套件测试一个需要登录的 Web 应用程序。您可以创建一个有依赖关系的方法,通过登录到这个应用程序来创建整个测试套件,从而避免 JUnit 的隔离机制。这种解决方法不错,但是如果登录失败,即使登录该应用程序后的其他功能都正常工作,整个测试套件依然会全部失败!

跳过,而不是标为失败

与 JUnit 不同,TestNG 利用 Test 注释的 dependsOnMethods 属性来应对测试的依赖性问题。有了这个便利的特性,就可以轻松指定依赖方法。例如,前面所说的登录将在某个方法之前 运行。此外,如果依赖方法失败,它将被跳过,而不是标记为失败。

清单 3. 使用 TestNG 进行依赖性测试
import net.sourceforge.jwebunit.WebTester;

public class AccountHistoryTest  {
 private WebTester tester;

 @BeforeClass
 protected void init() throws Exception {
  this.tester = new WebTester();
  this.tester.getTestContext().
   setBaseUrl("http://div.acme.com:8185/ceg/");
 }

 @Test
 public void verifyLogIn() {
  this.tester.beginAt("/");		
  this.tester.setFormElement("username", "admin");
  this.tester.setFormElement("password", "admin");
  this.tester.submit();		
  this.tester.assertTextPresent("Logged in as admin");
 }

 @Test (dependsOnMethods = {"verifyLogIn"})
 public void verifyAccountInfo() {
  this.tester.clickLinkWithText("History", 0);		
  this.tester.assertTextPresent("GTG Data Feed");
 }
}

在清单 3 中定义了两个测试:一个验证登录,另一个验证账户信息。请注意,通过使用 Test 注释的 dependsOnMethods = {"verifyLogIn"}子句,verifyAccountInfo 测试指定了它依赖 verifyLogIn() 方法。

通过 TestNG 的 Eclipse 插件(例如)运行该测试时,如果 verifyLogIn 测试失败,TestNG 将直接跳过 verifyAccountInfo 测试,请参见图 1:

图 1. 在 TestNG 中跳过的测试

对于大型测试套件,TestNG 这种不标记为失败,而只是跳过的处理方法可以减轻很多压力。您的团队可以集中精力查找为什么百分之五十的测试套件被跳过,而不是去找百分之五十的测试套件失败的原因!更有利的是,TestNG 采取了只重新运行失败测试的机制,这使它的依赖性测试设置更为完善。

失败和重运行

在大型测试套件中,这种重新运行失败测试的能力显得尤为方便。这是 TestNG 独有的一个特性。在 JUnit 4 中,如果测试套件包括 1000 项测试,其中 3 项失败,很可能就会迫使您重新运行整个测试套件(修改错误以后)。不用说,这样的工作可能会耗费几个小时。

一旦 TestNG 中出现失败,它就会创建一个 XML 配置文件,对失败的测试加以说明。如果利用这个文件执行 TestNG 运行程序,TestNG 就运行失败的测试。所以,在前面的例子里,您只需重新运行那三个失败的测试,而不是整个测试套件。

实际上,您可以通过清单 2 中的 Web 测试的例子自己看到这点。verifyLogIn() 方法失败时,TestNG 自动创建一个 testng-failed.xml 文件。该文件将成为如清单 4 所示的替代性测试套件:

清单 4. 失败测试的 XML 文件
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite thread-count="5" verbose="1" name="Failed suite [HistoryTesting]" 
       parallel="false" annotations="JDK5">
 <test name="test.com.acme.ceg.AccountHistoryTest(failed)" junit="false">
  <classes>
   <class name="test.com.acme.ceg.AccountHistoryTest">
    <methods>
     <include name="verifyLogIn"/>
    </methods>
   </class>
  </classes>
 </test>
</suite>

运行小的测试套件时,这个特性似乎没什么大不了。但是如果您的测试套件规模较大,您很快就会体会到它的好处。

参数化测试

TestNG 中另一个有趣的特性是参数化测试。在 JUnit 中,如果您想改变某个受测方法的参数组,就只能给每个 不同的参数组编写一个测试用例。多数情况下,这不会带来太多麻烦。然而,我们有时会碰到一些情况,对其中的业务逻辑,需要运行的测试数目变化范围很大。

在这样的情况下,使用 JUnit 的测试人员往往会转而使用 FIT 这样的框架,因为这样就可以用表格数据驱动测试。但是 TestNG 提供了开箱即用的类似特性。通过在 TestNG 的 XML 配置文件中放入参数化数据,就可以对不同的数据集重用同一个测试用例,甚至有可能会得到不同的结果。这种技术完美地避免了只能 假定一切正常的测试,或是没有对边界进行有效验证的情况。

在清单 5 中,我用 Java 1.4 定义了一个 TestNG 测试,该测试可接收两个参数:classname 和 size。这两个参数可以验证某个类的层次结构(也就是说,如果传入 java.util.Vector,则 HierarchyBuilder 所构建的 Hierarchy 的值将为 2 )。

清单 5. 一个 TestNG 参数化测试
package test.com.acme.da;

import com.acme.da.hierarchy.Hierarchy;
import com.acme.da.hierarchy.HierarchyBuilder;

public class HierarchyTest {
 /**
  * @testng.test
  * @testng.parameters value="class_name, size"
  */
 public void assertValues(String classname, int size) throws Exception{
  Hierarchy hier = HierarchyBuilder.buildHierarchy(classname);
  assert hier.getHierarchyClassNames().length == size: "didn't equal!";
 }
}

清单 5 列出了一个泛型测试,它可以采用不同的数据反复重用。请花点时间思考一下这个问题。如果有 10 个不同的参数组合需要在 JUnit 中测试,您只能写 10 个测试用例。每个测试用例完成的任务基本是相同的,只是受测方法的参数有所改变。但是,如果使用参数化测试,就可以只定义一个 测试用例,然后,(举例来说)把所需的参数模式加到 TestNG 的测试套件文件中。清单 6 中展示了这中方法:

清单 6. 一个 TestNG 参数化测试套件文件
<!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd">
<suite name="Deckt-10">
 <test name="Deckt-10-test">

  <parameter name="class_name" value="java.util.Vector"/>
  <parameter name="size" value="2"/> 	

  <classes>  		
   <class name="test.com.acme.da.HierarchyTest"/>
  </classes>
 </test>  
</suite>

清单 6 中的 TestNG 测试套件文件只对该测试定义了一个参数组(class_name 为 java.util.Vector,且 size 等于 2),但却具有无限的可能。这样做的一个额外的好处是:将测试数据移动到 XML 文件的无代码工件就意味着非程序员也可以指定数据。

高级参数化测试

尽管从一个 XML 文件中抽取数据会很方便,但偶尔会有些测试需要有复杂类型,这些类型无法用 String 或原语值来表示。TestNG 可以通过它的 @DataProvider 注释处理这样的情况。@DataProvider 注释可以方便地把复杂参数类型映射到某个测试方法。例如,清单 7 中的verifyHierarchy 测试中,我采用了重载的 buildHierarchy 方法,它可接收一个 Class 类型的数据, 它断言(asserting)Hierarchy 的getHierarchyClassNames() 方法应该返回一个适当的字符串数组:

清单 7. TestNG 中的 DataProvider 用法
package test.com.acme.da.ng;

import java.util.Vector;

import static org.testng.Assert.assertEquals;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import com.acme.da.hierarchy.Hierarchy;
import com.acme.da.hierarchy.HierarchyBuilder;

public class HierarchyTest {

 @DataProvider(name = "class-hierarchies")
 public Object[][] dataValues(){
  return new Object[][]{
   {Vector.class, new String[] {"java.util.AbstractList", 
     "java.util.AbstractCollection"}},
   {String.class, new String[] {}}
  };
 }

 @Test(dataProvider = "class-hierarchies")
 public void verifyHierarchy(Class clzz, String[] names) 
  throws Exception{
    Hierarchy hier = HierarchyBuilder.buildHierarchy(clzz);
    assertEquals(hier.getHierarchyClassNames(), names, 
	  "values were not equal");		
 }
}

dataValues() 方法通过一个多维数组提供与 verifyHierarchy 测试方法的参数值匹配的数据值。TestNG 遍历这些数据值,并根据数据值调用了两次 verifyHierarchy。在第一次调用时,Class 参数被设置为 Vector.class ,而 String 数组参数容纳 “java.util.AbstractList ” 和 “ java.util.AbstractCollection ” 这两个 String 类型的数据。这样挺方便吧?

为什么只选择其一?

我已经探讨了对我而言,TestNG 的一些独有优势,但是它还有其他几个特性是 JUnit 所不具备的。例如 TestNG 中使用了测试分组,它可以根据诸如运行时间这样的特征来对测试分类。也可在 Java 1.4 中通过 javadoc 风格的注释来使用它,如 清单 5 所示。

正如我在本文开头所说,JUnit 4 和 TestNG 在表面上是相似的。然而,设计 JUnit 的目的是为了分析代码单元,而 TestNG 的预期用途则针对高级测试。对于大型测试套件,我们不希望在某一项测试失败时就得重新运行数千项测试,TestNG 的灵活性在这里尤为有用。这两个框架都有自己的优势,您可以随意同时使用它们。

参考资料

学习

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文 。
  • TestNG 使 Java 单元测试轻而易举”(Filippo Diotalevi,developerWorks,2005 年 1 月):TestNG 不仅仅是性能强大、富于创新、易于扩充、使用灵活,它也展示了 Java 注释的一种有趣应用方式。
  • JUnit 4 抢先看”(Elliotte Rusty Harold,developerWorks,2005 年 9 月):软件测试人员 Elliotte Harold 兴趣盎然地试用了 JUnit 4 并详细介绍了如何在您的工作中使用这个新的框架。
  • 在 TestNG 中使用 JUnit 扩展(Andrew Glover,thediscoblog.com,2006 年 3 月):某个框架声称它是 JUnit 扩展并不表示它可以在 TestNG 中使用。
  • 使用 TestNG 的统计测试(Cedric Beust,beust.com,2006 年 2 月):使用 TestNG 进行高级测试,本文由该项目的创始人编写。
  • 重新运行失败的测试”(Andrew Glover,testearly.com,2006 年 4 月):关于如何在 TestNG 中重新运行失败测试的深入分析。
  • 追求代码质量:决心采用 FIT(Andrew Glover,developerWorks,2006 年 2 月):有利于加强商业客户和开发者沟通的集成测试框架。
  • JUnit 4 you(Fabiano Cruz,Fabiano Cruz’s Blog,2006 年 6 月):关于 JUnit 4 的生态系统支持的有趣内容。
  • TestNG 测试的代码覆盖(改善代码质量论坛, 2006 年 3 月):请参与关于在 TestNG 中集成代码覆盖工具的讨论。
  • 追求代码质量 系列 (Andrew Glover,developerWorks):查看本系列从代码度量到测试框架和重构的所有文章。
  • developerWorks:查阅数百篇关于 Java 编程各个方面的文章。

获得产品和技术

讨论

相关文章

02 Mar 07:33

Groovy模板引擎上(基础模板介绍)

by 树下偷懒的蚁

原文链接 作者:groovy团队  译者:树下偷懒的蚁

1.简介
Groovy支持多种方式动态的生成文本譬如:GStrings, printf(基于Java5),MarkupBuilder 。除此之外,模板框架则是非常适用基于静态模板生成文本的应用程序。

2.模板框架

在Groovy中,模板框架包含TemplateEngine抽象基类(引擎必须实现),Template接口(引擎生成的模板必须实现)。

Groovy包含的以下几种模板引擎:

  • SimpleTemplateEngine -基础模板引擎
  • GStringTemplateEngine -将模板作为可写的闭包 (适用于流操作)
  • XmlTemplateEngine -适用于输出XML格式的模板引擎
  • MarkupTemplateEngine – 非常完整优化的模板引擎

3. SimpleTemplateEngine

SimpleTemplateEngine允许在模板中使用类似JSP风格的代码(如下例),脚本和EL表达式。样例

import groovy.text.SimpleTemplateEngine

def text = 'Dear &quot;$firstname $lastname&quot;,\nSo nice to meet you in &lt;% print city %&gt;.\nSee you in ${month},\n${signed}'

def binding = [&quot;firstname&quot;:&quot;Sam&quot;, &quot;lastname&quot;:&quot;Pullara&quot;, &quot;city&quot;:&quot;San Francisco&quot;, &quot;month&quot;:&quot;December&quot;, &quot;signed&quot;:&quot;Groovy-Dev&quot;]

def engine = new SimpleTemplateEngine()
template = engine.createTemplate(text).make(binding)

def result = 'Dear &quot;Sam Pullara&quot;,\nSo nice to meet you in San Francisco.\nSee you in December,\nGroovy-Dev'

assert result == template.toString()

 

然而,通常在模板中混入业务逻辑不是良好的习惯。但是有时一些简单的逻辑是有用的。上述的例子中,我们可以修改一下:

$firstname

可以改为(假设模板已经import了capitalize)

${firstname.capitalize()}

或者这样

<% print city %>

改为:

<% print city == "New York" ? "The Big Apple" : city %>

3.1.高级应用说明

如果直接将模板嵌入到脚本中(如我们上面做的那样),必须小心反斜杠转义。模板中的字符串在传入到模板框架之前需要Groovy解析,而GString表达式以及脚本代码作为Groovy程序的一部分,必须要转义反斜杠。例如,想用引号把 The Big Apple引起来,可以这样做:

<% print city == "New York" ? "\\"The Big Apple\\"" : city %>

相似的,如果想新起一行,我们可以这样用:

\\n

“\n” 可以在静态模板文本中使用,也可以在外部模板文件中使用。同样,如果要显示反斜线本身,要用:

\\\\

在外部文件中:

\\\\

4.GStringTemplateEngine

使用GStringTemplateEngine的方法,和上述的例子有点类似(显示更多的参数)。首先,我们将模板存在文件中:

test.template

Dear "$firstname $lastname",
So nice to meet you in <% out << (city == "New York" ? "\\"The Big Apple\\"" : city) %>.
See you in ${month},
${signed}

注意:我们使用out替代print支持GStringTemplateEngine的流特性。因为我们将文件存储在单独的文件中,所以不需要转义反斜线。调用过程如下:

def f = new File('test.template')
engine = new GStringTemplateEngine()
template = engine.createTemplate(f).make(binding)
println template.toString()

输入结果如下:

Dear "Sam Pullara",
So nice to meet you in "The Big Apple".
See you in December,
Groovy-Dev

5. XmlTemplateEngine

XmlTemplateEngine适用于输入模板输出结果都是XML样式的场景。可以在模板的任意表达式中使用${expression} 和 $variable符号。同时也支持特殊的标签:<gsp:scriptlet> (用户插入代码片段) and <gsp:expression> (用于输入结果的代码片段).

注解和处理指令在处理的过程中会被移除,同时特殊的XML符号比如:<,>, " 和 '会被相应的XML符号转义。输出结果将按照标准的XML输出格式进行缩进。gsp:tags 定义的Xmlns命名空间会被移除但是其他的命名空间将会被保留(可能转换成XML树中等效的结果)。

正常情况下,模板原文件会保存在单独的文件中,但是下面的例子提供一个String类型的XML模板。

def binding = [firstname: 'Jochen', lastname: 'Theodorou', nickname: 'blackdrag', salutation: 'Dear']
def engine = new groovy.text.XmlTemplateEngine()
def text = '''\
 &lt;document xmlns:gsp='http://groovy.codehaus.org/2005/gsp' xmlns:foo='baz' type='letter'&gt;
 &lt;gsp:scriptlet&gt;def greeting = &quot;${salutation}est&quot;&lt;/gsp:scriptlet&gt;
 &lt;gsp:expression&gt;greeting&lt;/gsp:expression&gt;
 &lt;foo:to&gt;$firstname &quot;$nickname&quot; $lastname&lt;/foo:to&gt;
 How are you today?
 &lt;/document&gt;
'''
def template = engine.createTemplate(text).make(binding)
println template.toString()

输出结果如下:

<document type=’letter’> Dearest <foo:to xmlns:foo=’baz’> Jochen &quot;blackdrag&quot; Theodorou </foo:to> How are you today? </document>

6. The MarkupTemplateEngine

此模板引擎主要适用于生成XML风格类似的标记(XML, XHTML, HTML5, …),但是也可以用于生成任意文本。和传统的模板引擎不同的是,此模板引擎基于DSL。如下模板样例:

 

xmlDeclaration()
cars {
   cars.each {
       car(make: it.make, model: it.model)
   }
}

如果用下面的数据模型填充:

model = [cars: [new Car(make: 'Peugeot', model: '508'), new Car(make: 'Toyota', model: 'Prius')]]

渲染的结果:

<?xml version='1.0'?> <cars><car make='Peugeot' model='508'/><car make='Toyota' model='Prius'/></cars>

此模板引擎的主要特点:

  • 标记构建器风格的语法
  • 模板编译成字节代码
  • 渲染迅速
  • 可选择的模板数据类型校验
  • 包含
  • 国际化支持
  • 碎片化/布局

原创文章,转载请注明: 转载自并发编程网 – ifeve.com

本文链接地址: Groovy模板引擎上(基础模板介绍)

02 Mar 07:29

Guava 是个风火轮之基础工具(1)

by JamesPan

前言

Guava 是 Java 开发者的好朋友。虽然我在开发中使用 Guava 很长时间了,Guava API 的身影遍及我写的生产代码的每个角落,但是我用到的功能只是 Guava 的功能集中一个少的可怜的真子集,更别说我一直没有时间认真的去挖掘 Guava 的功能,没有时间去学习 Guava 的实现。直到最近,我开始阅读 Getting Started with Google Guava,感觉有必要将我学习和使用 Guava 的一些东西记录下来。

Joiner

我们经常需要将几个字符串,或者字符串数组、列表之类的东西,拼接成一个以指定符号分隔各个元素的字符串,比如把 [1, 2, 3] 拼接成 “1 2 3″。

在 Python 中我只需要简单的调用 str.join 函数,就可以了,就像这样。

' '.join(map(str, [1, 2, 3]))

到了 Java 中,如果你不知道 Guava 的存在,基本上就得手写循环去实现这个功能,代码瞬间变得丑陋起来。

Guava 为我们提供了一套优雅的 API,让我们能够轻而易举的完成字符串拼接这一简单任务。还是上面的例子,借助 Guava 的 Joiner 类,代码瞬间变得优雅起来。

Joiner.on(' ').join(1, 2, 3);

被拼接的对象集,可以是硬编码的少数几个对象,可以是实现了 Iterable 接口的集合,也可以是迭代器对象。

除了返回一个拼接过的字符串,Joiner 还可以在实现了 Appendable 接口的对象所维护的内容的末尾,追加字符串拼接的结果。

StringBuilder sb = new StringBuilder("result:");
Joiner.on(" ").appendTo(sb, 1, 2, 3);
System.out.println(sb);//result:1 2 3

Guava 对空指针有着严格的限制,如果传入的对象中包含空指针,Joiner 会直接抛出 NPE。与此同时,Joiner 提供了两个方法,让我们能够优雅的处理待拼接集合中的空指针。

如果我们希望忽略空指针,那么可以调用 skipNulls 方法,得到一个会跳过空指针的 Joiner 实例。如果希望将空指针变为某个指定的值,那么可以调用 useForNull 方法,指定用来替换空指针的字符串。

Joiner.on(' ').skipNulls().join(1, null, 3);//1 3
Joiner.on(' ').useForNull("None").join(1, null, 3);//1 None 3

需要注意的是,Joiner 实例是不可变的,skipNulls 和 useForNull 都不是在原实例上修改某个成员变量,而是生成一个新的 Joiner 实例。

Joiner.MapJoiner

MapJoiner 是 Joiner 的内部静态类,用于帮助将 Map 对象拼接成字符串。

Joiner.on("#").withKeyValueSeparator("=").join(ImmutableMap.of(1, 2, 3, 4));//1=2#3=4

withKeyValueSeparator 方法指定了键与值的分隔符,同时返回一个 MapJoiner 实例。有些家伙会往 Map 里插入键或值为空指针的键值对,如果我们要拼接这种 Map,千万记得要用 useForNull 对 MapJoiner 做保护,不然 NPE 妥妥的。

源码分析

源码来自 Guava 18.0。Joiner 类的源码约 450 行,其中大部分是注释、函数重载,常用手法是先实现一个包含完整功能的函数,然后通过各种封装,把不常用的功能隐藏起来,提供优雅简介的接口。这样子的好处显而易见,用户可以使用简单接口解决 80% 的问题,那些罕见而复杂的需求,交给全功能函数去支持。

初始化方法

由于构造函数被设置成了私有,Joiner 只能通过 Joiner#on 函数来初始化。最基础的 Joiner#on 接受一个字符串入参作为分隔符,而接受字符入参的 Joiner#on 方法是前者的重载,内部使用 String#valueOf 函数将字符变成字符串后调用前者完成初始化。或许这是一个利于字符串内存回收的优化。

追加拼接结果

整个 Joiner 类最核心的函数莫过于 <A extends Appendable> Joiner#appendTo(A, Iterator<?>),一切的字符串拼接操作,最后都会调用到这个函数。这就是所谓的全功能函数,其他的一切 appendTo 只不过是它的重载,一切的 join 不过是它和它的重载的封装。

public <A extends Appendable> A appendTo(A appendable, Iterator<?> parts) throws IOException {
  checkNotNull(appendable);
  if (parts.hasNext()) {
    appendable.append(toString(parts.next()));
    while (parts.hasNext()) {
      appendable.append(separator);
      appendable.append(toString(parts.next()));
    }
  }
  return appendable;
}

这段代码的第一个技巧是使用 if 和 while 来实现了比较优雅的分隔符拼接,避免了在末尾插入分隔符的尴尬;第二个技巧是使用了自定义的 toString 方法而不是 Object#toString 来将对象序列化成字符串,为后续的各种空指针保护开了方便之门。

注意到一个比较有意思的 appendTo 重载。

public final StringBuilder appendTo(StringBuilder builder, Iterator<?> parts) {
  try {
    appendTo((Appendable) builder, parts);
  } catch (IOException impossible) {
    throw new AssertionError(impossible);
  }
  return builder;
}

在 Appendable 接口中,append 方法是会抛出 IOException 的。然而 StringBuilder 虽然实现了 Appendable,但是它覆盖实现的 append 方法却是不抛出 IOException 的。于是就出现了明知不可能抛异常,却又不得不去捕获异常的尴尬。

这里的异常处理手法十分机智,异常变量命名为 impossible,我们一看就明白这里是不会抛出 IOException 的。但是如果 catch 块里面什么都不做又好像不合适,于是抛出一个 AssertionError,表示对于这里不抛异常的断言失败了。

另一个比较有意思的 appendTo 重载是关于可变长参数。

public final <A extends Appendable> A appendTo(
    A appendable, @Nullable Object first, @Nullable Object second, Object... rest)
        throws IOException {
  return appendTo(appendable, iterable(first, second, rest));
}

注意到这里的 iterable 方法,它把两个变量和一个数组变成了一个实现了 Iterable 接口的集合,手法精妙!

private static Iterable<Object> iterable(
    final Object first, final Object second, final Object[] rest) {
  checkNotNull(rest);
  return new AbstractList<Object>() {
    @Override public int size() {
      return rest.length + 2;
    }

    @Override public Object get(int index) {
      switch (index) {
        case 0:
          return first;
        case 1:
          return second;
        default:
          return rest[index - 2];
      }
    }
  };
}

如果是我来实现,可能是简单粗暴的创建一个 ArrayList 的实例,然后把这两个变量一个数组的全部元素放到 ArrayList 里面然后返回。这样子代码虽然短了,但是代价却不小:为了一个小小的重载调用而产生了 O(n) 的时空复杂度。

看看人家 G 社的做法。要想写出这样的代码,需要熟悉顺序表迭代器的实现。迭代器内部维护着一个游标,cursor。迭代器的两大关键操作,hasNext 判断是否还有没遍历的元素,next 获取下一个元素,它们的实现是这样的。

public boolean hasNext() {
    return cursor != size();
}

public E next() {
    checkForComodification();
    try {
        int i = cursor;
        E next = get(i);
        lastRet = i;
        cursor = i + 1;
        return next;
    } catch (IndexOutOfBoundsException e) {
        checkForComodification();
        throw new NoSuchElementException();
    }
}

hasNext 中关键的函数调用是 size,获取集合的大小。next 方法中关键的函数调用是 get,获取第 i 个元素。Guava 的实现返回了一个被覆盖了 size 和 get 方法的 AbstractList,巧妙的复用了由编译器生成的数组,避免了新建列表和增加元素的开销。

空指针处理

当待拼接列表中可能包含空指针时,我们用 useForNull 将空指针替换为我们指定的字符串。它是通过返回一个覆盖了方法的 Joiner 实例来实现的。

  public Joiner useForNull(final String nullText) {
    checkNotNull(nullText);
    return new Joiner(this) {
      @Override CharSequence toString(@Nullable Object part) {
        return (part == null) ? nullText : Joiner.this.toString(part);
      }

      @Override public Joiner useForNull(String nullText) {
        throw new UnsupportedOperationException("already specified useForNull");
      }

      @Override public Joiner skipNulls() {
        throw new UnsupportedOperationException("already specified useForNull");
      }
    };
  }

首先是使用复制构造函数保留先前初始化时候设置的分隔符,然后覆盖了之前提到的 toString 方法。为了防止重复调用 useForNull 和 skipNulls,还特意覆盖了这两个方法,一旦调用就抛出运行时异常。为什么不能重复调用 useForNull ?因为覆盖了 toString 方法,而覆盖实现中需要调用覆盖前的 toString。

在不支持的操作中抛出 UnsupportedOperationException 是 Guava 的常见做法,可以在第一时间纠正不科学的调用方式。

skipNulls 的实现就相对要复杂一些,覆盖了原先全功能 appendTo 中使用 if 和 while 的优雅实现,变成了 2 个 while 先后执行。第一个 while 找到 第一个不为空指针的元素,起到之前的 if 的功能,第二个 while 功能和之前的一致。

public Joiner skipNulls() {
  return new Joiner(this) {
    @Override public <A extends Appendable> A appendTo(A appendable, Iterator<?> parts)
        throws IOException {
      checkNotNull(appendable, "appendable");
      checkNotNull(parts, "parts");
      while (parts.hasNext()) {
        Object part = parts.next();
        if (part != null) {
          appendable.append(Joiner.this.toString(part));
          break;
        }
      }
      while (parts.hasNext()) {
        Object part = parts.next();
        if (part != null) {
          appendable.append(separator);
          appendable.append(Joiner.this.toString(part));
        }
      }
      return appendable;
    }

    @Override public Joiner useForNull(String nullText) {
      throw new UnsupportedOperationException("already specified skipNulls");
    }

    @Override public MapJoiner withKeyValueSeparator(String kvs) {
      throw new UnsupportedOperationException("can't use .skipNulls() with maps");
    }
  };
}

拼接键值对

MapJoiner 实现为 Joiner 的一个静态内部类,它的构造函数和 Joiner 一样也是私有,只能通过 Joiner#withKeyValueSeparator 来生成实例。类似地,MapJoiner 也实现了 appendTo 方法和一系列的重载,还用 join 方法对 appendTo 做了封装。MapJoiner 整个实现和 Joiner 大同小异,在实现中大量 Joiner 的 toString 方法来保证空指针保护行为和初始化时的语义一致。

MapJoiner 也实现了一个 useForNull 方法,这样的好处是,在获取 MapJoiner 之后再去设置空指针保护,和获取 MapJoiner 之前就设置空指针保护,是等价的,用户无需去关心顺序问题。

原创文章,转载请注明: 转载自并发编程网 – ifeve.com

本文链接地址: Guava 是个风火轮之基础工具(1)

02 Mar 03:22

文章: 细数硅谷最火的高科技创业公司

by 董飞

在硅谷大家非常热情的谈创业谈机会,我也通过自己的一些观察和积累,看到了不少最近几年涌现的热门创业公司。我给大家一个列表,这个是华尔街网站的全世界创业公司融资规模评选(http://graphics.wsj.com/billion-dollar-club/)。它本来的标题是billion startup club,我在去年国内讲座也分享过,不到一年的时间,截至到2015年1月17日,现在的排名和规模已经发生了很大的变化。首先估值在10Billlon的达到了7家,而一年前一家都没有。第二第一名是中国人家喻户晓的小米,第三,前20名中,绝大多数(8成在美国,在加州,在硅谷,在旧金山!)比如Uber, Airbnb, Dropbox, Pinterest. 第四 里面也有不少相似模式成功的,比如Flipkart就是印度市场的淘宝,Uber与Airbnb都是共享经济的范畴。所以大家还是可以在移动(Uber),大数据(Palantir),消费级互联网,通讯(Snapchat),支付(Square),O2O App里面寻找下大机会。这里面很多公司我都亲自面试和感受过他们的环境。

有如此之多的高估值公司,是否意味着存在很大的泡沫?

看了那么多高估值公司,很多人都觉得非常疯狂,这是不是很大泡沫了,泡沫是不是要破了,是很多人的疑问。我认为在硅谷这个充满梦想的地方,投资人鼓励创业者大胆去做同样也助长了泡沫,很多项目在几个月的时间就会估值翻2,3倍,如Uber,Snapchat上我也惊讶于他们的巨额融资规模。那么这张图就是讲“新兴技术炒作”周期,把各类技术按照技术成熟度和期望值分类,这是硅谷创业孵化器YCombinator 课程How to start a startup(How to Start a Startup)提到。创新萌芽Innovation Trigger”、“期望最顶点Peak ofInflated Expectation”、“下调预期至低点Trough of Disillusion”、“回归理想Slope ofEnlightenment”、“生产率平台Plateau of Productivity”,越往左,技术约新潮,越处于概念阶段;越往右,技术约成熟,约进入商业化应用,发挥出提高生产率的效果。纵轴代表预期值,人们对于新技术通常会随着认识的深入,预期不断升温,伴之以媒体炒作而到达顶峰;随之因技术瓶颈或其他原因,预期逐渐冷却至低点,但技术技术成熟后,期望又重新上升,重新积累用户,然后就到了可持续增长的健康轨道上来。今年和去年的图对比显示,物联网、自动驾驶汽车、消费级3D打印、自然语言问答等概念正在处于炒作的顶峰。而大数据已从顶峰滑落,NFC和云计算接近谷底。

未来,高科技创业的趋势是什么?

我先提一个最近看的一部电影《Imitation Game》,讲计算机逻辑的奠基者艾伦图灵(计算机届最高奖以他命名)艰难的一生,他当年为破译德军密码制作了图灵机为二战胜利作出卓越贡献,挽回几千万人的生命,可在那个时代因为同性恋被判化学阉割,自杀结束了短暂的42岁生命。他的一个伟大贡献就是在人工智能的开拓工作,他提出图灵测试(Turing Test),测试某机器是否能表现出与人等价或无法区分的智能。我们现在回到今天,人工智能已经有了很大进步,从专家系统到基于统计的学习,从支持向量机到神经网络深度学习,每一步都带领机器智能走向下一个阶梯。在Google资深科学家吴军博士(数学之美,浪潮之巅作者),他提出当前技术发展三个趋势,第一,云计算和和移动互联网,这是正在进行时;第二,机器智能,现在开始发生,但对社会的影响很多人还没有意识到;第三,大数据和机器智能结合,这是未来时,一定会发生,有公司在做,但还没有太形成规模。他认为未来机器会控制98%的人,而现在我们就要做个选择,怎么成为剩下的2%?【独家】吴军:未来机器将会控制98%的人 李开复在2015年新年展望也提出未来五年物联网带来庞大创业机会。

为什么大数据和机器智能结合的未来一定会到来?

其实在工业革命之前(1820年),世界人均GDP在1800年前的两三千年里基本没有变化,而从1820年到2001年的180年里,世界人均GDP从原来的667美元增长到6049美元。由此足见,工业革命带来的收入增长的确是翻天覆地的。这里面发生了什么,大家可以去思考一下。但人类的进步并没有停止或者说稳步增长,在发明了电力,电脑,互联网,移动互联网,全球年GDP增长 从万分之5到2%,信息也是在急剧增长,根据计算,最近两年的信息量是之前30年的总和,最近10年是远超人类所有之前累计信息量之和。在计算机时代,有个著名的摩尔定律,就是说同样成本每隔18个月晶体管数量会翻倍,反过来同样数量晶体管成本会减半,这个规律已经很好的match了最近30年的发展,并且可以衍生到很多类似的领域:存储,功耗,带宽,像素。而下面这个是冯诺伊曼,20世纪最重要的数学家之一,在现代计算机、博弈论和核武器等诸多领域内有杰出建树的最伟大的科学全才之一。他提出(技术)将会逼近人类历史上的某种本质的奇点,在那之后 全部人类行为都不可能以我们熟悉的面貌继续存在。这就是著名的奇点理论。目前会越来越快指数性增长,美国未来学家Ray Kurzweil称人类能够在2045年实现数字化永生,他自己也创办奇点大学,相信随着信息技术、无线网、生物、物理等领域的指数级增长,将在2029年实现人工智能,人的寿命也将会在未来15年得到大幅延长。

国外值得关注的大数据公司都有哪些?国内又有哪些?

这是2014年总结的Big Data公司列表,我们大致可以分成基础架构和应用,而底层都是会用到一些通用技术,如Hadoop,Mahout,HBase,Cassandra,我在下面也会涵盖。我可以举几个例子,在分析这一块,cloudera,hortonworks,mapr作为Hadoop的三剑客,一些运维领域,mangodb,couchbase都是nosql的代表,作为服务领域AWS和Google BigQuery剑拔弩张,在传统数据库,Oracle收购了MySQL,DB2老牌银行专用,Teradata做了多年数据仓库。上面的Apps更多,比如社交消费领域Google, Amazon, Netflix, Twitter, 商业智能:SAP,GoodData,一些在广告媒体领域:TURN,Rocketfuel,做智能运维sumologic等等。去年的新星 Databricks 伴随着Spark的浪潮震撼Hadoop的生态系统。

对于迅速成长的中国市场,大公司也意味着大数据,BAT三家都是对大数据的投入也是不惜余力,我5年前在Baidu的的时候,就提出框计算的东东,最近两年成立了硅谷研究院,挖来Andrew Ng作为首席科学家,研究项目就是百度大脑,在语音,图片识别大幅提高精确度和召回率,最近还做了个无人自行车非常有趣。腾讯作为最大的社交应用对大数据也是情有独钟,自己研发了C++平台的海量存储系统。淘宝去年双十一主战场,2分钟突破10亿,交易额突破571亿,背后是有很多故事,当年在百度做Pyramid(按Google三辆马车打造的金字塔三层分布式系统)有志之士,继续在OceanBase创造神话。而阿里云当年备受争议,马云也怀疑是不是被王坚忽悠,最后经历了双十一的洗礼证明了OceanBase和阿里云的靠谱。小米的雷军对大数据也是寄托厚望,一方面这么多数据几何级数增长,另一方面存储带宽都是巨大成本,没价值就真破产。

与大数据技术关系最紧密的就是云计算,您曾在Amazon 云计算部门工作过,能否简单介绍一下亚马逊的Redshift框架吗?

本人在Amazon 云计算部门工作过,所以还是比较了解AWS,总体上成熟度很高,有大量startup都是基于上面开发,比如有名的Netflix,Pinterest,Coursera。Amazon还是不断创新,每年召开reInvent大会推广新的云产品和分享成功案例,在这里面我随便说几个,像S3是简单面向对象的存储,DynamoDB是对关系型数据库的补充,Glacier对冷数据做归档处理,Elastic MapReduce直接对MapReduce做打包提供计算服务,EC2就是基础的虚拟主机,Data Pipeline 会提供图形化界面直接串联工作任务。

Redshift,它是一种(massively parallel computer)架构,是非常方便的数据仓库解决方案,就是SQL接口,跟各个云服务无缝连接,最大特点就是快,在TB到PB级别非常好的性能,我在工作中也是直接使用,它还支持不同的硬件平台,如果想速度更快,可以使用SSD的,当然支持容量就小些。

Hadoop是现今最流行的大数据技术,在它出现的当时,是什么造成了Hadoop的流行?当时Hadoop具有哪些设计上的优势?

看Hadoop从哪里开始的,不得不提Google的先进性,在10多年前,Google出了3篇paper论述分布式系统的做法,分别是GFS, MapReduce, BigTable,非常NB的系统,但没人见过,在工业界很多人痒痒的就想按其思想去仿作,当时Apache Nutch Lucene的作者Doug Cutting也是其中之一,后来他们被Yahoo收购,专门成立Team去投入做,就是Hadoop的开始和大规模发展的地方,之后随着Yahoo的衰落,牛人去了Facebook, Google, 也有成立了Cloudera, Hortonworks等大数据公司,把Hadoop的实践带到各个硅谷公司。而Google还没有停止,又出了新的三辆马车,Pregel, Caffeine, Dremel, 后来又有很多步入后尘,开始新一轮开源大战。

为啥Hadoop就比较适合做大数据呢?首先扩展很好,直接通过加节点就可以把系统能力提高,它有个重要思想是移动计算而不是移动数据,因为数据的移动是很大的成本需要网络带宽。其次它提出的目标就是利用廉价的普通计算机(硬盘),这样虽然可能不稳定(磁盘坏的几率),但通过系统级别上的容错和冗余达到高可靠性。并且非常灵活,可以使用各种data,二进制,文档型,记录型。使用各种形式(结构化,半结构化,非结构化所谓的schemaless),在按需计算上也是个技巧。

围绕在Hadoop周围的有哪些公司和产品?

提到Hadoop一般不会说某一个东西,而是指生态系统,在这里面太多交互的组件了,涉及到IO,处理,应用,配置,工作流。在真正的工作中,当几个组件互相影响,你头疼的维护才刚刚开始。我也简单说几个:Hadoop Core就三个HDFS,MapReduce,Common,在外围有NoSQL: Cassandra, HBase, 有Facebook开发的数据仓库Hive,有Yahoo主力研发的Pig工作流语言,有机器学习算法库Mahout,工作流管理软件Oozie,在很多分布式系统选择Master中扮演重要角色的Zookeeper。

能否用普通人都能理解的方式解释一下Hadoop的工作原理?

我们先说HDFS,所谓Hadoop的分布式文件系统,它是能真正做到高强度容错。并且根据locality原理,对连续存储做了优化。简单说就是分配大的数据块,每次连续读整数个。如果让你自己来设计分布式文件系统,在某机器挂掉还能正常访问该怎么做?首先需要有个master作为目录查找(就是Namenode),那么数据节点是作为分割好一块块的,同一块数据为了做备份不能放到同一个机器上,否则这台机器挂了,你备份也同样没办法找到。HDFS用一种机架位感知的办法,先把一份拷贝放入同机架上的机器,然后在拷贝一份到其他服务器,也许是不同数据中心的,这样如果某个数据点坏了,就从另一个机架上调用,而同一个机架它们内网连接是非常快的,如果那个机器也坏了,只能从远程去获取。这是一种办法,现在还有基于erasure code本来是用在通信容错领域的办法,可以节约空间又达到容错的目的,大家感兴趣可以去查询。

接着说MapReduce,首先是个编程范式,它的思想是对批量处理的任务,分成两个阶段,所谓的Map阶段就是把数据生成key, value pair, 再排序,中间有一步叫shuffle,把同样的key运输到同一个reducer上面去,而在reducer上,因为同样key已经确保在同一个上,就直接可以做聚合,算出一些sum, 最后把结果输出到HDFS上。对应开发者来说,你需要做的就是编写Map和reduce函数,像中间的排序和shuffle网络传输,容错处理,框架已经帮你做好了。

MapReduce模型有什么问题?

第一:需要写很多底层的代码不够高效,第二:所有的事情必须要转化成两个操作Map/Reduce,这本身就很奇怪,也不能解决所有的情况。

Spark从何而来?Spark相比于Hadoop MapReduce设计上有什么样的优势?

其实Spark出现就是为了解决上面的问题。先说一些Spark的起源。发自 2010年Berkeley AMPLab,发表在hotcloud 是一个从学术界到工业界的成功典范,也吸引了顶级VC:Andreessen Horowitz的 注资. 在2013年,这些大牛(Berkeley系主任,MIT最年轻的助理教授)从Berkeley AMPLab出去成立了Databricks,引无数Hadoop大佬尽折腰,它是用函数式语言Scala编写,Spark简单说就是内存计算(包含迭代式计算,DAG计算,流式计算 )框架,之前MapReduce因效率低下大家经常嘲笑,而Spark的出现让大家很清新。 Reynod 作为Spark核心开发者, 介绍Spark性能超Hadoop百倍,算法实现仅有其1/10或1/100。在去年的Sort benchmark上,Spark用了23min跑完了100TB的排序,刷新了之前Hadoop保持的世界纪录。

Linkedin都采用了哪些大数据开源技术?

在Linkedin,有很多数据产品,比如People you may like, job you may be interested, 你的用户访问来源,甚至你的career path都可以挖掘出来。那么在Linkedin也是大量用到开源技术,我这里就说一个最成功的Kafka,它是一个分布式的消息队列,可以用在tracking,机器内部metrics,数据传输。数据在前端后端会经过不同的存储或者平台,每个平台都有自己的格式,如果没有一个unified log,会出现灾难型的O(m*n)的数据对接复杂度,如果你设定的格式一旦发生变化,也是要修改所有相关的。所以这里提出的中间桥梁就是Kafka,大家约定用一个格式作为传输标准,然后在接受端可以任意定制你想要的数据源(topics),最后实现的线性的O(m+n)的复杂度。对应的设计细节,还是要参考设计文档 Apache Kafka 这里面主要作者Jay Kreps,Rao Jun 出来成立了Kafka作为独立发展的公司。

在Linkedin,Hadoop作为批处理的主力,大量应用在各个产品线上,比如广告组。我们一方面需要去做一些灵活的查询分析广告主的匹配,广告预测和实际效果,另外在报表生成方面也是Hadoop作为支持。如果你想去面试Linkedin 后端组,我建议大家去把Hive, Pig, Azkaban(数据流的管理软件),Avro 数据定义格式,Kafka,Voldemort 都去看一些设计理念,linkedin有专门的开源社区,也是build自己的技术品牌。Blog | LinkedIn Data Team

如果想从事大数据方面的工作,是否可以推荐一些有效的学习方法?有哪些推荐的书籍?

我也有一些建议,首先还是打好基础,Hadoop虽然是火热,但它的基础原理都是书本上很多年的积累,像算法导论,Unix设计哲学,数据库原理,深入理解计算机原理,Java设计模式,一些重量级的书可以参考。Hadoop 最经典的the definitive guide, 我在知乎上也有分享有什么关于 Spark 的书推荐? - 董飞的回答

其次是选择目标,如果你像做数据科学家,我可以推荐coursera的data science课程,通俗易懂Coursera - Specializations

学习Hive,Pig这些基本工具,如果做应用层,主要是把Hadoop的一些工作流要熟悉,包括一些基本调优,如果是想做架构,除了能搭建集群,对各个基础软件服务很了解,还要理解计算机的瓶颈和负载管理,Linux的一些性能工具。最后还是要多加练习,大数据本身就是靠实践的,你可以先按API写书上的例子,能够先调试成功,在下面就是多积累,当遇到相似的问题能找到对应的经典模式,再进一步就是实际问题,也许周边谁也没遇到,你需要些灵感和网上问问题的技巧,然后根据实际情况作出最佳选择。

谈一谈Coursera在大数据架构方面和其他硅谷创业公司相比有什么特点?是什么原因和技术取向造成了这些特点?

首先介绍一下Coursera, 作为MOOC(大型开放式网络课程)中领头羊,2012年由Stanford大学的Andrew和Daphne两名教授创立,目前160+员工,原Yale校长担任CEO。它的使命universal access to world's best education。很多人问我为什么加入,我还是非常认可公司的使命。我相信教育可以改变人生,同样我们也可以改变教育。能不能把技术跟教育结合起来,这是一个很有趣的话题。里面有很多可以结合,比如提供高可靠平台支持大规模用户在线并发访问,利用数据挖掘分析学生行为做个性化课程学习,并提高课程满意度,通过机器学习识别作业,互相评判,用技术让人们平等便捷的获取教育服务。

Coursera作为创业公司,非常想保持敏捷和高效。从技术上来说,所有的都是在基于AWS开发,可以想像随意启动云端服务,做一些实验。我们大致分成产品组,架构组,和数据分析组。我把所有用到的开发技术都列在上面。因为公司比较新,所以没有什么历史遗留迁移的问题。大家大胆的使用Scala作为主要编程语言,采用Python作为脚本控制,比如产品组就是提供的课程产品,里面大量使用Play Framework,Javascript的backbone作为控制中枢。而架构组主要是维护底层存储,通用服务,性能和稳定性。我在的数据组由10多人构成,一部分是对商业产品,核心增长指标做监控,挖掘和改进。一部分是搭建数据仓库完善跟各个部门的无缝数据流动,也用到很多技术例如使用Scalding编写Hadoop MapReduce程序,也有人做AB testing框架, 推荐系统,尽可能用最少人力做影响力的事情。其实除了开源世界,我们也积极使用第三方的产品,比如sumologic做日志错误分析,Redshift作为大数据分析平台,Slack做内部通讯。而所有的这些就是想解放生产力,把重心放到用户体验,产品开发和迭代上去。

Coursera是一个使命驱动的公司,大家不是为了追求技术的极致,而是为了服务好老师,同学,解决他们的痛点,分享他们的成功。这点是跟其他技术公司最大的区别。从一方面来说,现在还是早期积累阶段,大规模计算还没有来临,我们只有积极学习,适应变化才能保持创业公司的高速成长。

最后我的联系方式dongfeiwww@gmail.com

www.linkedin.com/in/dongfei

www.facebook.com/donglaoshi123

02 Mar 03:19

谷歌发布可在Hadoop中运行原生代码的C语言版本MapReduce开源框架

by Srini Penchikala

谷歌上周宣布发布C语言版本的MapReduce开源框架MR4C,利用该框架开发者可以在Hadoop框架中运行原生代码。

MR4C框架将原生开发算法的性能和灵活性与Hadoop执行框架的可扩展性和生产力相结合。该项目的目标是抽象化MapReduce框架的细节,让用户将精力集中在开发定制化算法之上。

该框架最初由Skybox团队开发,用于卫星图像处理和地理空间数据科学的用例。该团队希望既能利用用C和C++语言开发的图像处理库又能利用适于可扩展数据处理的Hadoop框架的作业跟踪和集群管理能力。

在MR4C中,算法存储在原生共享对象中,这些对象通过本地文件或统一资源标识符(URI)访问数据。输入/输出数据集、运行时参数和外部函数库都通过JavaScript对象表示法(JSON)文件进行配置。映射器分裂和资源分配可以用基于Apache YARN(适用于Hadoop v2)的工具配置或在集群层级配置(适用于MapReduce v1(MRv1))。多个算法的工作流可以通过自动生成的配置连接在一起。该框架还支持用Hadoop JobTracker接口浏览日志回调和过程报告。而且还可以用与目标Hadoop集群所用的相同接口在本地机器上对工作流进行测试。关于这个框架更多详细信息,可以从MR4C GitHub网站上检出该框架的相关文档和源码。如果有兴趣参与到项目中,MR4C团队已经创建了一个网页来帮助项目贡献者。

查看英文原文:Google Open Sources MapReduce Framework for C to Run Native Code in Hadoop

09 Feb 03:37

maven小贴士:可执行jar概述

by Martin

一个可执行jar是一个非常有用的产物。这意味着,只要Java装在客户端机器,无论是Windows还是Mac,你的用户只需双击这个jar包程序便会启动... 阅读原文 »

The post maven小贴士:可执行jar概述 appeared first on 头条 - 伯乐在线.

09 Feb 01:04

Modern C [pdf]

08 Feb 00:46

Writing Multithreaded Applications in C++

08 Feb 00:46

Go concurrency isn't parallelism: Real world lessons with Monte Carlo sims

07 Feb 06:35

Large HashMap overview: JDK, FastUtil, Goldman Sachs, HPPC, Koloboke, Trove – January 2015 version

by admin

by Mikhail Vorontsov

This is a major update of the previous version of this article. The reasons for this update are:

  • The major performance updates in fastutil 6.6.0
  • Updates in the “get” test from the original article, addition of “put/update” and “put/remove” tests
  • Adding identity maps to all tests
  • Now using different objects for any operations after map population (in case of Object keys – except identity maps). Old approach of reusing the same keys gave the unfair advantage to Koloboke.

I would like to thank Sebastiano Vigna for providing the initial versions of “get” and “put” tests.

Introduction

This article will give you an overview of hash map implementations in 5 well known libraries and JDK HashMap as a baseline. We will test separately:

  • Primitive to primitive maps
  • Primitive to object maps
  • Object to primitive maps
  • Object to Object maps
  • Object (identity) to Object maps

This article will provide you the results of 3 tests:

  • “Get” test: Populate a map with a pregenerated set of keys (in the JMH setup), make ~50% successful and ~50% unsuccessful “get” calls. For non-identity maps with object keys we use a distinct set of keys (the different object with the same value is used for successful “get” calls).
  • “Put/update” test: Add a pregenerated set of keys to the map. In the second loop add the equal set of keys (different objects with the same values) to this map again (make the updates). Identical keys are used for identity maps and for maps with primitive keys.
  • “Put/remove” test: In a loop: add 2 entries to a map, remove 1 of existing entries (“add” pointer is increased by 2 on each iteration, “remove” pointer is increased by 1).

This article will just give you the test results. There will be a followup article on the most interesting implementation details of the various hash maps.

Test participants

JDK 8

JDK HashMap is the oldest hash map implementation in this test. It got a couple of major updates recently – a shared underlying storage for the empty maps in Java 7u40 and a possibility to convert underlying hash bucket linked lists into tree maps (for better worse case performance) in Java 8.

FastUtil 6.6.0

FastUtil provides a developer a set of all 4 options listed above (all combinations of primitives and objects). Besides that, there are several other types of maps available for each parameter type combination: array map, AVL tree map and RB tree map. Nevertheless, we are only interested in hash maps in this article.

Goldman Sachs Collections 5.1.0

Goldman Sachs has open sourced its collections library about 3 years ago. In my opinion, this library provides the widest range of collections out of box (if you need them). You should definitely pay attention to it if you need more than a hash map, tree map and a list for your work :) For the purposes of this article, GS collections provide a normal, synchronized and unmodifiable versions of each hash map. The last 2 are just facades for the normal map, so they don’t provide any performance advantages.

HPPC 0.6.1

HPPC provides array lists, array dequeues, hash sets and hash maps for all primitive types. HPPC provides normal hash maps for primitive keys and both normal and identity hash maps for object keys.

Koloboke 0.6.5

Koloboke is the youngest of all libraries in this article. It is developed as a part of an OpenHFT project by Roman Leventov. This library currently provides hash maps and hash sets for all primitive/object combinations. This library was recently renamed from HFTC, so some artifacts in my tests will still use the old library name.

Trove 3.0.3

Trove is available for a long time and quite stable. Unfortunately, not much development is happening in this project at the moment. Trove provides you the list, stack, queue, hash set and map implementations for all primitive/object combinations. I have already written about Trove.

Data storage implementations and tests

This article will look at 5 different sorts of maps:

  1. intint
  2. intInteger
  3. Integerint
  4. IntegerInteger
  5. Integer (identity map)Integer

We will use JMH 1.0 for testing. Here is the test description: for each map size in (10K, 100K, 1M, 10M, 100M) (outer loop) generate a set of random keys (they will be used for each test at a given map size) and then run a test for each map implementations (inner loop). Each test will be run 100M / map_size times. “get”, “put” and “remove” tests are run separately, so you can update the test source code and run only a few of them.

Note that each test suite takes around 7-8 hours on my box. Spreadsheet-friendly results will be printed to stdout once all test suites will finish.

int-int

Each section will start with a table showing how data is stored inside each map. Only arrays will be shown here (some maps have special fields for a few corner cases).

tests.maptests.primitive.FastUtilMapTest int[] key, int[] value
tests.maptests.primitive.GsMutableMapTest int[] keys, int[] values
tests.maptests.primitive.HftcMutableMapTest long[] (key-low bits, value-high bits)
tests.maptests.primitive.HppcMapTest int[] keys, int[] values, boolean[] allocated
tests.maptests.primitive.TroveMapTest int[] _set, int[] _values, byte[] _states

As you can see, Koloboke is using a single array, FastUtil and GS use 2 arrays, and HPPC and Trove use 3 arrays to store the same data. Let’s see what would be the actual performance.

“Get” test results

All “get” tests make around 50% of unsuccessful get calls in order to test both success and failure paths in each map.

Each test results section will contain the results graph. X axis will show a map size, Y axis – time to run a test in milliseconds. Note, that each test in a graph has a fixed number of map method calls: 100M get call for “get” test; 200M put calls for “put” test; 100M put and 50M remove calls for “remove” tests.

There would be the links to OpenOffice spreadsheets with all test results at the end of this article.

int-int 'get' test results

int-int ‘get’ test results

GS and FastUtil test results lines are nearly parallel, but FastUtil is faster due to a lower constant factor. Koloboke becomes fastest only on large enough maps. Trove is slower than other implementations at each map size.

“Put” test results

“Put” tests insert all keys into a map and then use another equal set of keys to insert entries into a map again (these methods calls would update the existing entries). We make 100M put calls with “insert” functionality and 100M put calls with “update” functionality in each test.

int-int 'put' test results

int-int ‘put’ test results

This test shows the implementation difference more clear: Koloboke is fastest from the start (though FastUtil is as fast on small maps); GS and FastUtil are parallel again (but GS is always slower). HPPC and Trove are the slowest.

“Remove” test results

In “remove” test we interleave 2 put operations with 1 remove operation, so that a map size grows by 1 after each group of put/remove calls. In total we make 100M put and 50M remove calls.

int-int 'remove' test results

int-int ‘remove’ test results

Results are similar to “put” test (of course, both tests make a majority of put calls!): Koloboke is quickly becoming the fastest implementation; FastUtil is a bit faster than GS on all map sizes; HPPC and Trove are the slowest, but HPPC performs reasonably good on map sizes up to 1M entries.

int-int summary

An underlying storage implementation is the most important factor defining the hash map performance: the fewer memory accesses an implementation makes (especially for large maps which do not into CPU cache) to access an entry – the faster it would be. As you can see, the single array Koloboke is faster than other implementations in most of tests on large map sizes. For smaller map sizes, CPU cache starts hiding the costs of accessing several arrays – in this case other implementations may be faster due to less CPU commands required for a method call: FastUtil is the second best implementation for primitive collection tests due to its highly optimized code.


int-Object

tests.maptests.prim_object.FastUtilIntObjectMapTest int[] key, Object[] value
tests.maptests.prim_object.GsIntObjectMapTest int[] keys, Object[] values
tests.maptests.prim_object.HftcIntObjectMapTest int[] keys, Object[] values
tests.maptests.prim_object.HppcIntObjectMapTest int[] keys, Object[] values, boolean[] allocated
tests.maptests.prim_object.TroveIntObjectMapTest int[] _set, Object[] _values, byte[] _states

There are 2 groups here: FastUtil, GS and Koloboke are using 2 arrays; HPPC and Trove are using 3 arrays.

“Get” test results

int-Object 'get' test results

int-Object ‘get’ test results

As you can, see FastUtil and Koloboke are very close to each other, though FastUtil is consistently faster. GS and HPPC form the next group, where HPPC is slightly faster than GS (which is a surprise despite the extra underlying array). Trove is noticeably slower.

“Put” test results

int-Object 'put' test results

int-Object ‘put’ test results

FastUtil, Koloboke and GS are leaders in the “put” test (and FastUtil is a clear winner here). HPPC is getting slower than Trove on the large map sizes.

“Remove” test results

int-Object 'remove' test results

int-Object ‘remove’ test results

This picture is very similar to the previous one: nearly identical results of FastUtil and Koloboke; slightly slower GS; HPPC is pretty good on the smaller map sizes, but is getting slower on the large maps; Trove is slower than HPPC, but parallel to it.

int-Object summary

Extra byte/boolean array used by HPPC/Trove makes them predictably slower than 3 other implementations in this test. Once the underlying storage becomes identical, second order optimizations starts to make the difference. As a result, FastUtil is getting faster than other 2-array maps, and HPPC is getting close to 2-array maps on the smaller maps sizes, where the CPU cache can fit the whole/most of the map, so the extra array does not make a serious difference.

Object-int

tests.maptests.object_prim.FastUtilObjectIntMapTest Object[] key, int[] value
tests.maptests.object_prim.GsObjectIntMapTest Object[] keys, int[] values
tests.maptests.object_prim.HftcObjectIntMapTest Object[] keys, int[] values
tests.maptests.object_prim.HppcObjectIntMapTest Object[] keys, int[] values, boolean[] allocated
tests.maptests.object_prim.TroveObjectIntMapTest Object[] _set, int[] _values

Only HPPC is still using 3 arrays for Object-int mapping. Hopefully, this would be fixed in the next HPPC release.

“Get” test results

Object-int 'get' results

Object-int ‘get’ results

FastUtil is a leader in this test. Koloboke and GS are very close, though GS is running a little slower if a map no longer fits into CPU cache. HPPC is surprisingly faster than Trove…

“Put” test results

Object-int 'put' results

Object-int ‘put’ results

FastUtil, Koloboke and GS are very close to each other on the map sizes up to 1M, but you can see the difference after this point: FastUtil is the fastest, Koloboke is the second and GS is the third. Trove is a little slower than those 3 implementations, but much faster than HPPC, which behaves really bad on the very large map sizes.

“Remove” test results

Object-int 'remove' results

Object-int ‘remove’ results

This test is very similar to the previous one with the only difference: the gap between Trove and 3 fastest implementations is much bigger.

Object-int summary

This test is again highlighting the importance of having the minimal possible number of underlying arrays in a map implementation: once you have an extra one, your implementation becomes non-competitive. The next important lesson you can learn is to use the underlying arrays with a size equal to a power of 2. This allows you to use bit operations while calculating a lookup position in a map instead of an expensive mod (used by Trove).

Object-Object

tests.maptests.object.FastUtilObjMapTest Object[] key, Object[] value
tests.maptests.object.GsObjMapTest Object[] table – interleaved keys and values
tests.maptests.object.HftcMutableObjTest Object[] table – interleaved keys and values
tests.maptests.object.HppcObjMapTest Object[] keys, Object[] values, boolean[] allocated
tests.maptests.object.JdkMapTest Node<K,V>[] table – each Node could be a part of a linked list or a TreeMap (Java 8)
tests.maptests.object.TroveObjMapTest Object[] _set, Object[] _values

This group of tests will be bigger than the previous ones. We will see if Koloboke can make use of the fact (you can specify it in the factory) that there would be no null keys. We will also try to work around the JDK HashMap “feature” that there may be one rehashing before you will add the requested number of entries in the map (JDK implementation may not allocate arrays large enough to store the requested map size).

“Get” test results

Object-Object 'get' results

Object-Object ‘get’ results

This test results are pretty surprising:

  • Both JDK versions are the fastest (rehashing does not make a difference in this test because it happens at setup).
  • FastUtil and GS are close to JDK, but slightly slower. Nevertheless, they require less memory overhead, so they may still be considered as an option.
  • Koloboke is close to the above mentioned implementations on some map sizes, but slower on others. This is most likely due to the variable fill factor (Koloboke does not perform well if you try to set a fixed fill factor).
  • Surprisingly, Trove is slower than HPPC in this test despite an extra “allocated” array in the HPPC implementation.

“Put” test results

Object-Object 'put' results

Object-Object ‘put’ results

  • Koloboke implementation is the fastest in this test due to best possible storage structure.
  • FastUtil keeps up with Koloboke on the smaller map sizes (while the map fits in CPU cache), but becomes a little slower on large maps due to a less efficient memory access pattern.
  • GS, on the other hand, is slower than Koloboke and FastUtil on the smaller maps (due to less efficient code), but is getting closer to Koloboke once a map no longer fits in CPU cache.
  • As for JDK maps, you can see that a map with correct preallocated capacity is always faster than a default map (by default I mean a map where you specify just the right capacity in the constructor, say 10.000, instead of an inflated capacity of 10.000/fill_factor(0.5)=20.000). The difference is bigger on some sizes and smaller on the others, due to either one or 2 factors in effect: 1) you will always have a smaller chance of hash collisions in the bigger capacity map; 2) you may avoid rehashing by specifying a bigger capacity in the JDK map.
  • Trove is faster than HPPC in this test due to a 2-array underlying implementation.

“Remove” test results

Object-Object 'remove' results

Object-Object ‘remove’ results

  • Koloboke is the fastest in this test and GS is very close to GS. FastUtil and a proper capacity JDK map are very close, but slightly slower.
  • Default capacity JDK map is noticeably slower than the first 4 implementations.
  • It is followed by Trove (not far behind) and HPPC (too far behind).

Object-Object summary

The first lesson you should remember from this group of maps is that a JDK HashMap may not allocate enough storage for the requested number of elements. As a result, you may be penalised by rehashing. Nevertheless, JDK HashMap is extremely good if you mostly use it as a read-only storage.

The second lesson is that an underlying storage is still the most important factor affecting the hash map performance – an implementation should try to minimize a number of memory accesses for each operation and do not expect that a CPU cache would help it.

Identity maps

The most important property of identity maps is that they expect that the same object will be used for all accesses to the map. It means that an identity map will use == instead of yourObject.equals() and System.identityHashCode() instead of yourObject.hashCode().

Keep in mind that some non-identity maps also make == check prior to requested_key.equals(stored_map_key) check (for example, JDK and Koloboke implement such check) in hope that some previously inserted keys may be later used for queries. Pay attention if this is your application case.

tests.maptests.identity_object.FastUtilRef2ObjectMapTest Object[] key, Object[] value
tests.maptests.identity_object.GsIdentityMapTest Object[] table – interleaved keys and values
tests.maptests.identity_object.HftcIdentityMapTest Object[] table – interleaved keys and values
tests.maptests.identity_object.HppcIdentityMapTest Object[] keys, Object[] values, boolean[] allocated
tests.maptests.identity_object.JDKIdentityMapTest Object[] table – interleaved keys and values
tests.maptests.identity_object.TroveIdentityMapTest Object[] _set, Object[] _values

There are 3 groups here: JDK, Koloboke and GS use a single interleaved array, FastUtil and Trove use 2 arrays, finally HPPC uses 3 arrays.

“Get” test results

Object (identity)-Object 'get' results

Object (identity)-Object ‘get’ results

“Put” test results

Object (identity)-Object 'put' results

Object (identity)-Object ‘put’ results

“Remove” test results

Object (identity)-Object 'remove' results

Object (identity)-Object ‘remove’ results

All these tests are mode difficult to comment. We can see that Trove is simply slow and HPPC is penalised for the third underlying array. FastUtil, GS and JDK are consistently good. Koloboke is also good, but is surprisingly slower than most of implementations on the small maps sizes in “get” tests.

Summary

  • FastUtil 6.6.0 turned out to be consistently fast. It may become even faster if it would introduce any other storage structures except 2 arrays for keys and values.
  • Koloboke is getting second in many tests, but it still outperforms FastUtil in int-int tests.
  • GS implementation is good enough, but is slower than FastUtil and Koloboke.
  • JDK maps are pretty good for Object-Object maps provided that you can tolerate the extra memory consumption and you will call HashMap constructor with required capacity = actual_capacity / fill_factor + 1 to avoid rehashing.
  • Trove suffers from using mod operation for array index calculations and HPPC is too slow due to an extra underlying array (for cell states).

Source code

The article source code is now hosted at GitHub: https://github.com/mikvor/hashmapTest.

Please note you should run this project via tests.MapTestRunner class:

mvn clean install
java -cp target/benchmarks.jar tests.MapTestRunner

The full test set may take around 24 hours to complete. You need a computer with proper CPU cooling to run this test set, so it can sustain an hours long CPU load without throttling (small laptops are seldom designed for such load). You need 20G+ heap to run the 100M tests, so it makes sense to shrink MapTestRunner.TOTAL_SIZE to 10M if you want to use a commodity computer for testing.

Test results

Here are the article test results in form of OpenOffice spreadsheet files.

The post Large HashMap overview: JDK, FastUtil, Goldman Sachs, HPPC, Koloboke, Trove – January 2015 version appeared first on Java Performance Tuning Guide.

07 Feb 01:04

Go for C++ Programmers

07 Feb 00:31

AerospikeDB与Redis性能比较:在AWS上的NoSQL基准测试

by 谢丽

AerospikeDB以低延迟和高吞吐量而闻名,已经用于许多大型的、要求堪称苛刻的实时平台。而Redis同样以速度著称,并且也经常用作缓存。有鉴于此,Aerospike团队近日联合拥有大数据和云架构师、AWS社区英雄、谷歌云开发专家、微软MVP(SQL Server)等众多头衔的Lynn Langit在AWS云的虚拟机上对AerospikeDB和Redis进行了基准测试,测试结果已经发布在Aerospike官方博客上。而Lynn也发表了题为《学到的经验——在AWS云上进行NoSQL基准测试(AerospikeDB与Redis)》的博文,对测试过程和结果进行了更为详细的描述。

由于AerospikeDB是多线程的,而Redis是单线程的,所以为了公平起见,需要对Redis进行扩展,以便它能够使用每个AWS EC2实例上的多个内核。在这个过程中,Lynn发现了AerospikeDB和Redis在扩展或分片的可管理性方面的差异:

  • Redis需要开发人员自己管理分片并提供分片算法用于在各分片之间平衡数据;而AerospikeDB可以自动处理相当于分片的工作;
  • 在Redis中,为了增加吞吐量,需要增加Redis分片的数量,并重构分片算法及重新平衡数据,这通常需要停机;而在AerospikeDB中,可以动态增加数据卷和吞吐量,无需停机,并且AerospikeDB可以自动平衡数据和流量;
  • 在Redis中,如果需要复制及故障转移功能,则需要开发人员自己在应用程序层同步数据;而在AerospikeDB中,只需设置复制因子,然后由AerospikeDB完成同步复制操作,保持即时一致性;而且AerospikeDB可以透明地完成故障转移;
  • 此外,AerospikeDB既可以完全在内存中运行,也可以利用Flash/SSD存储的优点。

接下来,Lynn针对下列工作负载做了两组基准测试:

  • 负载一:50%-50% 读/写(-w RU, 50);
  • 负载二:80%-20% 读/写(-w RU, 80);
  • 负载三:100% 读(-w RU, 100)。

第一组基准测试是在单个没有永久存储的AWS R3.8xlarge节点上进行的,测试结果如下:

(图一)

从上图可以看出,在运行(负载三)时,AerospikeDB和Redis性能相近,均接近1 MTPS。

第二组测试是在同样的实例上进行的,但引入了永久性存储。所有数据既会保存在内存中,也会存储在EBS SSD(gp2)存储上。在本组测试中,Lynn为AerospikeDB配置了一个新的命名空间,并使用了配置参数“data-in-memory”。而且,为了避免写入单个文件造成瓶颈,她还为AerospikeDB配置了12个不同的可写入位置。对于Redis,则启用了“appendonly”选项。在这种模式下,一旦AOF文件增长到一定的大小,Redis就会在后台重写AOF文件。这时,Redis的吞吐量就会下降。为了避免这种情况出现,Lynn将auto-aof-rewrite-min-size参数设为一个很大的值。这在一定程度上会夸大Redis的性能。在这个场景中,磁盘写成为瓶颈。因此,Lynn针对AerospikeDB和Redis均减少了客户端线程的数量,保证不出现写错误。测试结果如下:

(图二)

从上图可以看出,在运行(负载二)和(负载三)时,AerospikeDB都比Redis略快。

此外,Lynn还单独测试了AOF重写对吞吐量的影响,测试结果如下:

(图三)

从上图可以看出,AOF重写对Redis读写性能均有较大的影响。

按照Lynn的说法,上述基准测试结果可能会因为云环境本身的不稳定性、调优技术和基础设置的差异而有所不同。感兴趣的读者可以查看原文了解Lynn的测试步骤及配置细节。

在上述结果发布后,Redis创建者Salvatore Sanfilippo很快就以《我们为什么不做基准测试来比较Redis和其它DB》为题发表博文对此进行了回应。他认为,这种对比有广告嫌疑,并提供了一些从Redis得出更好结果的方法。紧接着,Redis首席开发大使Itamar Haber也发表了题为《漏掉的经验——在AWS云上进行NoSQL基准测试(AerospikeDB与Redis)》的博文,对Aerospike团队和Lynn的测试结果提出质疑,主要包含如下几点:

  • 该测试没有使用推荐的Redis做法,比如使用管道和多键操作;
  • 该测试没有测试工作负载:20%-80%读写(-w RU,20)和100%写(-w RU,0);
  • 比较(图一)和(图二)可以得出:在针对(负载三)的测试中,第一次测试结果为928K TPS,第二次测试结果为860K TPS,使用了AOF并不能完全解释这种差别;此处还有一点令人疑惑,后端使用EBS的Redis其性能(180K TPS)竟然高于只在内存中使用的Redis服务器(132K TPS)。

同时,Itamar指出,对于AOF,一个常见的做法是引入一个从属Redis实例,专门用于持久化处理,以减轻主实例的负担。

最后,他写道:

比较是一件很难做对却很容易做错的事。


感谢郭蕾对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。

06 Feb 01:11

How to transfer large amounts of data via network

06 Feb 01:08

Ask Slashdot: What Tools To Clean Up a Large C/C++ Project?

by timothy
An anonymous reader writes I find myself in the uncomfortable position of having to 'cleanup' a relatively large C/C++ project. We are talking ~200 files, 11MB of source code, 220K lines of code... A superficial glance shows that they are a lot of functions that seems to be doing the same things, a lot of 'unused' stuff, and a lot of inconsistency between what is declared in .h files and what is implemented in the corresponding .cpp files. Are there any tools that will help me catalog this mess and make it easier for me to locate/erase unused things, cleanup .h files, find functions with similar names?

Share on Google+

Read more of this story at Slashdot.








06 Feb 01:01

Hadoop学习笔记:MapReduce框架详解

by zzr0427

开始聊mapreduce,mapreduce是hadoop的计算框架,我学hadoop是从hive开始入手,再到hdfs,当我学习hdfs时候,就感觉到hdfs和mapreduce关系的紧密。这个可能是我做技术研究的思路有关,我开始学习某一套技术总是想着这套技术到底能干什么,只有当我真正理解了这套技术解决了什么问题时候,我后续的学习就能逐步的加快,而学习hdfs时候我就发现,要理解hadoop框架的意义,hdfs和mapreduce是密不可分,所以当我写分布式文件系统时候,总是感觉自己的理解肤浅,今天我开始写mapreduce了,今天写文章时候比上周要进步多,不过到底能不能写好本文了,只有试试再说了。

Mapreduce初析

Mapreduce是一个计算框架,既然是做计算的框架,那么表现形式就是有个输入(input),mapreduce操作这个输入(input),通过本身定义好的计算模型,得到一个输出(output),这个输出就是我们所需要的结果。

我们要学习的就是这个计算模型的运行规则。在运行一个mapreduce计算任务时候,任务过程被分为两个阶段:map阶段和reduce阶段,每个阶段都是用键值对(key/value)作为输入(input)和输出(output)。而程序员要做的就是定义好这两个阶段的函数:map函数和reduce函数。

Mapreduce的基础实例

讲解mapreduce运行原理前,首先我们看看mapreduce里的hello world实例WordCount,这个实例在任何一个版本的hadoop安装程序里都会有,大家很容易找到,这里我还是贴出代码,便于我后面的讲解,代码如下:

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.hadoop.examples;

import java.io.IOException;
import java.util.StringTokenizer;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;

public class WordCount {

  public static class TokenizerMapper 
       extends Mapper<Object, Text, Text, IntWritable>{

    private final static IntWritable one = new IntWritable(1);
    private Text word = new Text();

    public void map(Object key, Text value, Context context
                    ) throws IOException, InterruptedException {
      StringTokenizer itr = new StringTokenizer(value.toString());
      while (itr.hasMoreTokens()) {
        word.set(itr.nextToken());
        context.write(word, one);
      }
    }
  }

  public static class IntSumReducer 
       extends Reducer<Text,IntWritable,Text,IntWritable> {
    private IntWritable result = new IntWritable();

    public void reduce(Text key, Iterable<IntWritable> values, 
                       Context context
                       ) throws IOException, InterruptedException {
      int sum = 0;
      for (IntWritable val : values) {
        sum += val.get();
      }
      result.set(sum);
      context.write(key, result);
    }
  }

  public static void main(String[] args) throws Exception {
    Configuration conf = new Configuration();
    String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
    if (otherArgs.length != 2) {
      System.err.println("Usage: wordcount <in> <out>");
      System.exit(2);
    }
    Job job = new Job(conf, "word count");
    job.setJarByClass(WordCount.class);
    job.setMapperClass(TokenizerMapper.class);
    job.setCombinerClass(IntSumReducer.class);
    job.setReducerClass(IntSumReducer.class);
    job.setOutputKeyClass(Text.class);
    job.setOutputValueClass(IntWritable.class);
    FileInputFormat.addInputPath(job, new Path(otherArgs[0]));
    FileOutputFormat.setOutputPath(job, new Path(otherArgs[1]));
    System.exit(job.waitForCompletion(true) ? 0 : 1);
  }
}

如何运行它,这里不做累述了,大伙可以百度下,网上这方面的资料很多。这里的实例代码是使用新的api,大家可能在很多书籍里看到讲解mapreduce的WordCount实例都是老版本的api,这里我不给出老版本的api,因为老版本的api不太建议使用了,大家做开发最好使用新版本的api,新版本api和旧版本api有区别在哪里:

  1. 新的api放在:org.apache.hadoop.mapreduce,旧版api放在:org.apache.hadoop.mapred
  2. 新版api使用虚类,而旧版的使用的是接口,虚类更加利于扩展,这个是一个经验,大家可以好好学习下hadoop的这个经验。

其他还有很多区别,都是说明新版本api的优势,因为我提倡使用新版api,这里就不讲这些,因为没必要再用旧版本,因此这种比较也没啥意义了。

下面我对代码做简单的讲解,大家看到要写一个mapreduce程序,我们的实现一个map函数和reduce函数。我们看看map的方法:

public void map(Object key, Text value, Context context) throws IOException, InterruptedException {…}

这里有三个参数,前面两个Object key, Text value就是输入的key和value,第三个参数Context context这是可以记录输入的key和value,例如:context.write(word, one);此外context还会记录map运算的状态。

对于reduce函数的方法:

public void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {…}

reduce函数的输入也是一个key/value的形式,不过它的value是一个迭代器的形式Iterable<IntWritable> values,也就是说reduce的输入是一个key对应一组的值的value,reduce也有context和map的context作用一致。

至于计算的逻辑就是程序员自己去实现了。

下面就是main函数的调用了,这个我要详细讲述下,首先是:

Configuration conf = new Configuration();

运行mapreduce程序前都要初始化Configuration,该类主要是读取mapreduce系统配置信息,这些信息包括hdfs还有mapreduce,也就是安装hadoop时候的配置文件例如:core-site.xml、hdfs-site.xml和mapred-site.xml等等文件里的信息,有些童鞋不理解为啥要这么做,这个是没有深入思考mapreduce计算框架造成,我们程序员开发mapreduce时候只是在填空,在map函数和reduce函数里编写实际进行的业务逻辑,其它的工作都是交给mapreduce框架自己操作的,但是至少我们要告诉它怎么操作啊,比如hdfs在哪里啊,mapreduce的jobstracker在哪里啊,而这些信息就在conf包下的配置文件里。

接下来的代码是:

  String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
    if (otherArgs.length != 2) {
      System.err.println("Usage: wordcount <in> <out>");
      System.exit(2);
    }

If的语句好理解,就是运行WordCount程序时候一定是两个参数,如果不是就会报错退出。至于第一句里的GenericOptionsParser类,它是用来解释常用hadoop命令,并根据需要为Configuration对象设置相应的值,其实平时开发里我们不太常用它,而是让类实现Tool接口,然后再main函数里使用ToolRunner运行程序,而ToolRunner内部会调用GenericOptionsParser。

接下来的代码是:

    Job job = new Job(conf, "word count");
    job.setJarByClass(WordCount.class);
    job.setMapperClass(TokenizerMapper.class);
    job.setCombinerClass(IntSumReducer.class);
    job.setReducerClass(IntSumReducer.class);

第一行就是在构建一个job,在mapreduce框架里一个mapreduce任务也叫mapreduce作业也叫做一个mapreduce的job,而具体的map和reduce运算就是task了,这里我们构建一个job,构建时候有两个参数,一个是conf这个就不累述了,一个是这个job的名称。

第二行就是装载程序员编写好的计算程序,例如我们的程序类名就是WordCount了。这里我要做下纠正,虽然我们编写mapreduce程序只需要实现map函数和reduce函数,但是实际开发我们要实现三个类,第三个类是为了配置mapreduce如何运行map和reduce函数,准确的说就是构建一个mapreduce能执行的job了,例如WordCount类。

第三行和第五行就是装载map函数和reduce函数实现类了,这里多了个第四行,这个是装载Combiner类,这个我后面讲mapreduce运行机制时候会讲述,其实本例去掉第四行也没有关系,但是使用了第四行理论上运行效率会更好。

接下来的代码:

    job.setOutputKeyClass(Text.class);
    job.setOutputValueClass(IntWritable.class);

这个是定义输出的key/value的类型,也就是最终存储在hdfs上结果文件的key/value的类型。

最后的代码是:

    FileInputFormat.addInputPath(job, new Path(otherArgs[0]));
    FileOutputFormat.setOutputPath(job, new Path(otherArgs[1]));
    System.exit(job.waitForCompletion(true) ? 0 : 1);

第一行就是构建输入的数据文件,第二行是构建输出的数据文件,最后一行如果job运行成功了,我们的程序就会正常退出。FileInputFormat和FileOutputFormat是很有学问的,我会在下面的mapreduce运行机制里讲解到它们。

好了,mapreduce里的hello word程序讲解完毕,我这个讲解是从新办api进行,这套讲解在网络上还是比较少的,应该很具有代表性的。

Mapreduce运行机制

下面我要讲讲mapreduce的运行机制了,前不久我为公司出了一套hadoop面试题,里面就问道了mapreduce运行机制,出题时候我发现这个问题我自己似乎也将不太清楚,因此最近几天恶补了下,希望在本文里能说清楚这个问题。

下面我贴出几张图,这些图都是我在百度图片里找到的比较好的图片:

图片一:

  图片二:

  图片三:

  图片四:

  图片五:

  图片六:

我现在学习技术很喜欢看图,每次有了新理解就会去看看图,每次都会有新的发现。

谈mapreduce运行机制,可以从很多不同的角度来描述,比如说从mapreduce运行流程来讲解,也可以从计算模型的逻辑流程来进行讲解,也许有些深入理解了mapreduce运行机制还会从更好的角度来描述,但是将mapreduce运行机制有些东西是避免不了的,就是一个个参入的实例对象,一个就是计算模型的逻辑定义阶段,我这里讲解不从什么流程出发,就从这些一个个牵涉的对象,不管是物理实体还是逻辑实体。

首先讲讲物理实体,参入mapreduce作业执行涉及4个独立的实体:

  1. 客户端(client):编写mapreduce程序,配置作业,提交作业,这就是程序员完成的工作;
  2. JobTracker:初始化作业,分配作业,与TaskTracker通信,协调整个作业的执行;
  3. TaskTracker:保持与JobTracker的通信,在分配的数据片段上执行Map或Reduce任务,TaskTracker和JobTracker的不同有个很重要的方面,就是在执行任务时候TaskTracker可以有n多个,JobTracker则只会有一个(JobTracker只能有一个就和hdfs里namenode一样存在单点故障,我会在后面的mapreduce的相关问题里讲到这个问题的)
  4. Hdfs:保存作业的数据、配置信息等等,最后的结果也是保存在hdfs上面

那么mapreduce到底是如何运行的呢?

首先是客户端要编写好mapreduce程序,配置好mapreduce的作业也就是job,接下来就是提交job了,提交job是提交到JobTracker上的,这个时候JobTracker就会构建这个job,具体就是分配一个新的job任务的ID值,接下来它会做检查操作,这个检查就是确定输出目录是否存在,如果存在那么job就不能正常运行下去,JobTracker会抛出错误给客户端,接下来还要检查输入目录是否存在,如果不存在同样抛出错误,如果存在JobTracker会根据输入计算输入分片(Input Split),如果分片计算不出来也会抛出错误,至于输入分片我后面会做讲解的,这些都做好了JobTracker就会配置Job需要的资源了。分配好资源后,JobTracker就会初始化作业,初始化主要做的是将Job放入一个内部的队列,让配置好的作业调度器能调度到这个作业,作业调度器会初始化这个job,初始化就是创建一个正在运行的job对象(封装任务和记录信息),以便JobTracker跟踪job的状态和进程。

初始化完毕后,作业调度器会获取输入分片信息(input split),每个分片创建一个map任务。接下来就是任务分配了,这个时候tasktracker会运行一个简单的循环机制定期发送心跳给jobtracker,心跳间隔是5秒,程序员可以配置这个时间,心跳就是jobtracker和tasktracker沟通的桥梁,通过心跳,jobtracker可以监控tasktracker是否存活,也可以获取tasktracker处理的状态和问题,同时tasktracker也可以通过心跳里的返回值获取jobtracker给它的操作指令。任务分配好后就是执行任务了。在任务执行时候jobtracker可以通过心跳机制监控tasktracker的状态和进度,同时也能计算出整个job的状态和进度,而tasktracker也可以本地监控自己的状态和进度。当jobtracker获得了最后一个完成指定任务的tasktracker操作成功的通知时候,jobtracker会把整个job状态置为成功,然后当客户端查询job运行状态时候(注意:这个是异步操作),客户端会查到job完成的通知的。如果job中途失败,mapreduce也会有相应机制处理,一般而言如果不是程序员程序本身有bug,mapreduce错误处理机制都能保证提交的job能正常完成。

下面我从逻辑实体的角度讲解mapreduce运行机制,这些按照时间顺序包括:输入分片(input split)、map阶段、combiner阶段、shuffle阶段和reduce阶段

1. 输入分片(input split):在进行map计算之前,mapreduce会根据输入文件计算输入分片(input split),每个输入分片(input split)针对一个map任务,输入分片(input split)存储的并非数据本身,而是一个分片长度和一个记录数据的位置的数组,输入分片(input split)往往和hdfs的block(块)关系很密切,假如我们设定hdfs的块的大小是64mb,如果我们输入有三个文件,大小分别是3mb、65mb和127mb,那么mapreduce会把3mb文件分为一个输入分片(input split),65mb则是两个输入分片(input split)而127mb也是两个输入分片(input split),换句话说我们如果在map计算前做输入分片调整,例如合并小文件,那么就会有5个map任务将执行,而且每个map执行的数据大小不均,这个也是mapreduce优化计算的一个关键点。

2. map阶段:就是程序员编写好的map函数了,因此map函数效率相对好控制,而且一般map操作都是本地化操作也就是在数据存储节点上进行;

3. combiner阶段:combiner阶段是程序员可以选择的,combiner其实也是一种reduce操作,因此我们看见WordCount类里是用reduce进行加载的。Combiner是一个本地化的reduce操作,它是map运算的后续操作,主要是在map计算出中间文件前做一个简单的合并重复key值的操作,例如我们对文件里的单词频率做统计,map计算时候如果碰到一个hadoop的单词就会记录为1,但是这篇文章里hadoop可能会出现n多次,那么map输出文件冗余就会很多,因此在reduce计算前对相同的key做一个合并操作,那么文件会变小,这样就提高了宽带的传输效率,毕竟hadoop计算力宽带资源往往是计算的瓶颈也是最为宝贵的资源,但是combiner操作是有风险的,使用它的原则是combiner的输入不会影响到reduce计算的最终输入,例如:如果计算只是求总数,最大值,最小值可以使用combiner,但是做平均值计算使用combiner的话,最终的reduce计算结果就会出错。

4. shuffle阶段:将map的输出作为reduce的输入的过程就是shuffle了,这个是mapreduce优化的重点地方。这里我不讲怎么优化shuffle阶段,讲讲shuffle阶段的原理,因为大部分的书籍里都没讲清楚shuffle阶段。Shuffle一开始就是map阶段做输出操作,一般mapreduce计算的都是海量数据,map输出时候不可能把所有文件都放到内存操作,因此map写入磁盘的过程十分的复杂,更何况map输出时候要对结果进行排序,内存开销是很大的,map在做输出时候会在内存里开启一个环形内存缓冲区,这个缓冲区专门用来输出的,默认大小是100mb,并且在配置文件里为这个缓冲区设定了一个阀值,默认是0.80(这个大小和阀值都是可以在配置文件里进行配置的),同时map还会为输出操作启动一个守护线程,如果缓冲区的内存达到了阀值的80%时候,这个守护线程就会把内容写到磁盘上,这个过程叫spill,另外的20%内存可以继续写入要写进磁盘的数据,写入磁盘和写入内存操作是互不干扰的,如果缓存区被撑满了,那么map就会阻塞写入内存的操作,让写入磁盘操作完成后再继续执行写入内存操作,前面我讲到写入磁盘前会有个排序操作,这个是在写入磁盘操作时候进行,不是在写入内存时候进行的,如果我们定义了combiner函数,那么排序前还会执行combiner操作。

每次spill操作也就是写入磁盘操作时候就会写一个溢出文件,也就是说在做map输出有几次spill就会产生多少个溢出文件,等map输出全部做完后,map会合并这些输出文件。这个过程里还会有一个Partitioner操作,对于这个操作很多人都很迷糊,其实Partitioner操作和map阶段的输入分片(Input split)很像,一个Partitioner对应一个reduce作业,如果我们mapreduce操作只有一个reduce操作,那么Partitioner就只有一个,如果我们有多个reduce操作,那么Partitioner对应的就会有多个,Partitioner因此就是reduce的输入分片,这个程序员可以编程控制,主要是根据实际key和value的值,根据实际业务类型或者为了更好的reduce负载均衡要求进行,这是提高reduce效率的一个关键所在。到了reduce阶段就是合并map输出文件了,Partitioner会找到对应的map输出文件,然后进行复制操作,复制操作时reduce会开启几个复制线程,这些线程默认个数是5个,程序员也可以在配置文件更改复制线程的个数,这个复制过程和map写入磁盘过程类似,也有阀值和内存大小,阀值一样可以在配置文件里配置,而内存大小是直接使用reduce的tasktracker的内存大小,复制时候reduce还会进行排序操作和合并文件操作,这些操作完了就会进行reduce计算了。

5. reduce阶段:和map函数一样也是程序员编写的,最终结果是存储在hdfs上的。

Mapreduce的相关问题

这里我要谈谈我学习mapreduce思考的一些问题,都是我自己想出解释的问题,但是某些问题到底对不对,就要广大童鞋帮我确认了。

① jobtracker的单点故障:jobtracker和hdfs的namenode一样也存在单点故障,单点故障一直是hadoop被人诟病的大问题,为什么hadoop的做的文件系统和mapreduce计算框架都是高容错的,但是最重要的管理节点的故障机制却如此不好,我认为主要是namenode和jobtracker在实际运行中都是在内存操作,而做到内存的容错就比较复杂了,只有当内存数据被持久化后容错才好做,namenode和jobtracker都可以备份自己持久化的文件,但是这个持久化都会有延迟,因此真的出故障,任然不能整体恢复,另外hadoop框架里包含zookeeper框架,zookeeper可以结合jobtracker,用几台机器同时部署jobtracker,保证一台出故障,有一台马上能补充上,不过这种方式也没法恢复正在跑的mapreduce任务。

② 做mapreduce计算时候,输出一般是一个文件夹,而且该文件夹是不能存在,我在出面试题时候提到了这个问题,而且这个检查做的很早,当我们提交job时候就会进行,mapreduce之所以这么设计是保证数据可靠性,如果输出目录存在reduce就搞不清楚你到底是要追加还是覆盖,不管是追加和覆盖操作都会有可能导致最终结果出问题,mapreduce是做海量数据计算,一个生产计算的成本很高,例如一个job完全执行完可能要几个小时,因此一切影响错误的情况mapreduce是零容忍的。

③ Mapreduce还有一个InputFormat和OutputFormat,我们在编写map函数时候发现map方法的参数是之间操作行数据,没有牵涉到InputFormat,这些事情在我们new Path时候mapreduce计算框架帮我们做好了,而OutputFormat也是reduce帮我们做好了,我们使用什么样的输入文件,就要调用什么样的InputFormat,InputFormat是和我们输入的文件类型相关的,mapreduce里常用的InputFormat有FileInputFormat普通文本文件,SequenceFileInputFormat是指hadoop的序列化文件,另外还有KeyValueTextInputFormat。OutputFormat就是我们想最终存储到hdfs系统上的文件格式了,这个根据你需要定义了,hadoop有支持很多文件格式,这里不一一列举,想知道百度下就看到了。

好了,文章写完了,呵呵,这篇我自己感觉写的不错,是目前hadoop系列文章里写的最好的,我后面会再接再厉的。加油!!!

Hadoop学习笔记:MapReduce框架详解,首发于博客 - 伯乐在线

06 Feb 00:33

Fastsocket学习笔记之小结篇

by nieyong

前言

前面啰啰嗦嗦的几篇文字,各个方面介绍了Fastsocket,盲人摸象一般,能力有限,还得继续深入学习不是。这不,到了该小结收尾的时候了。

缘起,内核已经成为瓶颈

使用Linux作为服务器,在请求量很小的时候,是不用担心其性能。但在海量的数据请求下,Linux内核在TCP/IP网络处理方面,已经成为瓶颈。比如新浪在某台HAProxy服务器上取样,90%的CPU时间被内核占用,应用程序只能够分配到较少的CPU时钟周期的资源。

经过Haproxy系统详尽分析后,发现大部分CPU资源消耗在kernel里,并且在多核平台下,kernel在网络协议栈处理过程中存在着大量同步开销。

同时在多核上进行测试,HTTP CPS(Connection Per Second)吞吐量并没有随着CPU核数增加呈现线性增长:

内核3.9之前的Linux TCP调用

  • kernel 3.9之前的tcp socket实现
  • bind系统调用会将socket和port进行绑定,并加入全局tcp_hashinfo的bhash链表中
  • 所有bind调用都会查询这个bhash链表,如果port被占用,内核会导致bind失败
  • listen则是根据用户设置的队列大小预先为tcp连接分配内存空间
  • 一个应用在同一个port上只能listen一次,那么也就只有一个队列来保存已经建立的连接
  • nginx在listen之后会fork处多个worker,每个worker会继承listen的socket,每个worker会创建一个epoll fd,并将listen fd和accept的新连接的fd加入epoll fd
  • 但是一旦新的连接到来,多个nginx worker只能排队accept连接进行处理
  • 对于大量的短连接,accept显然成为了一个瓶颈

Linux网络堆栈所存在问题

  • TCP处理&多核

    • 一个完整的TCP连接,中断发生在一个CPU核上,但应用数据处理可能会在另外一个核上
    • 不同CPU核心处理,带来了锁竞争和CPU Cache Miss(波动不平衡)
    • 多个进程监听一个TCP套接字,共享一个listen queue队列
    • 用于连接管理全局哈希表格,存在资源竞争
    • epoll IO模型多进程对accept等待,惊群现象
  • Linux VFS的同步损耗严重

    • Socket被VFS管理
    • VFS对文件节点Inode和目录Dentry有同步需求
    • SOCKET只需要在内存中存在即可,非严格意义上文件系统,不需要Inode和Dentry
    • 代码层面略过不必须的常规锁,但又保持了足够的兼容性

Fastsocket所作改进

  1. TCP单个连接完整处理做到了CPU本地化,避免了资源竞争
  2. 保持完整BSD socket API

CPU之间不共享数据,并行化各自独立处理TCP连接,也是其高效的主要原因。其架构图可以看出其改进:

Fastsocket架构图

Fastsocket架构图可以很清晰说明其大致结构,内核态和用户态通过ioctl函数传输。记得netmap在重写网卡驱动里面通过ioctl函数直接透传到用户态中,其更为高效,但没有完整的TCP/IP网络堆栈支持嘛。

Fastsocket的TCP调用图

  • 多个进程可以同时listen在同一个port上
  • 动态链接库libfsocket.so拦截socket、bind、listen等系统调用并进入这个链接库进行处理
  • 对于listen系统调用,fastsocket会记录下这个fd,当应用通过epoll将这个fd加入到epoll fdset中时,libfsocket.so会通过ioctl为该进程clone listen fd关联的socket、sock、file的系统资源
  • 内核模块将clone的socket再次调用bind和listen
  • bind系统调用检测到另外一个进程绑定到已经被绑定的port时,会进行相关检查
  • 通过检查sock将会被记录到port相关联的一个链表中,通过该链表可以知道所有bind同一个port的sock
  • 而sock是关联到fd的,进程则持有fd,那么所有的资源就已经关联到一起
  • 新的进程再次调用listen系统调用的时候,fastsocket内核会再次为其关联的sock分配accept队列
  • 结果是多个进程也就拥有了多个accept队列,可避免cpu cache miss
  • fastsocket提供将每个listen和accept的进程绑定到用户指定的CPU核
  • 如果用户未指定,fastsocket将会为该进程默认绑定一个空闲的CPU核

Fastsocket短连接性能

在新浪测试中,在24核的安装有Centos 6.5的服务器上,借助于Fastsocket,Nginx和HAProxy每秒处理连接数指标(connection/second)性能很惊人,分别增加290%和620%。这也证明了,Fastsocket带来了TCP连接快速处理的能力。 除此之外,借助于硬件特性:

  • 借助于Intel超级线程,可以获得另外20%的性能增长
  • HAProxy代理服务器借助于网卡Flow-Director特性支持,吞吐量可增加15%

Fastsocket V1.0正式版从2014年3月份开始已经在新浪生产环境中使用,用作代理服务器,因此大家可以考虑是否可以采用。针对1.0版本,以下环境较为收益:

  • 服务器至少不少于8个CPU核心
  • 短连接被大量使用
  • CPU周期大部分消耗在网络软中断和套接字系统调用上
  • 应用程序使用基于epoll的非阻塞IO
  • 应用程序使用多个进程单独接受连接

多线程嘛,就得需要参考示范应用所提供实践建议了。

Nginx测试服务器配置

  • nginx工作进程数量设置成CPU核数个
  • http keep-alive特性被禁用
  • 测试端http_load从nginx获取64字节静态文件,并发量为500*CPU核数
  • 启用内存缓存静态文件访问,用于排除磁盘影响
  • 务必禁用accept_mutex(多核访问accept产生锁竞争,另fastsocket内核模块为其去除了锁竞争)

从下表测试图片中,可以看到:

  1. Fastsocket在24核服务器达到了475K Connection/Second,获得了21倍的提升
  2. Centos 6.5在CPU核数增长到12核时并没有呈现线性增长势头,反而在24核时下降到159k CPS
  3. Linux kernel 3.13在24核时获得了近乎两倍于Centos 6.5的吞吐量,283K CPS,但在12核后呈现出扩展性瓶颈

HAProxy重要配置

  • 工作进程数量等同于CPU核数个
  • 需要启用RFD(Receive Flow Deliver)
  • http keep-alive需要禁用
  • 测试端http_load并发量为500*CPU核数
  • 后端服务器响应外围64个字节的消息

测试结果中:

  • fastsocket呈现出了惊人的扩展性能
  • 24核,Linux kernel 3.13成绩为139K CPS
  • 24核,Centos 6.5借助Fastsocket,获得了370K CPS的吞吐量

Fastsocket Throughput

实际部署环境的成绩

Fastsocket Online

8核服务器线上环境运行了24小时的成绩,图a展示了部署fastsocket之前CPU利用率,图b为部署了fastsocekt之后的CPU利用率。 Fastsocket带来的收益:

  • 每个CPU核心负载均衡
  • 平均CPU利用率降低10%
  • HAProxy处理能力增长85%

其实吧,这一块期待新浪公布更多的数据。

长连接的支持正在开发中

长连接支持,还是需要等一等的。但是要支持什么类型长连接?百万级别应用服务器类型,还是redis,可能是后者。虽然目前正做,但目前没有时间表,但目前所做特性总结如下:

  1. 网络堆栈的定制
    • SKB-Pool,每一CPU核对应一个预分配skb pool,替换内核缓冲区kernel slab
      • Percore skb pool
      • 合并skb头部和数据
      • 本地Pool和重复循环使用的Pool(Flow-Director)
    • Fast-Epoll
      • 多进程之间TCP连接共享变得稀少
      • 在file结构体中保存Epoll entry,用以节省调用epoll_ctl时红黑树查询的开销
  2. 跨层的设计
    • Direct-TCP,数据包隶属于已建立套接字会直接跳过路由过程
      • 记录TCP套接字的输入路由信息(Record input route information in TCP socket)
      • 直接查找网络套接字在进入网络堆栈之前(Lookup socket directly before network stack)
      • 从套接字读取输入路由信息(Read input route information from socket)
      • 标记数据包被路有过(Mark the packet as routed)
    • Receive-CPU-Selection 类似于RFS,但更轻巧、精准与快速
      • 把当前CPU核id编码到套接字中(Application marks current CPU id in the socket)
      • 直接查询套接字在进入网络堆栈之前(Lookup socket directly before network stack)
      • 读取套接字中包含的CPU核,然后发送给它(Read CPU id from socket and deliver accordingly)
    • RPS-Framework 数据包在进入网络堆栈之前,让开发者在内核模块之外定制数据包投递规则,扩充RPS功能

Redis测试结果

测试环境:

  • CPU: Intel E5 2640 v2 (6 core) * 2
  • NIC: Intel X520

Redis配置选项:

  • TCP持久连接
  • 8个Redis实例,绑定不同端口
  • 使用到8个CPU核心,并且绑定CPU核

测试结果:

  • 仅开启RSS:20%的吞吐量增加
  • 启用网卡Flow-Director特性:45%吞吐量增加

但需要注意:

  • 仅为实验测试阶段
  • 为V1.0补充,Nginx和HAProxy同样会收益

Fastsocket v1.1

V1.1版本要增加长连接的支持,那么类似于Redis的服务器应用程序就很受益了,因为没有具体的时间表,只能够慢慢等待了。

以后一些优化措施

  1. 在上下文切换时,避免拷贝操作,Zero-Copy
  2. 中断机制完善,减少中断
  3. 支持批量提交,降低系统函数调用
  4. 提交到Linux kernel主分支上去
  5. HugeTLB/HugePage等

Fastsocket和mTCP等简单对比

说是对比,其实是我从mTCP论文中摘取出来,增加了Fastsocket一栏,可以看出人们一直努力的脚步。

Types Accept queue Conn. Locality Socket API Event Handling Packet I/O Application Mod- ification Kernel Modification
PSIO ,
DPDK ,
PF RING ,
netmap
No TCP stack Batched No interface for transport layer No
(NIC driver)
Linux-2.6 Shared None BSD socket Syscalls Per packet Transparent No
Linux-3.9 Per-core None BSD socket Syscalls Per packet Add option SO REUSEPORT No
Affinity-Accept Per-core Yes BSD socket Syscalls Per packet Transparent Yes
MegaPipe Per-core Yes lwsocket Batched syscalls Per packet Event model to completion I/O Yes
FlexSC,VOS Shared None BSD socket Batched syscalls Per packet Change to use new API Yes
mTCP Per-core Yes User-level socket Batched function calls Batched Socket API to mTCP API No
(NIC driver)
Fastsocket Per-core Yes BSD socket Ioctl + kernel calls Per packet Transparent No

有一个大致的印象,也方便对比,但这只能是一个暂时的摘要而已,人类对性能的渴求总是朝着更好的方向发展着。

部署尝试

怎么说呢,Fastsocket是为大家耳熟能详服务器程序Nginx,HAProxy等而开发的。但若应用环境为大量的短连接,并且是小文件类型请求,不需要强制支持Keep-alive特性(短连接要的是快速请求-相应,然后关闭),那么管理员可以尝试一下Fastsocket,至于部署策略,选择性部署几台作为实验看看结果。

小结

本系列到此算是告一段落啦。以后呢,自然是希望Fastsocket尽快发布对长连接的支持,还有更高性能的提升咯 :))

资源引用



nieyong 2015-02-05 15:21 发表评论
04 Feb 06:35

20条Linux命令面试问答

by scsecrystal

问:1 如何查看当前的Linux服务器的运行级别?

答: ‘who -r’ 和 ‘runlevel’ 命令可以用来查看当前的Linux服务器的运行级别。

问:2 如何查看Linux的默认网关?

答: 用 “route -n” 和 “netstat -nr” 命令,我们可以查看默认网关。除了默认的网关信息,这两个命令还可以显示当前的路由表。

问:3 如何在Linux上重建初始化内存盘镜像文件?

答: 在CentOS 5.X / RHEL 5.X中,可以用mkinitrd命令来创建初始化内存盘文件,举例如下:

# mkinitrd -f -v /boot/initrd-$(uname -r).img $(uname -r)

如果你想要给特定的内核版本创建初始化内存盘,你就用所需的内核名替换掉 ‘uname -r’ 。

在CentOS 6.X / RHEL 6.X中,则用dracut命令来创建初始化内存盘文件,举例如下:

# dracut -f

以上命令能给当前的系统版本创建初始化内存盘,给特定的内核版本重建初始化内存盘文件则使用以下命令:

# dracut -f initramfs-2.x.xx-xx.el6.x86_64.img 2.x.xx-xx.el6.x86_64

问:4 cpio命令是什么?

答: cpio就是复制入和复制出的意思。cpio可以向一个归档文件(或单个文件)复制文件、列表,还可以从中提取文件。

问:5 patch命令是什么?如何使用?

答: 顾名思义,patch命令就是用来将修改(或补丁)写进文本文件里。patch命令通常是接收diff的输出并把文件的旧版本转换为新版本。举个例子,Linux内核源代码由百万行代码文件构成,所以无论何时,任何代码贡献者贡献出代码,只需发送改动的部分而不是整个源代码,然后接收者用patch命令将改动写进原始的源代码里。

创建一个diff文件给patch使用,

# diff -Naur old_file new_file > diff_file

旧文件和新文件要么都是单个的文件要么都是包含文件的目录,-r参数支持目录树递归。

一旦diff文件创建好,我们就能在旧的文件上打上补丁,把它变成新文件:

# patch < diff_file

问:6 aspell有什么用 ?

答: 顾名思义,aspell就是Linux操作系统上的一款交互式拼写检查器。aspell命令继任了更早的一个名为ispell的程序,并且作为一款免费替代品 ,最重要的是它非常好用。当aspell程序主要被其它一些需要拼写检查能力的程序所使用的时候,在命令行中作为一个独立运行的工具的它也能十分有效。

问:7 如何从命令行查看域SPF记录?

答: 我们可以用dig命令来查看域SPF记录。举例如下:

linuxtechi@localhost:~$ dig -t TXT google.com

问:8 如何识别Linux系统中指定文件(/etc/fstab)的关联包?

答:

# rpm -qf /etc/fstab

以上命令能列出提供“/etc/fstab”这个文件的包。

问:9 哪条命令用来查看bond0的状态?

答:

cat /proc/net/bonding/bond0

问:10 Linux系统中的/proc文件系统有什么用?

答: /proc文件系统是一个基于内存的文件系统,其维护着关于当前正在运行的内核状态信息,其中包括CPU、内存、分区划分、I/O地址、直接内存访问通道和正在运行的进程。这个文件系统所代表的并不是各种实际存储信息的文件,它们指向的是内存里的信息。/proc文件系统是由系统自动维护的。

问:11 如何在/usr目录下找出大小超过10MB的文件?

答:

# find /usr -size +10M

问:12 如何在/home目录下找出120天之前被修改过的文件?

答:

# find /home -mtime +120

问:13 如何在/var目录下找出90天之内未被访问过的文件?

答:

# find /var \! -atime -90

问:14 在整个目录树下查找文件“core”,如发现则无需提示直接删除它们。

答:

# find / -name core -exec rm {} \;

问:15 strings命令有什么作用?

答: strings命令用来提取和显示非文本文件中的文本字符串。(LCTT 译注:当用来分析你系统上莫名其妙出现的二进制程序时,可以从中找到可疑的文件访问,对于追查入侵有用处)

问:16 tee 过滤器有什么作用 ?

答: tee 过滤器用来向多个目标发送输出内容。如果用于管道的话,它可以将输出复制一份到一个文件,并复制另外一份到屏幕上(或一些其它程序)。

linuxtechi@localhost:~$ ll /etc | nl | tee /tmp/ll.out

在以上例子中,从ll输出可以捕获到 /tmp/ll.out 文件中,并且同样在屏幕上显示了出来。

问:17 export PS1 = ”$LOGNAME@hostname:\$PWD: 这条命令是在做什么?

答: 这条export命令会更改登录提示符来显示用户名、本机名和当前工作目录。

问:18 ll | awk ‘{print $3,”owns”,$9}’ 这条命令是在做什么?

答: 这条ll命令会显示这些文件的文件名和它们的拥有者。

问:19 :Linux中的at命令有什么用?

答: at命令用来安排一个程序在未来的做一次一次性执行。所有提交的任务都被放在 /var/spool/at 目录下并且到了执行时间的时候通过atd守护进程来执行。

问:20 linux中lspci命令的作用是什么?

答: lspci命令用来显示你的系统上PCI总线和附加设备的信息。指定-v,-vv或-vvv来获取越来越详细的输出,加上-r参数的话,命令的输出则会更具有易读性。

20条Linux命令面试问答,首发于博客 - 伯乐在线

03 Feb 00:53

给 Git 中级用户的 25 个小贴士

by kinolee

Andy Jeffries 给 Git 中级用户总结分享的 25 个小贴士。你不需要去做大量搜索,或许这些小贴士对你就很有帮助的。

我从开始使用git到现在已经差不多18个月了,以为自己已经很懂git了。直到我看到github上 Scott Chacon在 LVS, a supplier/developer of betting/gaming software 上的教学,第一天就受益匪浅。

作为一个很享受git的人,我想要分享从各种社区学到的实用经验,让大家不需要花费过多的功夫就能找到答案。

基本技巧

1.安装后的第一步

安装git后,第一件事你需要设置你的名字和邮箱,因为每次提交都需要这些信息。

$ git config --global user.name "Some One"
$ git config --global user.email "someone@gmail.com"

2.是基于指针的

git上的所有东西都是储存在文件里的,当你创建一次提交时,它会创建一个包含你的提交信息和相关数据(名字,邮箱,日期/时间、上一次提交等等)的文件并连接一个树文件,而这个树文件包含了对象列表或者其他树。这上面的对象或者blob文件就是这次提交的实际内容(你可以认为这也是一个文件,尽管并没有储存在对象里而是储存在树中)。所有的文件都以经过SHA-1计算后的文件名(译者注:经过SHA-1计算后的数,即git中的版本号)储存在上面。

从这里可以看出,分支和标签都是包含一个指向这次提交的sha-1数(版本号)简单的文件,这样使用引用会变得更快和更灵活,创建一个新的分支是就像创建文件一样简单,SHA – 1数(版本号)也会引用你这个分支的提交。当然,如果你使用GIT命令行工具(或者GUI)你将无法接触这些。但真的很简单。

你可能听说过HEAD引用,这是一个指向你当前提交的内容的SHA-1 数(版本号)的指针。如果你正在解决合并冲突,使用HEAD不会对你的特定分支有任何改动只会指向你当前的分支。

所有分支的指针都保存在 .git/refs/heads,HEAD指针保存在.git/HEAD,标签则保存在 .git/refs/tags,有时间就去看看吧。

3.  两个母体(Parent),当然!

当我们在日志文件中查看合并提交信息,你会看到两个母体,第一个母体是正在进行的分支,第二个是你要合并的分支。

4.合并冲突

现在,我发现有合并冲突并解决了它,这是一件在我们编辑文件时很正常的事。将 <<<<, ====, >>>> 这些标记移除后,并保存你想要保存的代码。有些时候在代码被直接替代之前,能看到冲突是件挺不错的事。比如在两个冲突的分支变动之前,可以用这样的命令方式:

$ git diff --merge
diff --cc dummy.rb  
index 5175dde,0c65895..4a00477  
--- a/dummy.rb
+++ b/dummy.rb
@@@ -1,5 -1,5 +1,5 @@@
  class MyFoo
    def say
-     puts "Bonjour"
 -    puts "Hello world"
++    puts "Annyong Haseyo"
    end
  end

If the file is binary, diffing files isn’t so easy… What you’ll normally want to do is to try each version of the binary file and decide which one to use (or manually copy portions over in the binary file’s editor). To pull a copy of the file from a particular branch (say you’re merging master and feature132):

如果是二进制文件(binary),区别这些文件并不容易。通常你会查看每个二进制文件的版本,再决定使用哪个(或者在二进制文件编辑器中手动复制),并将其推送至特定的分支。(比如你要合并master和feature132)

$ git checkout master flash/foo.fla # or...
$ git checkout feature132 flash/foo.fla
$ # Then...
$ git add flash/foo.fla

Another way is to cat the file from git – you can do this to another filename then copy the correct file over (when you’ve decided which it is) to the normal filename:

另一个方法就是在git中cat文件,你可以将其命名为另一个文件名,然后将你决定的那个文件改为正确的文件名:

$ git show master:flash/foo.fla > master-foo.fla
$ git show feature132:flash/foo.fla > feature132-foo.fla
$ # Check out master-foo.fla and feature132-foo.fla
$ # Let's say we decide that feature132's is correct
$ rm flash/foo.fla
$ mv feature132-foo.fla flash/foo.fla
$ rm master-foo.fla
$ git add flash/foo.fla

更新:感谢carls在原博评论中提醒我,可以使用 “git checkout —ours flash/foo.fla” 和“git checkout —theirs flash/foo.fla” 在不用考虑你需要合并的分支来检查指定版本,就我个人而言,我喜欢更明确的方法,但这也是一个选择…

记住,解决完合并冲突后要添加文件。(我之前就犯过这样的错误)

服务,分支和标注

5. 远程服务

Git有一个非常强大的特性,就是可以有多个远程服务端(以及你运行的一个本地仓库)。你不需要总是进行访问,你可以有多个服务端并能从其中一个(合并工作)读取再写入另一个。添加一个远程服务端很简单:

$ git remote add john git@github.com:johnsomeone/someproject.git

If you want to see information about your remote servers you can do:

如果你想查看远程服务端的信息你可以:

# shows URLs of each remote server
$ git remote -v 

# gives more details about each
$ git remote show name 

You can always see the differences between a local branch and a remote branch:

你总是能看到本地分支和远程分支不同的地方:

$ git diff master..john/master

You can also see the changes on HEAD that aren’t on that remote branch:

你同样也能看到远程分支上没有的HEAD指针的改动:

$ git log remote/branch..
# Note: no final refspec after ..

6. Tagging  标签

在Git中有两种类型的标注:轻量级标注和注释型标注。

记住第二个是Git的指针基础,两者区别很简单,轻量级标注是简单命名提交的指针,你可以将其指向另一个提交。注释型标注是一个有信息和历史并指向标注对象的名字指针,它有着自己的信息,如果需要的话,可以进行GPG标记。

创建两种类型的标签很简单(其中一个命令行有改动)

$ git tag to-be-tested
$ git tag -a v1.1.0 # Prompts for a tag message

7. Creating Branches 创建分支

在git中创建分支是件非常简单的事情(非常快并只需要不到100byte的文件大小)。创建新分支并切换到该分支,通常是下面这样的:

$ git branch feature132
$ git checkout feature132

当然,如果你想切换到该分支,最直接的方式是使用这样一条命令:

$ git checkout -b feature132

如果你想要重新命名本地分支,也很简单:

$ git checkout -b twitter-experiment feature132
$ git branch -d feature132

更新:或者你(Brian Palmer在原博的评论中指出的)可以使用 -m来切换到“git branch”(就像Mike指出,如果你只需要一个特定的分支,就可以重命名当前分支)

$ git branch -m twitter-experiment
$ git branch -m feature132 twitter-experiment

8.合并分支

以后你可能回想合并你的变动,有两种方式可以做到这一点:

$ git checkout master
$ git merge feature83 # Or...
$ git rebase feature83

merge和rebase的区别是,merge会尝试解决改动并创建的新的提交来融合他们。rebase则是将从你最后一次从另一个分支分离之后的改动并入,并直接沿用另一个分支的head指针。尽管如此,在你往远端服务器上推送分支之前,不要使用rebase。这会让你混乱。

如果你不能确定哪个分支(哪些需要合并,哪些需要移除)。这里有两个git分支切换方式来帮助你:

# Shows branches that are all merged in to your current branch
$ git branch --merged

# Shows branches that are not merged in to your current branch
$ git branch --no-merged

9.远程分支

如果你想将本地分支放置远程服务端,你可以用这条命令进行推送:

$ git push origin twitter-experiment:refs/heads/twitter-experiment
# Where origin is our server name and twitter-experiment is the branch

如果你想要从服务端删除分支:

$ git push origin :twitter-experiment

如果你想要查看远程分支的状态:

$ git remote show origin

这将列出那些曾经存在而现在不存在的远程分支,这将帮助你轻易地删除你本地多余的分支。

$ git remote prune

最后,如果本地追踪远程分支,常用方式是:

$ git branch --track myfeature origin/myfeature
$ git checkout myfeature

尽管这样,Git的新版本将启动自动追踪,如果你使用-b来checkout:

$ git checkout -b myfeature origin/myfeature

Storing Content in Stashes, Index and File System 在stash储存内容、索引和文件系统

10. Stashing

在Git中你可以将当前的工作区的内容保存到Git栈中并从最近的一次提交中读取相关内容。以下是个简单的例子:

$ git stash
# Do something...
$ git stash pop

很多人推荐使用git stash apply来代替pop。这样子恢复后储存的stash内容并不会删除,而‘pop’恢复的同时把储存的stash内容也删了 ,使用git stash apply 就可以移除任何栈中最新的内容。

<code data-language="javascript">$ git stash drop
</code>

git可以自动创建基于当前提交信息的指令,如果你更喜欢使用通用的信息(相当于不会对前一次提交做任何改动)

<code data-language="javascript">$ git stash save "My stash message"
</code>

如果你想使用某个stash(不一定是最后一个),你可以这样将其列表显示出来然后使用:

<code data-language="javascript">$ git stash list
  stash@{0}: On master: Changed to German
  stash@{1}: On master: Language is now Italian
$ git stash apply stash@{1}
</code>

11.添加交互

在svn中,如果你文件有了改动之后,然后会提交所有改动的文件,在 Git中为了能更好的提交特定的文件或者某个补丁,你需要在交互模式提交选择提交的文件的内容。

$ git add -i
staged     unstaged path

*** Commands ***
  1: status      2: update   3: revert   4: add untracked
  5: patch      6: diff     7: quit     8: help
What now&gt;

这是基于菜单的交互式提示符。您可以使用命令前的数字或进入高亮字母(如果你有高亮输入)模式。常用形式是,输入你想执行的操作前的数字。(你可以像1或1 – 4或2、4、7的格式来执行命令)。

如果你想进入补丁模式(在交互模式中输入p或5),同样也可以这样操作:

$ git add -p    
diff --git a/dummy.rb b/dummy.rb  
index 4a00477..f856fb0 100644  
--- a/dummy.rb
+++ b/dummy.rb
@@ -1,5 +1,5 @@
 class MyFoo
   def say
-    puts "Annyong Haseyo"
+    puts "Guten Tag"
   end
 end
Stage this hunk [y,n,q,a,d,/,e,?]?

如你所见,你将在选择添加改动的那部分文件的底部获得一些选项。此外,使用“?”会说明这个选项。

12. 文件系统中的储存/检索

有些项目(比如Git自己的项目)需要直接在Git的文件系统中添加额外的并不想被检查的文件。

让我们开始在Git中保存随机文件

$ echo "Foo" | git hash-object -w --stdin
51fc03a9bb365fae74fd2bf66517b30bf48020cb

比如数据库中的对象,如果你不想让一些对象被垃圾回收,最简单的方式是给它加标签:

$ git tag myfile 51fc03a9bb365fae74fd2bf66517b30bf48020cb

在这里我们设置myfile的标签,当我们需要检索该文件时可以这样:

$ git cat-file blob myfile

这对开发者可能需要的但是并不想每次都去检查的有用文件(密码,gpg键等等)很管用(特别是在生产过程中)。

Logging and What Changed? 记录日志和什么改变了?

13. 查看日志

在不使用“git log”的情况下,你不能查看你长期的最近提交内容,但是,仍然有一些更便于你使用的方法,比如,你可以这样查看单次提交变动的内容:

$ git log -p

或者你只看文件变动的摘要:

$ git log --stat

这个很赞的别名,可以让你在一行命令下简化提交,并展示不错的图形化分支。

$ git config --global alias.lol "log --pretty=oneline --abbrev-commit --graph --decorate"
$ git lol
* 4d2409a (master) Oops, meant that to be in Korean
* 169b845 Hello world

14.在日志中查找

如果你想根据指定的作者查找:

$ git log --author=Andy

更新:感谢 Johannes的评论,解除了我的一些困惑,

或者你可以搜索你提交信息的内容:

$ git log --grep="Something in the message"

这些强大的指令被称为pickaxe指令,来检查被移除或添加特定块的内容(比如,当他们第一次出现或者被移除),添加任何一行内容都会告诉你(但是并不包括那行内容刚刚被改动)

$ git log -S "TODO: Check for admin status"

如果你改动一个特定的文件会怎么样?如:lib/foo.rb

$ git log lib/foo.rb

如果你有feature/132 和ferature/145这两个分支,并想查看这些不在master上的分支内容。( ^ 符号是意味着非)

$ git log feature/132 feature/145 ^master

你同样可以使用ActiveSupport风格的日期来缩短时间范围:

$ git log --since=2.months.ago --until=1.day.ago

默认会使用OR来合并查询,但你也可改用AND(如果你有不止一个条件)

$ git log --since=2.months.ago --until=1.day.ago --author=andy -S "something" --all-match

15.选择试图/改动的之前的版本。

根据你知道的信息,可以按照以下方式来找到之前的版本:

$ git show 12a86bc38 # By revision
$ git show v1.0.1 # By tag
$ git show feature132 # By branch name
$ git show 12a86bc38^ # Parent of a commit
$ git show 12a86bc38~2 # Grandparent of a commit
$ git show feature132@{yesterday} # Time relative
$ git show feature132@{2.hours.ago} # Time relative

注意:不像前一部分所说,在最后的插入符号意味着提交的父类,在前面的插入符号意味着不在这个分支上。

16. 选择一个方式

最简单的方式:

$ git log origin/master..new
# [old]..[new] - everything you haven't pushed yet

你也可以省略[new],这样将默认使用当前的HEAD指针。

Rewinding Time & Fixing Mistakes 回滚和修复错误

17.重置更改

如果你没有提交你可以简单的撤销改动:

$ git reset HEAD lib/foo.rb

通常我们使用”unstage“这样的别名来代替:

$ git config --global alias.unstage "reset HEAD"
$ git unstage lib/foo.rb

如果你已经提交了,有两种情况:如果是最后一次提交你仅仅需要amend:

$ git commit --amend

这将不执行最后一次提交,恢复你原来的内容,提交信息将默认为你下次提交的信息。

如果你已经提交过不止一次了并且想完全回到之前那个记录,你可以重置分支回到指定的时间。

$ git checkout feature132
$ git reset --hard HEAD~2

如果你想将分支回滚但想要SHA1数(版本号)不一样(也许你可以将分支的HEAD指向另一个分支,或者之后的提交),你可以通过如下方式:

$ git checkout FOO
$ git reset --hard SHA

实际上还有个更快的方式(这样并不会改变你的文件复制内容,并回归到第一次FOO的状态并指向SHA)

$ git update-ref refs/heads/FOO SHA

18. 提交至错误的分支

好吧,假定你提交到master上了,但是你想提交的是名为experimental的主题分支上,如果想移除这个改动,你可以在当前创建一个分支并将head指针回滚再检查新的分支

$ git branch experimental   # Creates a pointer to the current master state
$ git reset --hard master~3 # Moves the master branch pointer back to 3 revisions ago
$ git checkout experimental

如果你在分支的分支的分支进行了改动将会很麻烦,那么你需要做的就是在其他处进行分支rebase改动

$ git branch newtopic STARTPOINT
$ git rebase oldtopic --onto newtopic

19. rebase的交互

这是个很不错的功能,我曾看过演示但一直以来并没有真正搞懂,现在我知道了,非常简单。假如你进行了三次提交,但是你想重新编辑它们(或者结合它们)。

$ git rebase -i master~3

然后你让你的编辑器打开一些指令,你需要做的就是修改指令来选择/squash/编辑(或删除)/提交和保存/退出,编辑完使用git rebase —continue 来通过你的每一个指令。

如果你选择编辑一个,它将离开你的提交状态,所以你需要使用git commit -amend来编辑它。

注意:不要在rebase的时候提交——只能添加了之后再使用—continue, —skip 或—abort.

20. 清除

如果你在分支中提交了一些内容(也许是一些SVN上老的资源文件)并想从历史记录中完全移除,可以这样:

$ git filter-branch --tree-filter 'rm -f *.class' HEAD

如果你已经将其推送至origin,并提交了一些垃圾内容,你同样可以推送之前在本地系统这样做:

$ git filter-branch --tree-filter 'rm -f *.class' origin/master..HEAD

Miscellaneous Tips 各种各样的技巧

21.你看过的前面的引用

如果你知道你之前看到的SHA-1数(版本号),并需要进行一些重置/回滚,可以使用reflog命令查询最近查看的sha – 1数(版本号):

$ git reflog
$ git log -g # Same as above, but shows in 'log' format

22. 分支命名

一个有趣的小技巧,不要忘记分支名不仅仅限于a-z和0-9,在名字中使用/和.用于命名伪命名空间和版本控制,也是个不错的主意,例如:

$ # Generate a changelog of Release 132
$ git shortlog release/132 ^release/131
$ # Tag this as v1.0.1
$ git tag v1.0.1 release/132

23. 找到Dunnit

找出谁在一个文件中改变了一行代码,简单的命令是:

$ git blame FILE

有时候是上一个文件发生了变动(如果你合并两个文件,或者你已经转移到一个函数),这样你就可以使用:

$ # shows which file names the content came from
$ git blame -C FILE

有时候需要通过点击来追踪来回的变动,这里有一个不错的内置gui:

$ git gui blame FILE

24. 数据库维护

通常Git并不需要过多的维护,它几乎可以自己搞定,尽管如此你也可以查看数据库使用的统计:

$ git count-objects -v

如果数值过高你可以选择将你的克隆垃圾回收。这不会影响你推送内容或其他人,但它可以让你的命令运行的更快,并使用更少的空间:

$ git gc

它也可以在运行时进行一致性检验:

$ git fsck --full

你可以在后面添加-auto 参数(如果你在服务器跑定时任务时),这在统计数据时是必须的。

当检查的结果是“dangling”或“unreachable”这样的是正常的,这通常是回滚和rebase的结果。 得到“missing” 或 “sha1 mismatch” 这样的结果是不好的…你需要得到专业的帮助!

25. 恢复失去的分支

如果你意外的删除一个分支,可以重新创建它:

$ git branch experimental SHA1_OF_HASH

你可以使用git reflog查看你最近访问过的SHA1数(版本号)

另一个方式就是使用 git fsck —lost-found ,悬空对象(dangling commit )是就是失去HEAD指针的提交,(删除的分支只是失去了HEAD指针成为悬空对象)

Done!完成!

这篇是我写过最长的博文,希望大家能从此文中获益,如果你有所收益或是有任何问题都可以在评论中告诉我!



给 Git 中级用户的 25 个小贴士,首发于博客 - 伯乐在线

02 Feb 11:22

C++ named operators

02 Feb 11:08

Awk 20 分钟入门介绍

by 进林

什么是Awk

Awk是一种小巧的编程语言及命令行工具。(其名称得自于它的创始人Alfred Aho、Peter Weinberger 和 Brian Kernighan姓氏的首个字母)。它非常适合服务器上的日志处理,主要是因为Awk可以对文件进行操作,通常以可读文本构建行。

我说它适用于服务器是因为日志文件,转储文件(dump files),或者任意文本格式的服务器终止转储到磁盘都会变得很大,并且在每个服务器你都会拥有大量的这类文件。如果你经历过这样的情境——在没有像Splunk或者其他等价的工具情况下不得不在50个不同的服务器里分析几G的文件,你会觉得去获取和下载所有的这些文件并分析他们是一件很糟糕的事。

我亲身经历过这种情境。当一些Erlang节点将要死掉并留下一个700MB到4GB的崩溃转储文件(crash dump)时,或者当我需要在一个小的个人服务器(叫做VPS)上快速浏览日志,查找一个常规模式时。

在任何情况下,Awk都不仅仅只是用来查找数据的(否则,grep或者ack已经足够使用了)——它同样使你能够处理数据并转换数据。

代码结构

Awk脚本的代码结构很简单,就是一系列的模式(pattern)和行为(action):

# comment
Pattern1 { ACTIONS; }

# comment
Pattern2 { ACTIONS; }

# comment
Pattern3 { ACTIONS; }

# comment
Pattern4 { ACTIONS; }

扫描文档的每一行时都必须与每一个模式进行匹配比较,而且一次只匹配一个模式。那么,如果我给出一个包含以下内容的文件:

this is line 1
this is line 2

this is line 1 这行就会与Pattern1进行匹配。如果匹配成功,就会执行ACTIONS。然后this is line 1 会和Pattern2进行匹配。如果匹配失败,它就会跳到Pattern3进行匹配,以此类推。

一旦所有的模式都匹配过了,this is line 2 就会以同样的步骤进行匹配。其他的行也一样,直到读取完整个文件。

简而言之,这就是Awk的运行模式

数据类型

Awk仅有两个主要的数据类型:字符串和数字。即便如此,Awk的字符串和数字还可以相互转换。字符串能够被解释为数字并把它的值转换为数字值。如果字符串不包含数字,它就被转换为0.

它们都可以在你代码里的ACTIONS部分使用 = 操作符给变量赋值。我们可以在任意时刻、任意地方声明和使用变量,也可以使用未初始化的变量,此时他们的默认值是空字符串:“”。

最后,Awk有数组类型,并且它们是动态的一维关联数组。它们的语法是这样的:var[key] = value 。Awk可以模拟多维数组,但无论怎样,这是一个大的技巧(big hack)。

模式

可以使用的模式分为三大类:正则表达式、布尔表达式和特殊模式。

正则表达式和布尔表达式

你使用的Awk正则表达式比较轻量。它们不是Awk下的PCRE(但是gawk可以支持该库——这依赖于具体的实现!请使用 awk
–version查看),然而,对于大部分的使用需求已经足够了:

/admin/ { ... } # any line that contains 'admin'
/^admin/ { ... } # lines that begin with 'admin'
/admin$/ { ... } # lines that end with 'admin'
/^[0-9.]+ / { ... } # lines beginning with series of numbers and periods
/(POST|PUT|DELETE)/ # lines that contain specific HTTP verbs

注意,模式不能捕获特定的组(groups)使它们在代码的ACTIONS部分执行。模式是专门匹配内容的。

布尔表达式与PHP或者Javascript中的布尔表达式类似。特别的是,在awk中可以使用&&(“与”)、||(“或”)、!(“非”)操作符。你几乎可以在所有类C语言中找到它们的踪迹。它们可以对常规数据进行操作。

与PHP和Javascript更相似的特性是比较操作符,==,它会进行模糊匹配(fuzzy matching)。因此“23”字符串等于23,”23″ == 23 表达式返回true。!= 操作符同样在awk里使用,并且别忘了其他常见的操作符:>,<,>=,和<=。

你同样可以混合使用它们:布尔表达式可以和常规表达式一起使用。 /admin/ || debug == true 这种用法是合法的,并且在遇到包含“admin”单词的行或者debug变量等于true时该表达式就会匹配成功。

注意,如果你有一个特定的字符串或者变量要与正则表达式进行匹配,~ 和!~ 就是你想要的操作符。 这样使用它们:string ~ /regex/ 和 string !~ /regex/。

同样要注意的是,所有的模式都只是可选的。一个包含以下内容的Awk脚本:

{ ACTIONS }

对输入的每一行都将会简单地执行ACTIONS。

特殊的模式

在Awk里有一些特殊的模式,但不是很多。

第一个是BEGIN,它仅在所有的行都输入到文件之前进行匹配。这是你可以初始化你的脚本变量和所有种类的状态的主要地方。

另外一个就是END。就像你可能已经猜到的,它会在所有的输入都被处理完后进行匹配。这使你可以在退出前进行清除工作和一些最后的输出。

最后一类模式,要把它进行归类有点困难。它处于变量和特殊值之间,我们通常称它们为域(Field)。而且名副其实。

使用直观的例子能更好地解释域:

# According to the following line
#
# $1 $2 $3
# 00:34:23 GET /foo/bar.html
# _____________ _____________/
# $0

# Hack attempt?
/admin.html$/ && $2 == "DELETE" {
print "Hacker Alert!";
}

域(默认地)由空格分隔。$0 域代表了一整行的字符串。 $1 域是第一块字符串(在任何空格之前), $2 域是后一块,以此类推。

一个有趣的事实(并且是在大多是情况下我们要避免的事情),你可以通过给相应的域赋值来修改相应的行。例如,如果你在一个块里执行 $0 = “HAHA THE LINE IS GONE”,那么现在下一个模式将会对修改后的行进行操作而不是操作原始的行。其他的域变量都类似。

行为

这里有一堆可用的行为(possible actions),但是最常用和最有用的行为(以我的经验来说)是:

{ print $0; } # prints $0. In this case, equivalent to 'print' alone
{ exit; } # ends the program
{ next; } # skips to the next line of input
{ a=$1; b=$0 } # variable assignment
{ c[$1] = $2 } # variable assignment (array)

{ if (BOOLEAN) { ACTION }
else if (BOOLEAN) { ACTION }
else { ACTION }
}
{ for (i=1; i<x; i++) { ACTION } }
{ for (item in c) { ACTION } }

这些内容将会成为你的Awk工具箱的主要工具,在你处理日志之类的文件时你可以随意地使用它们。

Awk里的变量都是全局变量。无论你在给定的块里定义什么变量,它对其他的块都是可见的,甚至是对每一行都是可见的。这严重限制了你的Awk脚本大小,不然他们会造成不可维护的可怕结果。请编写尽可能小的脚本。

函数

可以使用下面的语法来调用函数:

{ somecall($2) }

这里有一些有限的内置函数可以使用,所以我可以给出这些函数的通用文档(regular documentation)

用户定义的函数同样很简单:

# function arguments are call-by-value
function name(parameter-list) {
ACTIONS; # same actions as usual
}

# return is a valid keyword
function add1(val) {
return val+1;
}

特殊变量

除了常规变量(全局的,可以在任意地方使用),这里还有一系列特殊的变量,它们的的作用有点像配置条目(configuration entries):

BEGIN { # Can be modified by the user
FS = ","; # Field Separator
RS = "n"; # Record Separator (lines)
OFS = " "; # Output Field Separator
ORS = "n"; # Output Record Separator (lines)
}
{ # Can't be modified by the user
NF # Number of Fields in the current Record (line)
NR # Number of Records seen so far
ARGV / ARGC # Script Arguments
}

我把可修改的变量放在BEGIN里,因为我更喜欢在那重写它们。但是这些变量的重写可以放在脚本的任意地方然后在后面的行里生效。

示例

以上的就是Awk语言的核心内容。我这里没有大量的例子,因为我趋向于使用Awk来完成快速的一次性任务。

不过我依然有一些随身携带的脚本文件,用来处理一些事情和测试。我最喜欢的一个脚本是用来处理Erlang的崩溃转储文件,形如下面的:

=erl_crash_dump:0.3
Tue Nov 18 02:52:44 2014
Slogan: init terminating in do_boot ()
System version: Erlang/OTP 17 [erts-6.2] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false]
Compiled: Fri Sep 19 03:23:19 2014
Taints:
Atoms: 12167
=memory
total: 19012936
processes: 4327912
processes_used: 4319928
system: 14685024
atom: 339441
atom_used: 331087
binary: 1367680
code: 8384804
ets: 382552
=hash_table:atom_tab
size: 9643
used: 6949
...
=allocator:instr
option m: false
option s: false
option t: false
=proc:<0.0.0>
State: Running
Name: init
Spawned as: otp_ring0:start/2
Run queue: 0
Spawned by: []
Started: Tue Nov 18 02:52:35 2014
Message queue length: 0
Number of heap fragments: 0
Heap fragment data: 0
Link list: [<0.3.0>, <0.7.0>, <0.6.0>]
Reductions: 29265
Stack+heap: 1598
OldHeap: 610
Heap unused: 656
OldHeap unused: 468
Memory: 18584
Program counter: 0x00007f42f9566200 (init:boot_loop/2 + 64)
CP: 0x0000000000000000 (invalid)
=proc:<0.3.0>
State: Waiting
...
=port:#Port<0.0>
Slot: 0
Connected: <0.3.0>
Links: <0.3.0>
Port controls linked-in driver: efile
=port:#Port<0.14>
Slot: 112
Connected: <0.3.0>
...

产生下面的结果:

$ awk -f queue_fun.awk $PATH_TO_DUMP
MESSAGE QUEUE LENGTH: CURRENT FUNCTION
======================================
10641: io:wait_io_mon_reply/2
12646: io:wait_io_mon_reply/2
32991: io:wait_io_mon_reply/2
2183837: io:wait_io_mon_reply/2
730790: io:wait_io_mon_reply/2
80194: io:wait_io_mon_reply/2
...

这是在Erlang进程里运行的函数列表,它们导致了mailboxe变得很庞大。脚本在这:

# Parse Erlang Crash Dumps and correlate mailbox size to the currently running
# function.
#
# Once in the procs section of the dump, all processes are displayed with
# =proc:<0.M.N> followed by a list of their attributes, which include the
# message queue length and the program counter (what code is currently
# executing).
#
# Run as:
#
# $ awk -v threshold=$THRESHOLD -f queue_fun.awk $CRASHDUMP
#
# Where $THRESHOLD is the smallest mailbox you want inspects. Default value
# is 1000.
BEGIN {
if (threshold == "") {
threshold = 1000 # default mailbox size
}
procs = 0 # are we in the =procs entries?
print "MESSAGE QUEUE LENGTH: CURRENT FUNCTION"
print "======================================"
}

# Only bother with the =proc: entries. Anything else is useless.
procs == 0 && /^=proc/ { procs = 1 } # entering the =procs entries
procs == 1 && /^=/ && !/^=proc/ { exit 0 } # we're done

# Message queue length: 1210
# 1 2 3 4
/^Message queue length: / && $4 >= threshold { flag=1; ct=$4 }
/^Message queue length: / && $4 < threshold { flag=0 }

# Program counter: 0x00007f5fb8cb2238 (io:wait_io_mon_reply/2 + 56)
# 1 2 3 4 5 6
flag == 1 && /^Program counter: / { print ct ":", substr($4,2) }

你跟上思路没?如果跟上了,你已经了解了Awk。恭喜!

Awk 20 分钟入门介绍,首发于博客 - 伯乐在线