Archive for 一月, 2015

 

/**
 * @file pb.cpp
 *
 * dynamic1:
 * step1: Generate proto schema dynamical
 * step2: Set field value using reflection
 *
 * dynamic2:
 * step1: Load proto schema from file
 * step2: Set field value using reflection
 *
 * g++ -I/path/to/protobuf/include/ -L/path/to/protobuf/lib pb.cpp -lprotobuf -lpthread
 **/

#include <iostream>
#include <google/protobuf/descriptor.h>
#include <google/protobuf/descriptor.pb.h>
#include <google/protobuf/dynamic_message.h>
#include <google/protobuf/compiler/importer.h> // only dynamic2() need

using namespace std;
using namespace google::protobuf;
using namespace google::protobuf::compiler; // only dynamic2() need

static void dynamic1()
{
 FileDescriptorProto file_proto;
 file_proto.set_name("foo.proto"); // foo.proto file needn't really exists

 // create dynamic message proto names "Pair"
 DescriptorProto *message_proto = file_proto.add_message_type();
 message_proto->set_name("Pair"); 

 FieldDescriptorProto *field_proto = NULL;

 field_proto = message_proto->add_field(); // add first field, and definition
 field_proto->set_name("key");
 field_proto->set_type(FieldDescriptorProto::TYPE_STRING);
 field_proto->set_number(1);
 field_proto->set_label(FieldDescriptorProto::LABEL_REQUIRED);

 field_proto = message_proto->add_field(); // add second field, and definition
 field_proto->set_name("value");
 field_proto->set_type(FieldDescriptorProto::TYPE_UINT32);
 field_proto->set_number(2);
 field_proto->set_label(FieldDescriptorProto::LABEL_REQUIRED);

 // add the "Pair" message proto to file proto and build it
 DescriptorPool pool;
 const FileDescriptor *file_descriptor = pool.BuildFile(file_proto);
 const Descriptor *descriptor = file_descriptor->FindMessageTypeByName("Pair");
 cout << descriptor->DebugString();

 // build a dynamic message by "Pair" proto
 DynamicMessageFactory factory(&pool); const Message *message = factory.GetPrototype(descriptor);
 // create a real instance of "Pair"
 Message *pair = message->New();

 // write the "Pair" instance by reflection
 const Reflection *reflection = pair->GetReflection();
 const FieldDescriptor *field = NULL;
 field = descriptor->FindFieldByName("key"); // set first key and value
 reflection->SetString(pair, field, "my key");
 field = descriptor->FindFieldByName("value"); // set second key and value
 reflection->SetUInt32(pair, field, 1234);

 cout << pair->DebugString();
 delete pair;
}

static void dynamic2()
{
 DiskSourceTree sourceTree;
 //look up .proto file in current directory
 sourceTree.MapPath("", "./");
 Importer importer(&sourceTree, NULL);
 //runtime compile foo.proto
 if (NULL == importer.Import("foo.proto")) {
 std::cerr << "Fail to open proto file" << std::endl;
 return;
 }

 const Descriptor *descriptor = importer.pool()->FindMessageTypeByName("Pair");
 cout << descriptor->DebugString();

 // build a dynamic message by "Pair" proto
 DynamicMessageFactory factory;
 const Message *message = factory.GetPrototype(descriptor);
 // create a real instance of "Pair"
 Message *pair = message->New();

 // write the "Pair" instance by reflection
 const Reflection *reflection = pair->GetReflection();

 const FieldDescriptor *field = NULL;
 field = descriptor->FindFieldByName("key");
 reflection->SetString(pair, field, "my key");
 field = descriptor->FindFieldByName("value");
 reflection->SetUInt32(pair, field, 1111);

 field = descriptor->FindFieldByName("type");
 const EnumValueDescriptor* type =
 field->enum_type()->FindValueByName("TYPE_ONE");
 if (NULL == type) {
   std::cerr << "Fail to find enum descriptor" << std::endl;
   return ;
 }
 reflection->SetEnum(pair, field, type);

 cout << pair->DebugString();

 delete pair;
}

int main(int argc, const char *argv[])
{
 dynamic1();

 dynamic2();

 return 0;
}

 

$ cat foo.proto
enum EType {
TYPE_ONE = 1;
TYPE_TWO = 2;
}

message Pair {
required string key = 1;
required uint32 value = 2;
required EType type = 3;
}

参考文档

http://www.searchtb.com/2012/09/protocol-buffers.html

http://name5566.com/2473.html

常规用法

valgrind –leak-check=full –show-reachable=yes  –trace-children=yes -v ./process_name 

常见错误解读

http://www.ibm.com/developerworks/cn/linux/l-pow-debug/

http://my.oschina.net/mavericsoung/blog/125827

3.1 Illegal read / Illegal write errors

例如:

Invalid read of size 4 at 0x40F6BBCC: (within /usr/lib/libpng.so.2.1.0.9) by 0x40F6B804: (within /usr/lib/libpng.so.2.1.0.9) by 0x40B07FF4: read_png_image(QImageIO *) (kernel/qpngio.cpp:326) by 0x40AC751B: QImageIO::read() (kernel/qimage.cpp:3621) Address 0xBFFFF0E0 is not stack’d, malloc’d or free’d

这个错误的发生是因为对一些memcheck猜想不应该访问的内存进行了读写。

3.2 Use of uninitialised values

例如:

Conditional jump or move depends on uninitialised value(s) at 0x402DFA94: _IO_vfprintf (_itoa.h:49) by 0x402E8476: _IO_printf (printf.c:36) by 0x8048472: main (tests/manuel1.c:8)

这个错误的发生是因为使用了未初始化的数据。一般情况下有两种情形容易出现这个错误:程序中的局部变量未初始化;C语言malloc的内存未初始化;C++中new的对象其成员未被初始化。

3.3 Illegal frees

例如:

Invalid free() at 0x4004FFDF: free (vg_clientmalloc.c:577) by 0x80484C7: main (tests/doublefree.c:10) Address 0x3807F7B4 is 0 bytes inside a block of size 177 free’d at 0x4004FFDF: free (vg_clientmalloc.c:577) by 0x80484C7: main (tests/doublefree.c:10)

3.4 When a block is freed with an inappropriate deallocation function

例如:Mismatched free() / delete / delete [] at 0x40043249: free (vg_clientfuncs.c:171) by 0x4102BB4E: QGArray::~QGArray(void) (tools/qgarray.cpp:149) by 0x4C261C41: PptDoc::~PptDoc(void) (include/qmemarray.h:60) by 0x4C261F0E: PptXml::~PptXml(void) (pptxml.cc:44) Address 0x4BB292A8 is 0 bytes inside a block of size 64 alloc’d at 0x4004318C: operator new[](unsigned int) (vg_clientfuncs.c:152) by 0x4C21BC15: KLaola::readSBStream(int) const (klaola.cc:314) by 0x4C21C155: KLaola::stream(KLaola::OLENode const *) (klaola.cc:416) by 0x4C21788F: OLEFilter::convert(QCString const &) (olefilter.cc:272)

  • If allocated with malloc, calloc, realloc, valloc or memalign, you must deallocate with free.
  • If allocated with new[], you must deallocate with delete[].
  • If allocated with new, you must deallocate with delete.

    linux系统对上述错误可能不在意,但是移值到其他平台时却会有问题。

3.5 Passing system call parameters with inadequate read/write permissions

例如:Syscall param write(buf) points to uninitialised byte(s) at 0x25A48723: __write_nocancel (in /lib/tls/libc-2.3.3.so) by 0x259AFAD3: __libc_start_main (in /lib/tls/libc-2.3.3.so) by 0x8048348: (within /auto/homes/njn25/grind/head4/a.out) Address 0x25AB8028 is 0 bytes inside a block of size 10 alloc’d at 0x259852B0: malloc (vg_replace_malloc.c:130) by 0x80483F1: main (a.c:5) Syscall param exit(error_code) contains uninitialised byte(s) at 0x25A21B44: __GI__exit (in /lib/tls/libc-2.3.3.so) by 0x8048426: main (a.c:8)

Memcheck检查所有的被系统调用的参数。

  • It checks all the direct parameters themselves.
  • Also, if a system call needs to read from a buffer provided by your program, Memcheck checks that the entire buffer is addressable and has valid data, ie, it is readable.
  • Also, if the system call needs to write to a user-supplied buffer, Memcheck checks that the buffer is addressable.

    例如:

    #include <stdlib.h>#include <unistd.h>int main( void ){char* arr = malloc(10);int* arr2 = malloc(sizeof(int));write( 1 /* stdout */, arr, 10 );exit(arr2[0]);}

    错误信息:

    Syscall param write(buf) points to uninitialised byte(s)at 0x25A48723: __write_nocancel (in /lib/tls/libc-2.3.3.so)by 0x259AFAD3: __libc_start_main (in /lib/tls/libc-2.3.3.so)by 0x8048348: (within /auto/homes/njn25/grind/head4/a.out)Address 0x25AB8028 is 0 bytes inside a block of size 10 alloc’dat 0x259852B0: malloc (vg_replace_malloc.c:130)by 0x80483F1: main (a.c:5)Syscall param exit(error_code) contains uninitialised byte(s)at 0x25A21B44: __GI__exit (in /lib/tls/libc-2.3.3.so)by 0x8048426: main (a.c:8)

    传递了无效参数到系统函数中。

    C的以下库函数拷贝数据从一块内存到另一块内存时: memcpy(), strcpy(), strncpy(), strcat(), strncat(). 源和目的都不允许溢出。

    例如:

    ==27492== Source and destination overlap in memcpy(0xbffff294, 0xbffff280, 21)==27492== at 0x40026CDC: memcpy (mc_replace_strmem.c:71)==27492== by 0x804865A: main (overlap.c:40)

    3.7 Memory leak detection

    错误信息:

    Still reachable: A pointer to the start of the block is found. This usually indicates programming sloppiness. Since the block is still pointed at, the programmer could, at least in principle, free it before program exit. Because these are very common and arguably not a problem, Memcheck won’t report such blocks unless –show-reachable=yes is specified.

    Possibly lost, or “dubious”: A pointer to the interior of the block is found. The pointer might originally have pointed to the start and have been moved along, or it might be entirely unrelated. Memcheck deems such a block as “dubious”, because it’s unclear whether or not a pointer to it still exists.

    Definitely lost, or “leaked”: The worst outcome is that no pointer to the block can be found. The block is classified as “leaked”, because the programmer could not possibly have freed it at program exit, since no pointer to it exists. This is likely a symptom of having lost the pointer at some earlier point in the program.

3.6 Overlapping source and destination blocks

C的以下库函数拷贝数据从一块内存到另一块内存时: memcpy(), strcpy(), strncpy(), strcat(), strncat(). 源和目的都不允许溢出。

例如:

==27492== Source and destination overlap in memcpy(0xbffff294, 0xbffff280, 21)==27492== at 0x40026CDC: memcpy (mc_replace_strmem.c:71)==27492== by 0x804865A: main (overlap.c:40)

3.7 Memory leak detection

错误信息:

Still reachable: A pointer to the start of the block is found. This usually indicates programming sloppiness. Since the block is still pointed at, the programmer could, at least in principle, free it before program exit. Because these are very common and arguably not a problem, Memcheck won’t report such blocks unless –show-reachable=yes is specified.

Possibly lost, or “dubious”: A pointer to the interior of the block is found. The pointer might originally have pointed to the start and have been moved along, or it might be entirely unrelated. Memcheck deems such a block as “dubious”, because it’s unclear whether or not a pointer to it still exists.

Definitely lost, or “leaked”: The worst outcome is that no pointer to the block can be found. The block is classified as “leaked”, because the programmer could not possibly have freed it at program exit, since no pointer to it exists. This is likely a symptom of having lost the pointer at some earlier point in the program.

Valgrind的主要功能

Valgrind工具包包含多个工具,如Memcheck,Cachegrind,Helgrind, Callgrind,Massif。下面分别介绍个工具的作用:

Memcheck 工具主要检查下面的程序错误:

  • 使用未初始化的内存 (Use of uninitialised memory)
  • 使用已经释放了的内存 (Reading/writing memory after it has been free’d)
  • 使用超过 malloc分配的内存空间(Reading/writing off the end of malloc’d blocks)
  • 对堆栈的非法访问 (Reading/writing inappropriate areas on the stack)
  • 申请的空间是否有释放 (Memory leaks – where pointers to malloc’d blocks are lost forever)
  • malloc/free/new/delete申请和释放内存的匹配(Mismatched use of malloc/new/new [] vs free/delete/delete [])
  • src和dst的重叠(Overlapping src and dst pointers in memcpy() and related functions)

Callgrind

Callgrind收集程序运行时的一些数据,函数调用关系等信息,还可以有选择地进行cache 模拟。在运行结束时,它会把分析数据写入一个文件。callgrind_annotate可以把这个文件的内容转化成可读的形式。

Cachegrind

它模拟 CPU中的一级缓存I1,D1和L2二级缓存,能够精确地指出程序中 cache的丢失和命中。如果需要,它还能够为我们提供cache丢失次数,内存引用次数,以及每行代码,每个函数,每个模块,整个程序产生的指令数。这对优化程序有很大的帮助。

Helgrind

它主要用来检查多线程程序中出现的竞争问题。Helgrind 寻找内存中被多个线程访问,而又没有一贯加锁的区域,这些区域往往是线程之间失去同步的地方,而且会导致难以发掘的错误。Helgrind实现了名为” Eraser” 的竞争检测算法,并做了进一步改进,减少了报告错误的次数。

Massif

堆栈分析器,它能测量程序在堆栈中使用了多少内存,告诉我们堆块,堆管理块和栈的大小。Massif能帮助我们减少内存的使用,在带有虚拟内存的现代系统中,它还能够加速我们程序的运行,减少程序停留在交换区中的几率。

 

Valgrind相关参数介绍

1、  –error-limit=<yes|no>:是否限制检查出来的错误的数量。一般的时候都设置成no,不限制错误的数量。

2、  –db-attach=<yes|no>:打开该开关,能够及时查看代码,定位问题。但是对于我们测试,没有代码的情况下,这个配置项应该设置成no。

3、  –leak-check=<no|summary|yes|full>,其中no不检查泄露,summary只检查有多少个泄露,没有具体信息,yes或者full会将详细信息展现出来。

4、  –show-rearchable:退出的时候是否有指针指向alloc的内存,默认是不指向。

5、  –max-stackframe:使用最大的栈内存,默认:2000000。该参数根据程序使用内存大小情况对数据做具体的调整。

6、  –log-file:指定valgrind运行输出的数据。

 

  Valgrind记录文件的主要错误有以下几种:

1、 内存未初始化:Conditional jump or move depends on uninitialised value
2、 读写越界:Invalid read/write of size
3、 内存泄露:definitely lost
4、 原串与目标串内存交叠:Source and destination overlap in strcpy/memcpy
5、 非法释放或者删除:Invalid free() / delete / delete[]
6、 含义和5相同:Mismatched free() / delete / delete[]

 

  Valgrind作用限制

1、Valgrind需要有实际的数据运行到相应的分支,产生相应的问题,才能对问题进行记录,不论是读写越界,还是内存泄露,都必须有query走到。所以除了用Vagrind,还需要其他的测试手段。

2、valgrind本身有一些无法检查的错误。

  • Valgrind可以检查堆内存越界和内存泄露,可以检查简单的堆栈溢出且出core的问题,但是无法检查栈内存越界。
  • 堆栈错误出现了core,所以valgrind可以检测到错误信息,但是由于堆栈已经被破坏,无法准确还原真实的函数调用栈。
  • 对于全局指针,进程在收到15信号后是不进行释放,但是对应的内存有可能被系统回收。
  • 对于所有指针,进程在收到9信号后理解退出,valgrind无法决定是否有内存泄漏。

先引两篇内网文章:

shared_ptr四宗罪

 

在基于C++的大型系统的设计实现中,由于缺乏语言级别的GC支持,资源生存周期往往是一个棘手的问题。系统地解决这个问题的方法无非两种:

使用GC库

使用引用计数

严格地说,引用计数其实也是一种最朴素的GC。相对于现代的GC技术,引用计数的实现简单,但相应地,它也存在着循环引用和线程同步开销等问题。关于这二者孰优孰劣,已经有过很多讨论,在此就不搅这股混水了。我一直也没有使用过C++的GC库,在实际项目中总是采用引用计数的方案。而作为Boost的拥趸,首选的自然是shared_ptr。一直以来我也对shared_ptr百般推崇,然而最近的一些项目开发经验却让我在shared_ptr上栽了坑,对C++引用计数也有了一些新的的认识,遂记录在此。

本文主要针对基于boost::shared_ptr的C++引用计数实现方案进行一些讨论。 C++引用计数方案往往伴随着用于自动管理引用计数的智能指针。按是否要求资源对象自己维护引用计数,C++引用计数方案可以分为两类:

侵入式

侵入式的引用计数管理要求资源对象本身维护引用计数,同时提供增减引用计数的管理接口。通常侵入式方案会提供配套的侵入式引用计数智能指针。该智能指针通过调用资源对象的引用计数管理接口来自动增减引用计数。COM对象与CComPtr便是侵入式引用计数的一个典型实例。

非侵入式

非侵入式的引用计数管理对资源对象本身没有任何要求,而是完全借助非侵入式引用计数智能指针在资源对象外部维护独立的引用计数。shared_ptr便是基于这个思路。

初看起来,非侵入式方案由于对资源对象的实现没有任何要求,相较于侵入式方案更具吸引力。然而事实却并非如此。下面就来分析一下基于shared_ptr的非侵入式引用计数。在使用shared_ptr的引用计数解决方案中,引用计数完全由shared_ptr控制,资源对象对与自己对应的引用计数一无所知。而引用计数与资源对象的生存期息息相关,这就意味着资源对象丧失了对生存期的控制权,将自己的生杀大权拱手让给了shared_ptr。这种情况下,资源对象就不得不依靠至少一个shared_ptr实例来保障自己的生存。换言之,资源对象一旦“沾染”了shared_ptr,就一辈子都无法摆脱! 考察以下的简单用例:

用例一:

1 2 3 4 5 IResource* p = new CResource; { shared_ptr q( p ); } p->Use() // CRASH

单纯为了解决上述的崩溃,可以自定义一个什么也不做的deleter:

1 2 3 4 5 struct noop_deleter { void operator()( void* ) { // NO-OP } };

然后将上述用例的第三行改为:

shared_ptr q( p, noop_deleter() ); 但是这样一来,shared_ptr就丧失了借助RAII自动释放资源的能力,违背了我们利用智能指针自动管理资源生存期的初衷(话说回来,这倒并不是说noop_deleter这种手法毫无用处,Boost.Asio中就巧妙地利用shared_ptr、weak_ptr和noop_deleter来实现异步I/O事件的取消)。从这个简单的用例可以看出,shared_ptr就像是毒品一样,一旦沾染就难以戒除。更甚者,染毒者连换用其他“毒品”的权力都没有:shared_ptr的引用计数管理接口是私有的,无法从shared_ptr之外操控,也就无法从shared_ptr迁移到其他类型的引用计数智能指针。

不仅如此,资源对象沾染上shared_ptr之后,就只能使用最初的那个shared_ptr实例的拷贝来维系自己的生存期。考察以下用例:

用例二:

1 2 3 4 5 6 7 { shared_ptr p1( CResource ); shared_ptr p2( p1 ); // OK IResource* p3 = p1.get(); shared_ptr p4( p3 ); // ERROR // CRASH }

该用例的执行过程如下:

p1在构造的同时为资源对象创建了一份外部引用计数,并将之置为1 p2拷贝自p1,与p1共享同一个引用计数,将之增加为2 p4并非p1的拷贝,因此在构造的同时又为资源对象创建了另外一个外部引用计数,并将之置为1 在作用域结束时,p4析构,由其维护的额外的引用计数降为0,导致资源对象被析构 然后p2析构,对应的引用计数降为1 接着p1析构,对应的引用计数也归零,于是p1在临死之前再次释放资源对象 最后,由于资源对象被二次释放,程序崩溃 至此,我们已经认识到了shared_ptr的第一宗罪——传播毒品:

毒性一:一旦开始对资源对象使用shared_ptr,就必须一直使用 毒性二:无法换用其他类型的引用计数之智能指针来管理资源对象生存期 毒性三:必须使用最初的shared_ptr实例拷贝来维系资源对象生存期 乘胜追击,再揭露一下shared_ptr的第二宗罪——散布病毒。有点耸人听闻了?其实道理很简单:由于使用了shared_ptr的资源对象必须仰仗shared_ptr的存在才能维系生存期,这就意味着使用资源的客户对象也必须使用shared_ptr来持有资源对象的引用——于是shared_ptr的势力范围成功地从资源对象本身扩散到了资源使用者,侵入了资源客户对象的实现。同时,资源的使用者往往是通过某种形式的资源分配器来获取资源。自然地,为了向客户转交资源对象的所有权,资源分配器也不得不在接口中传递shared_ptr,于是shared_ptr也会侵入资源分配器的接口。

有一种情况可以暂时摆脱shared_ptr,例如:

shared_ptr AllocateResource() { shared_ptr pResource( new CResource ); bar( pResource.get() ); return pResource; }

void InitResource( IResource* r ) { // Do resource initialization… }

以上用例中,在InitResource的执行期间,由于AllocateResource的堆栈仍然存在,pResource不会析构,因此可以放心的在InitResource的参数中使用裸指针传递资源对象。这种基于调用栈的引用计数优化,也是一种常用的手段。但在InitResource返回后,资源对象终究还是会落入shared_ptr的魔掌。

由此可以看出,shared_ptr打着“非侵入式”的幌子,虽然没有侵入资源对象的实现,却侵入了资源分配接口以及资源客户对象的实现。而沾染上shared_ptr就摆脱不掉,如此传播下去,简直就是侵入了除资源对象实现以外的其他各个地方!这不是病毒是什么?

然而,基于shared_ptr的引用计数解决方案真的不会侵入资源对象的实现吗?

在一些用例中,资源对象的成员方法(不包括构造函数)需要获取指向对象自身,即包含了this指针的shared_ptr。Boost.Asio的chat示例便展示了这样一个用例:chat_session对象会在其成员函数中发起异步I/O操作,并在异步I/O操作回调中保存一个指向自己的shared_ptr以保证回调执行时自身的生存期尚未结束。这种手法在Boost.Asio中非常常见,在不考虑shared_ptr带来的麻烦时,这实际上也是一种相当优雅的异步流程资源生存期处理方法。但现在让我们把注意力集中在shared_ptr上。

通常,使用shared_ptr的资源对象必须动态分配,最常见的就是直接从堆上new出一个实例并交付给一个shared_ptr,或者也可以从某个资源池中分配再借助自定义的deleter在引用计数归零时将资源放回池中。无论是那种用法,该资源对象的实例在创建出来后,都总是立即交付给一个shared_ptr(记为p)。有鉴于之前提到的毒性三,如果资源对象的成员方法需要获取一个指向自己的shared_ptr,那么这个shared_ptr也必须是p的一个拷贝——或者更本质的说,必须与p共享同一个外部引用计数。然而对于资源对象而言,p维护的引用计数是外部的陌生事物,资源对象如何得到这个引用计数并由此构造出一个合法的shared_ptr呢?这是一个比较tricky的过程。为了解决这个问题,Boost提供了一个类模板enable_shared_from_this:

所有需要在成员方法中获取指向this的shared_ptr的类型,都必须以CRTP手法继承自enable_shared_from_this。即:

class CResource : public boost::enable_shared_from_this { // … }; 接着,资源对象的成员方法就可以使用enable_shared_from_this::shared_from_this()方法来获取所需的指向对象自身的shared_ptr了。问题似乎解决了。但是,等等!这样的继承体系不就对资源对象的实现有要求了吗?换言之,这不正是对资源对象实现的赤裸裸的侵入吗?这正是shared_ptr的第三宗罪——欺世盗名。

最后一宗罪,是铺张浪费。对了,说的就是性能。

基于引用计数的资源生存期管理,打一出生起就被扣着线程同步开销大的帽子。早期的Boost版本中,shared_ptr是借助Boost.Thread的mutex对象来保护引用计数。在后期的版本中采用了lock-free的原子整数操作一定程度上降低了线程同步开销。然而即使是lock-free,本质上也仍然是串行化访问,线程同步的开销多少都会存在。也许有人会说这点开销与引用计数带来的便利相比算不得什么。然而在我们项目的异步服务器框架的压力测试中,大量引用计数的增减操作,一举吃掉了5%的CPU。换言之,1/20的计算能力被浪费在了与业务逻辑完全无关的引用计数的维护上!而且,由于是异步流程的特殊性,也无法应用上面提及的基于调用栈的引用计数优化。

那么针对这个问题就真的没有办法了吗?其实仔细检视一下整个异步流程,有些资源虽然会先后被不同的对象所引用,但在其整个生存周期内,每一时刻都只有一个对象持有该资源的引用。用于数据收发的缓冲区对象就是一个典型。它们总是被从某个源头产生,然后便一直从一处被传递到另一处,最终在某个时刻被回收。对于这样的对象,实际上没有必要针对流程中的每一次所有权转移都进行引用计数操作,只要简单地在分配时将引用计数置1,在需要释放时再将引用计数归零便可以了。

对于侵入式引用计数方案,由于资源对象自身持有引用计数并提供了引用计数的操作接口,可以很容易地实现这样的优化。但shared_ptr则不然。shared_ptr把引用计数牢牢地攥在手中,不让外界碰触;外界只有通过shared_ptr的构造函数、析够函数以及reset()方法才能够间接地对引用计数进行操作。而由于shared_ptr的毒品特性,资源对象无法脱离shared_ptr而存在,因此在转移资源对象的所有权时,也必须通过拷贝shared_ptr的方式进行。一次拷贝就对应一对引用计数的原子增减操作。对于上述的可优化资源对象,如果在一个流程中被传递3次,除去分配和释放时的2次,还会导致6次无谓的原子整数操作。整整浪费了300%!

事实证明,在将基于shared_ptr的非侵入式引用计数方案更改为侵入式引用计数方案并施行上述优化后,我们的异步服务器框架的性能有了明显的提升。

好了,最后总结一下shared_ptr的四宗罪:

传播毒品

一旦对资源对象染上了shared_ptr,在其生存期内便无法摆脱。

散布病毒

在应用了shared_ptr的资源对象的所有权变换的整个过程中的所有接口都会受到shared_ptr的污染。

欺世盗名

在enable_shared_from_this用例下,基于shared_ptr的解决方案并非是非侵入式的。

铺张浪费

由于shared_ptr隐藏了引用计数的操作接口,只能通过拷贝shared_ptr的方式间接操纵引用计数,使得用户难以规避不必要的引用计数操作,造成无谓的性能损失。

探明这四宗罪算是最近一段时间的项目设计开发过程的一大收获。写这篇文章的目的不是为了将shared_ptr一棒子打死,只是为了总结基于shared_ptr的C++非侵入式引用计数解决方案的缺陷,也让自己不再盲目迷信shared_ptr。

 

Auto_ptr和shared_ptr

Stl 中 auto_ptr只是众多可能的智能指针之一,auto_ptr所做的事情,就是动态分配对象以及当对象不再需要时自动执行清理。
这里是一个简单的代码示例,如果没有auto_ptr,

1 void ProcessAdoption(istream &data)
2 {
3
4 while (data) // 如果还有数据
5 {
6 ALA *pa = readALAData(data); // 取出下一个数据
7 pa->DealProcessAdoption(data); // 处理
8
9 delete pa; // 释放资源
10 }
11 return;
12 }

如果在DealProcessAdoption有一个exception,会发生什么事情,因为ProcessAdoption不能捕获他,所以这段代码很危险,所以DealProcessAdoption后面的代码可能会跳过,造成内存泄露。
如果利用try catch去捕获他,会搞得代码很乱,又缺少美观性。

所以Stl提供了一个智能指针来解决这个问题,我们可以先模拟实现一个智能指针的类实现。

1 // 关于一个智能指针的定义
2 template<typename Type>
3 class auto_ptr
4 {
5 public:
6 auto_ptr(T *p =NULL) :Ptr(p)
7 { }
8 ~auto_ptr()
9 {
10 delete Ptr;
11 }
12 private:
13 Type *Ptr;
14 };
15
16
17 void ProcessAdoption(istream &data)
18 {
19
20 while (data) // 如果还有数据
21 {
22 auto_ptr<ALA> pa(readALADara(data));
23 pa->DealProcessAdoption(data);
24 }
25 return;
26 }

这个版本和原先版本的差异只有二处,
第一pa是一智能指针的对象,不是ALA*
第二不用自己去释放delete

然后我看到Effective STL的条款
8:永不建立auto_ptr的容器
关于此可以看的Effective STL的条款8

因为auto_ptr并不是完美无缺的,它的确很方便,但也有缺陷,在使用时要注意避免。首先,不要将auto_ptr对象作为STL容器的元素。C++标准明确禁止这样做,否则可能会碰到不可预见的结果

auto_ptr的另一个缺陷是将数组作为auto_ptr的参数: auto_ptr<char> pstr (new char[12] ); //数组;为定义
然后释放资源的时候不知道到底是利用delete pstr,还是 delete[] pstr;

然后收集了关于auto_ptr的几种注意事项:
1、auto_ptr不能共享所有权。
2、auto_ptr不能指向数组
3、auto_ptr不能作为容器的成员。
4、不能通过赋值操作来初始化auto_ptr
std::auto_ptr<int> p(new int(42)); //OK
std::auto_ptr<int> p = new int(42); //ERROR
这是因为auto_ptr 的构造函数被定义为了explicit
5、不要把auto_ptr放入容器

然后笔者从而推荐的是boost的shared_ptr,然后看完shared_ptr关于智能指针的介绍与例子。
5种针对auto_ptr不足的指针如下:需要详细了解可以去查看相当文档,与测试新代码。

scoped_ptr

<boost/scoped_ptr.hpp>

简单的单一对象的唯一所有权。不可拷贝。

scoped_array

<boost/scoped_array.hpp>

简单的数组的唯一所有权。不可拷贝。

shared_ptr

<boost/shared_ptr.hpp>

在多个指针间共享的对象所有权。

shared_array

<boost/shared_array.hpp>

在多个指针间共享的数组所有权。

weak_ptr

<boost/weak_ptr.hpp>

一个属于 shared_ptr 的对象的无所有权的观察者。

intrusive_ptr

<boost/intrusive_ptr.hpp>

带有一个侵入式引用计数的对象的共享所有权。

1. shared_ptr是Boost库所提供的一个智能指针的实现,shared_ptr就是为了解决auto_ptr在对象所有权上的局限性(auto_ptr是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针.
2. shared_ptr比auto_ptr更安全
3. shared_ptr是可以拷贝和赋值的,拷贝行为也是等价的,并且可以被比较,这意味这它可被放入标准库的一般容器(vector,list)和关联容器中(map)。

关于shared_ptr的使用其实和auto_ptr差不多,只是实现上有差别,关于shared_ptr的定义就不贴代码了,以为内开源,可以网上找
1、shared_ptr<T> p(new Y);

要了解更多关于auto_ptr的信息,可以查看more effective c++ 的p158页条款28
要了解shared_ptr 类模板信息,可以查看boost 1.37.0中文文档,而且支持数组的shared_array 类模板

Stl 中 auto_ptr只是众多可能的智能指针之一,auto_ptr所做的事情,就是动态分配对象以及当对象不再需要时自动执行清理。
这里是一个简单的代码示例,如果没有auto_ptr,

1 void ProcessAdoption(istream &data)
2 {
3
4 while (data) // 如果还有数据
5 {
6 ALA *pa = readALAData(data); // 取出下一个数据
7 pa->DealProcessAdoption(data); // 处理
8
9 delete pa; // 释放资源
10 }
11 return;
12 }

如果在DealProcessAdoption有一个exception,会发生什么事情,因为ProcessAdoption不能捕获他,所以这段代码很危险,所以DealProcessAdoption后面的代码可能会跳过,造成内存泄露。
如果利用try catch去捕获他,会搞得代码很乱,又缺少美观性。

所以Stl提供了一个智能指针来解决这个问题,我们可以先模拟实现一个智能指针的类实现。

1 // 关于一个智能指针的定义
2 template<typename Type>
3 class auto_ptr
4 {
5 public:
6 auto_ptr(T *p =NULL) :Ptr(p)
7 { }
8 ~auto_ptr()
9 {
10 delete Ptr;
11 }
12 private:
13 Type *Ptr;
14 };
15
16
17 void ProcessAdoption(istream &data)
18 {
19
20 while (data) // 如果还有数据
21 {
22 auto_ptr<ALA> pa(readALADara(data));
23 pa->DealProcessAdoption(data);
24 }
25 return;
26 }

这个版本和原先版本的差异只有二处,
第一pa是一智能指针的对象,不是ALA*
第二不用自己去释放delete

然后我看到Effective STL的条款
8:永不建立auto_ptr的容器
关于此可以看的Effective STL的条款8

因为auto_ptr并不是完美无缺的,它的确很方便,但也有缺陷,在使用时要注意避免。首先,不要将auto_ptr对象作为STL容器的元素。C++标准明确禁止这样做,否则可能会碰到不可预见的结果

auto_ptr的另一个缺陷是将数组作为auto_ptr的参数: auto_ptr<char> pstr (new char[12] ); //数组;为定义
然后释放资源的时候不知道到底是利用delete pstr,还是 delete[] pstr;

然后收集了关于auto_ptr的几种注意事项:
1、auto_ptr不能共享所有权。
2、auto_ptr不能指向数组
3、auto_ptr不能作为容器的成员。
4、不能通过赋值操作来初始化auto_ptr
std::auto_ptr<int> p(new int(42)); //OK
std::auto_ptr<int> p = new int(42); //ERROR
这是因为auto_ptr 的构造函数被定义为了explicit
5、不要把auto_ptr放入容器

然后笔者从而推荐的是boost的shared_ptr,然后看完shared_ptr关于智能指针的介绍与例子。
5种针对auto_ptr不足的指针如下:需要详细了解可以去查看相当文档,与测试新代码。

scoped_ptr

简单的单一对象的唯一所有权。不可拷贝。

scoped_array

简单的数组的唯一所有权。不可拷贝。

shared_ptr

在多个指针间共享的对象所有权。

shared_array

在多个指针间共享的数组所有权。

weak_ptr

一个属于 shared_ptr 的对象的无所有权的观察者。

intrusive_ptr

带有一个侵入式引用计数的对象的共享所有权。

1. shared_ptr是Boost库所提供的一个智能指针的实现,shared_ptr就是为了解决auto_ptr在对象所有权上的局限性(auto_ptr是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针.
2. shared_ptr比auto_ptr更安全
3. shared_ptr是可以拷贝和赋值的,拷贝行为也是等价的,并且可以被比较,这意味这它可被放入标准库的一般容器(vector,list)和关联容器中(map)。

关于shared_ptr的使用其实和auto_ptr差不多,只是实现上有差别,关于shared_ptr的定义就不贴代码了,以为内开源,可以网上找
1、shared_ptr<T> p(new Y);

要了解更多关于auto_ptr的信息,可以查看more effective c++ 的p158页条款28
要了解shared_ptr 类模板信息,可以查看boost 1.37.0中文文档,而且支持数组的shared_array 类模板

猜测有3种可能的影响因素:定长槽位、冗余结构体、rehash。

冗余结构体很好理解,针对每个item,redis需要分配一些结构体存储额外信息。且64位机每个指针8字节,32位机每个指针4字节(32位机虽然指针消耗较小,但有4GB的内存上限)。一般认为冗余字节至少64bytes。

定长槽位,会产生貌似的冗余字节,类似memcached的实现,预先分配一定量的多种大小的固定slot,插入数据时,找最合适的槽位写入,若预分配槽位不足再动态扩展,这样会降低内存的申请和释放,属于常用的内存管理方式。

rehash,为了容纳1k个key,达到较低的冲撞率,可能需要分配5k个key数组,这也可能产生一些冗余字节。

定长槽位

定量kv,逐渐增大k、v的size,若在一定范围内memory usage不变,证明有对齐节点。

固定size的value,不断增大的key:

 

固定长度的Key,不断增大的Value:

 

从上面可以看到,单独变化的key或value长度,都会造成阶梯状的内存变化,猜测可能存在内存预分配

冗余结构体

固定长度的KV,不断增大的count:(key len: 21, val len: 1,选择上面阶梯最右边的kv长度,认为对齐长度的影响最小)

 

从上图猜测,kv的冗余结构体消耗平均90字节左右

 

rehash没有想到简单的测试方法。

以上若要真正确认,可能只能去看redis的源码了。

 

代码:run_string

 

HBase, HDFS and durable sync

HBase,HDFS 与持久化的sync

原文链接:http://hadoop-hbase.blogspot.com/2012/05/hbase-hdfs-and-durable-sync.html

作者: 

 

HBase and HDFS go hand in hand to provide HBase’s durability and consistency guarantees.

Hbase与HDFS组合在一起,才提供了HBase的持久化和一致性保障。

 

One way of looking at this setup is that HDFS handles the distribution and storage of your data whereas HBase handles the distribution of CPU cycles and provides a consistent view of that data.

一种理解方式是,HDFS保证数据的分布式存储,而HBase提供分布式的CPU计算能力,并提供了数据的一致性视图。

 

As described in many other places, HBase

  1. Appends all changes to a WAL
  2. Batches/sorts changes in memory
  3. Flushes memory to immutable, sorted data files in HDFS
  4. Combines smaller data files to fewer, larger ones during compactions.

就像其他一些地方提到的,HBase

  1. 将所有的写操作追加到WAL日志里
  2. 在内存(memstore)里保存这些改变,并维持一定的数据结构
  3. 将memstore里的数据flush到存储在HDFS上、不可变的、排好序的数据文件(HFile)里
  4. 通过compactions将大量零散的HFiles小文件合并为少量HFiles大文件,依然存储在HDFS上

 

The part that is typical less clearly understood and documented are the exact durability guarantees provided by HDFS (and hence HBase).

这些步骤里,不好理解的其实是HDFS部分,它到底是如何提供持久化保证的呢?由于HDFS是存储基础,故也进一步影响到HBase的持久化保证。

 

HDFS sync has a colorful history, with needed support for HBase only available in an unreleased “append”-branch of HDFS for a long time. (Note that the append and sync features are independent and against common believe HBase only relies on the sync feature). See also this Cloudera blog post.

HDFS sync有一个多彩的历史,在很长一段时间里,都是在HDFS的一个未发布的“append”feature分支里,向HBase提供其所需的支持的。(需要注意的是,“append”和“sync” feature是相互独立的,而且出人意料的是,HBase其实仅依赖“sync”feature)。请参考Cloudera的博客

 

In order to understand what HDFS provides let’s take a look at how a DFSClient (client) interacts with a Datanode (DN).

为了理解HDFS提供的功能,我们先来看一下DFSClient(client)与DataNode(DN)的交互过程。

 

In a nutshell a DN just waits for commands. One of these commands is WRITE_BLOCK. When the DN receives a WRITE_BLOCK command it instantiates a BlockReceiver thread.

简而言之,DN会等待命令,其中一个命令是WRITE_BLOCK。DN接收到该命令后,会启动一个BlockReceiver线程。

 

The BlockReceiver then simply waits for packets on an InputStream and flushes the data to OS buffers. An open block is maintained at the DN as an open file. When a block is filled, the block and hence its associate files is closed and the BlockReceiver ends. For all practical purposes the DN forgets that the block existed.

BlockReceiver等待输入流(InputStream)上传来的数据包,并flush到操作系统buffer里。BlockReceiver打开的block,对应为DN服务器上一个打开的文件。一旦block被填满,block被关闭,即底层的文件被关闭,BlockReceiver结束。出于实际考虑,DN忽略这些存在过的block。

 

Replication to replica DNs is done via pipelining. The first DN forwards each packet to the next DN in the chain before the data is flushed locally, and waits for the downstreadm DN to respond. The default length of the replication chain is 3.

通过pipelining在多个DN副本间进行复制。第一个DN在数据flush到本地之前,会把每一个数据包发送给DN复制链的下一个DN,并等待其响应。默认的复制链长度是3。

 

The other side of the equation is the DFSClient. The DFSClient batches changes until a packet is filled.

交互的另一头是DFSClient,它把多个写操作打包为一个合理大小的数据包。

 

Since HADOOP-6313 a Syncable supports hflush and hsync.

  • hflush flushes all outstanding data (i.e. the current unfinished packet) from the client into the OS buffers on all DN replicas.
  • hsync flushes the data to the DNs like hflush and should also force the data to disk via fsync (or equivalent). But currently for HDFS hsync is implemented as hflush!

HADOOP-6313 开始, Syncable支持了hflush和hsync。

  • hflush:将client发出的所有数据flush到所有DN副本的操作系统buffer里,即使数据包未被填满
  • hsync:除了完成hflush类似的工作外,还应该(should)通过类似fsync的命令,强制把数据从操作系统buffer写入到磁盘里。但当前HDFS的hsync其实是通过hflush实现的(即,还是没能从os buffer持久化到磁盘)

 

The gist is that both closing of a block and issuing hsync/hflush on the client currently only guarantees that data was flushed from the client to the replica DNs and to OS buffers on each DN; not that the data actually reached a physical disk.

关键点就是,不论block的关闭,还是client调用hsync/hflush,当前都只能保证把数据存储到每个DN的操作系统buffer里,而不是物理硬盘!

 

For HBase and similar applications with durability guarantees this can be insufficient. If three or more DN machines crash at the same time (assume three replicas), for example caused by a multi rack or data center power outage, data might be lost.

对于HBase和类似需保证持久化的应用而言,这都是远远不足的。如果3个或更多的DN服务器在同一时间宕机(假设3副本),数据就丢了。宕机的原因可能是多个机架或数据中心的电力不足等。

 

Further, since HBase constantly compacts older, smaller HFiles into newer, larger ones, this potential data loss is not limited to new data.
(But note that, like most database setups, HBase should be deployed with redundant power supply anyway, so this is not necessarily an issue).

甚至,由于HBase还经常执行compacts以合并小文件等,折衷潜在的数据丢失风险也有可能波及compacts过程,从而扩大影响面。(不过,正常人在部署HBase时,都应该提供备用电池,所以3个一起坏的几率比较小)。

 

Due to the inner working of the DN it is difficult to implement 100% Posix fsync semantics. Imagine a client, which writes many blocks worth of data and then issues an hsync. In order to sync data correctly to disk either the client or all involved DNs would need to keep track of all blocks (full or partial) written to so far that have not yet been sync’ed.

由于DN内部的工作机制,很难100%实现Posix fsync的语义。想象一个client(通过BlockReceiver)写了一些blocks,并且调用了hsync。为了将数据同步到磁盘,client和所有DN副本都需要跟踪所有那些尚未被synced的blocks。

 

This would be a significant change to how either the client or the DN work and lead to more complicated code. It would also require to keep the block files open in order to retain the file descriptors so that an fsync could be potentially issued in the future. Since the client might in fact never issue a sync request the number of open files to retain is unbounded.

这将极大改变client和DN的工作机制,并使代码变得很复杂。这也要求一直保持block文件的打开状态,因为fsync需要用到文件描述符。而client很可能永远不会调用sync,那么打开文件的数目就变得无上限了。

 

The other option (similar to Posix’ O_SYNC) is to have the DNs call fsync upon receipt of every single packet. leading to many unnecessary fsyncs.

另一个选择是,在收到每一个数据包时,DN就调用fsync(类似Posix的O_SYNC选项),但这会导致很多不必要的fsync。

 

In HDFS-744 I propose a hybrid solution. A data stream can be created with a SYNC_BLOCK flag. This flag causes the DFSClient set a “sync” flag on the last packet of a block. I.e. the block file is fsync’ed upon close.

HDFS-744 里,原文作者提出一种解决方案。一个数据传输流在创建时可以被打上SYNC_BLOCK的标记。这个标记,需要DFSClient在block的最后一个数据包打上sync标记,即在block file被关闭前,强制fsync。

 

This flag is also set when the client issues hsync. If the client has outstanding data the current packet is tagged with the “sync” flag and sent immediately, otherwise an empty packet with this flags is sent.

当client调用hsync时,也会打上这个标记。如果这时有往外发送的数据,那会随之打上sync标记;否则就发送一个被打上sync标记的空数据包。

 

When a DN receives such a packet, it will immediately flush the currently open file (representing the current block – full on close or partial on hsync – being written) to disk.

当DN收到这种数据包,就会立刻将数据flush到磁盘,这时如果block满了就关闭文件(间接强刷到磁盘),如果没满就hsync。

 

In summary: With this compromise a client can guarantee – byte-by-byte if needed – which portion of an open file is guaranteed on a durable medium while avoiding either syncing every packet to disk or keeping track of past unsync’ed block.

总结:通过这种折衷方法,client可以精确到byte粒度地控制如何持久化,同时避免将每个数据包sync一次,或者跟踪那些未sync的blocks。

 

For HBase this would conveniently deal with compactions as blocks are sync’ed upon close and also with WAL edits as it correctly allows sync’ing the current block.

对于HBase而言,这种方法可以方便的解决compactions和WAL的持久化问题。compactions时,blocks会在关闭时持久化。而WAL已允许显示sync当前的block。

 

The downside is that upon close each block needs to be sync’ed to disk, even though the client might never issue a sync request for this stream; this leads to potentially unneeded fsyncs.

缺点是,由于每一次关闭block(可能是由于block满了导致的)都会导致数据被sync到磁盘,即使client可能没有想调用sync,这样可能会导致不必要的fsync。

 

HBASE-5954 proposes matching changes to HBase to make use of this new HDFS feature. This issue introduces a WAL sync config option and an HFile sync option.

HBASE-5954将这种HDFS的新特性集成进了HBase,引入了WAL sync配置项和HFile sync配置项。

 

The former causes HBase to issue an hsync when a batch of WAL entries is written. The latter makes sure HFiles (generated from memstore flushes or compactions) are guaranteed to be on a durable medium when the stream is closed.

WAL sync控制HBase在批量写入一定量的WAL entries后调用一次hsync。HFile sync保证数据流在关闭时,HFiles(从memstore内存flush的,或compations产生的)被可靠地持久化。

 

There are also a few simple performance tests listed in that issue.

这个ISSUE里还列出了一些简单的性能测试结果。

 

Future optimization is possible:

  • Only one of the replica DNs could issue the sync
  • Only one DN in each Rack could issue the sync
  • The sync could be done in parallel and the response from the DN need not wait for for it to finish (in this case the client has no guarantee that the sync actually finished when hsync returns, only that the DN promised to do it)
  • For HBase, both options could be made configurable per column family.

其他可能的优化方法:

  • 只在一个DN副本上调用sync
  • 每个机架上上的多个副本中,只有一个调用sync
  • 多副本并行异步sync(由于hsync返回时,实际的sync还未结束,所以client无法保证其是否执行成功,仅DN承诺)
  • 允许针对column family粒度设置这些配置项

单集群可能发生不一致的情况

写入数据时,Region Server发生故障

RS在接收到Put等请求时,首先写WAL日志,如果写入失败,返回false。然后再写memstore,同样如果失败则返回false。但如果在写memstore时,RS发生故障,不就多写数据了?这时client可能会收不到返回值,于是认为写入失败,但由于WAL里已经有数据,再WAL edits replay的时候,数据会被持久化。类似的情况,mysql是否也会发生呢?是如何解决的呢?

而如果是WAL和memstore都写入成功,但memstore还没有来得及被flush到disk,那么可以来WAL replay进行数据恢复。

WAL和HFile文件的多备份

HFile是持久化的数据文件,WAL是故障恢复的关键,它们的可靠性要求非常高。HBase是依赖HDFS来保障的。

HFile是由后台线程异步写入,对实时性要求略低。一般认为可以承受HDFS多DN副本间的pipeline同步方式。而且由于只有HFile正常关闭后,才认为写入成功,才会把历史数据(旧HFile或WAL或memstore)删除,所以容灾也比较好做,如果写入失败就换个DN服务器重写呗。

WAL可以选择同步或异步的方式写入。在异步方式下,是先写入到HDFS client(即HBase Region Server)的内存缓冲区里,待达到flush条件时,再批量发送给HDFS的DN节点。DN节点也是首先存储在自己的OS buffer里,后续才会同步到磁盘上。这些过程中,都存在丢失的的可能性,尤其是client的缓冲区,一旦机器故障,就无法恢复了。而DN由于一般多副本(默认3副本),在同时宕机时才会丢失数据,而线上一般都有备用电池,所以可能性较小。

在同步WAL方式下,每条写操作,都会被强刷到HDFS DN的OS buffer里,虽然可靠性提升了,但性能有损耗。需要进行折衷。

WAL replay过程

 

原始的WAL replay过程如上图所示。

还有优化版本的splitting:http://hbase.apache.org/book/regionserver.arch.html中的Procedure 1.3. Distributed Log Splitting, Step by Step

Bulk Load过程的性能与一致性折衷

bulk load时,由于频繁写入,可能会导致多次memstore满、flush到HFile,HFile文件数量增多、minor/major compaction,进而可能引发JVM的GC,对服务性能和可用性造成影响。

一种折衷方案是,在bulk load的时候,设置hbase.mapreduce.hfileoutputformat.compaction.exclude临时关闭对load涉及文件的compaction。也可以临时关闭WAL的同步写(有风险)。为了避免load后Region过大,也可以在load前提前split。

多集群间可能发生不一致的情况

由于集群间是通过类似client API的方式,进行写入的,应该会有较大风险存在数据不一致的情况。不过没有实际应用过。

参考资料

http://hadoop-hbase.blogspot.jp/2013/05/hbase-durability-guarantees.html

http://hadoop-hbase.blogspot.com/2012/05/hbase-hdfs-and-durable-sync.html

http://blog.cloudera.com/blog/2009/07/file-appends-in-hdfs/

http://hadoop.apache.org/docs/current/api/org/apache/hadoop/fs/Syncable.html

https://issues.apache.org/jira/browse/HADOOP-6313