Category Archives: eBay 学习笔记

内存碎片浅析(3)

内存碎片浅析(1) 介绍了如何判断出现内存碎片。内存碎片浅析(2) 介绍了一个用于解决内存碎片的实现。 上一次还残留一些问题没有说完,今天把这个主题写完。 如何从FreeList中分配内存: void* DataBuffer::FirstTry(size_t sz){        Chunk* chunk=NULL;        void*    resPtr=NULL;        int*      metadata=NULL;         if(sz <= m_chunksize){              while(!m_freelist.empty()){                      chunk = m_freelist.front();                      if(resPtr = chunk->New(sz)){                             metadata = (int*)resPtr;                             *metadata = chunk->getId();                             return metadata;                      }                      m_freelist.pop_front();               }           }else{              同样遍历m_freelist, 试图从freelist中找出一块能够满足分配需要的chunk。              如果能找到,将该chunk移到m_freelist的顶部,并返回分配好的内存指针           } … 繼續閱讀

发表在 eBay 学习笔记 | 发表评论

内存碎片浅析(2)

上一篇介绍了如何判断发生了内存碎片。接下来是我们实际工作中碰到的一个问题,最后我们组的Bruce比较好的解决了,在Bruce的Fix之前,我们有一个程序每3天内存就会涨到接近4GB而需要重启,现在该程序基本稳定在2GB的内存消耗之内。Bruce的Code Review是我做的,今天花点时间写写看Bruce的代码的学习心得。 找到造成内存碎片的地方 这是最难的,因为我不知道有什么好的系统的方法,一般总是看整个程序哪些地方有频繁的malloc/free操作。我们可以使用dtrace打印malloc/free的call stack,并且给出统计信息。看看哪些地方会触发brk操作。 没有一个通用的内存管理库可以彻底解决内存碎片问题,原因是不同的应用程序有不同的内存使用特点。所以一般成熟的商业软件都有自己的内存管理模块。但是解决内存碎片的思路是一样的,就是尽量减少频繁的向系统申请和返还内存。 爬虫程序的内存使用特点 爬虫程序的典型逻辑是Batch。每一轮处理一批数据,处理这批数据过程中有很多内存操作,然后处理下一批数据。这批数据和下批数据之间没有依赖关系。 Bruce的Memory Buffer Pool   每个Chunk的默认大小是都是4M(这是可配置的)。BufferPool是个Singleton。有个Freelist维护所有的空闲Chunk。BufferPool有个current chunk指向当前可以用于分配内存的Chunk,通常情况下Current Chunk是半满的。[Bruce的实现比这个要复杂,他引入了二级内存分配机制] 内存申请到达时,查看当前Chunk能不能满足要求,如果能满足,就返回Current Cursor给程序使用,然后Current Cursor+=allocation size,并且增加当前Chunk的引用计数。 如果当前Chunk不能满足分配要求,就从freelist 中拿出一块新的Chunk来服务请求。 当内存释放时,找到分配该内存的Chunk,引用计数减一,当引用计数==0时,将该Chunk返回free list。 下面我们来看看实例代码: class Chunk{int     m_id; //size_t m_size;size_t m_cursor;char* m_buffer;size_t m_refcnt;}; 为什么需要m_id? 当我们释放一块内存的时候,我们需要知道这块内存先前是在哪个Chunk中分配的,这样我们才能找到那块Chunk,并且将那块Chunk的引用计数m_refcnt–。Bruce的做法是为每一个Chunk分配一个唯一标识id,每次分配内存时多分配4个字节的meta data。目标meta data中只存放了该内存所在的Chunk的id。 void* Chunk::New(size_t allocSize){     if(0==allocSize … 繼續閱讀

发表在 eBay 学习笔记 | 发表评论

内存碎片浅析(1)

如果我们的应用程序占用内存越来越厉害,而我们又没有发现内存泄漏,多半是内存碎片memory fragmentation造成的。 如何判断发生了内存碎片 其实在Solaris下我至今不知道一个标准的发现内存碎片的好方法。我的一般做法是:使用libumem: 使用libumem定位memory leak和memory corruption(1) 排除内存泄漏的可能性,然后使用::umastat 查看 ::umastat cache buf buf buf memory alloc alloc name size in use total in use succeed fail ————————- —— —— —— ——— ——— —– umem_magazine_1 8 385 676 16384 2120 0 … 繼續閱讀

发表在 eBay 学习笔记 | 发表评论

Double Checked Lock

这个问题是徐辰面试我的时候问的,如何实现一个线程安全的singleton模式。先看一个最普通的singleton代码:SingleFoo* getInstance(){      if(m_pInstance == NULL)      {               m_pInstance = new SingleFoo;      }      return m_pInstance;};问题是 当多个线程同时第一次调用getInstance的时候,很有可能m_pInstance==NULL都返回真,于是SingleFoo实例被创建两次。 当时我的回答是,加锁,于是我写了下面的代码给徐辰看:pthread_mutex_lock(&s_mutex);if(m_pInstance == NULL) m_pInstance = new SingleFoo;pthread_mutex_unlock(&s_mutex); 然后徐辰说,那假设SingleFoo已经被创建了,这样的加锁方式就会强制以后每次调用getInstance的时候都要加锁一次,而实际上在SingleFoo已经创建好的情况下根本就不用再加锁了~~ 后来的答案是这样的:if(m_pInstance==NULL)    //第一次check,只有在m_pInstance没有被初始化的时候才有可能要加锁{     pthread_mutex_lock(&s_mutex);     if(m_pInstance == NULL)  //第二次check     {           m_pInstance = new SingleFoo;     }     pthread_mutex_unlock(&s_mutex);}这种方法就叫做double-check lock。当时我并不知道这种模式。好处是去除了每次都要加锁的负担,代价是多了一次check。不过check一次的代价比起加锁来说,要小很多。 等我进了SBE以后,我发现我们code base里面很多singleton的代码都是线程不安全的,一把锁也没有,于是我问徐辰,这小子诡异的朝我笑了笑~~这件事就算过去了。直到昨天,我收到一个bug。大概意思是在数据库里面出现了两条记录相同的记录(除了UID不同,UID是我们设置的auto-increase的主键),应该只有一条的。仔细看了以后,发现原因是一样的,当这个存储过程被两个线程同时调用的时候insert被调用了两次。于是修改过的代码如下: SQL版的 … 繼續閱讀

发表在 eBay 学习笔记 | 2条评论

模拟Perforce中的ChangeList

以前在NI的时候版本管理使用Perforce,Developer在Fix任何Bug的时候强制在CAR里面填上ChangeList ID,这样以后找起来谁做了这个Fix,更改了哪些文件,每个被更改文件都做了什么样的改动都非常方便。eBay使用clearcase做版本管理,问题是我们的clearcase没有购买changelist的feature。还有就是eBay的Bug系统不强制提交改动文件列表。 于是让我很不爽的是:站点上出了什么Issue,美国那边在查,然后他们就发封信说fix了,就给了一个新的Build ID,我们这里好像云里雾里一样。不知道他们做了什么改动,不知道他们trouble shooting的过程。而且很多feature我们都不熟悉的。将来我们独立带Pager以后怎么办? Edward同学有一个临时的方法,虽然慢一点,但是也管用的。有一样东西我们是拿得到的,就是Build ID。存在问题的build id和fix后的build id。在内部build系统上给定build id可以查到该build的configuration specification。 1. 首先新建两个dynamic view-bash-3.00$ cd views-bash-3.00$ cleartool mkview -tag jianxu_cmp1 cmp1-bash-3.00$ cleartool mkview -tag jianxu_cmp2 cmp2-bash-3.00$ lscmp1      cmp2  2. 给这两个view分别输入被对比的两个build的config spec-bash-3.00$ cleartool startview jianxu_cmp1-bash-3.00$ cd /view/jianxu_cmp1-bash-3.00$ cleartool edcs   –>输入不带bug fix的build的config … 繼續閱讀

发表在 eBay 学习笔记 | 发表评论

如何用C++封装线程安全的Signal Handler

任务情景: 有一个HTTP的Server,开了100个工作线程来响应请求;任何一个线程出问题,都不希望影响到其他线程;任何一个线程出了问题(比如SEGV),都能自恢复到健康状态并且 log下出错时的函数调用栈,方便程序员定位错误。 代码设计: 在Unix上要逮住SEGV(指针越界后发生访问违规就会引发SEGV),需要安装信号处理函数。我们需要告诉程序要为哪些信号安装信号处理函数,处理函数的地址是什么,健康恢复点是哪里–>引入一个SignalManager类来实现这些功能。因为信号处理函数中需要用到不少私有数据,一般C++的做法就是写一个SignalHandler类来封装所有的操作。 但是这里有一个问题:我们需要这100个线程都独立工作,互不影响。如果我们让100个线程共享一个SignalHandler会有问题的。比如我需要打印内存违规时的函数调用堆栈,在SignalHandler里面我用成员变量m_callstack[1024]来保存,因为只有一份m_callstack,当有多个线程同时发生SIGSEGV的时候,m_callstack里面的数据就有可能乱掉的,除非对m_callstack使用mutex进行同步保护。但是这样会导致性能下降,为什么呢?因为SIGSEGV是同步信号,什么叫同步信号呢,就是发出SIGSEGV的线程会等待信号处理函数返回以后再往下执行。 同步信号的执行流程: 执行instruction的时候发生错误引发SIGSEGV–>保存当前指令PC–>执行信号处理函数–>恢复PC重新执行出错指令。这里注意两点: 如果没有安装SIGSEGV的处理函数,程序默认行为是引发core dump。这显然不符合我们的设计要求。 信号处理函数返回后会重新执行出错指令。这也不是我们需要的,我们想要出错线程跳到一个安全的点,我们使用siglongjmp。 那有人会说,把m_callstack做成TLS(Thread Local Storage)不就可以了吗?对的!这个思路很好,但是要干就干的彻底,我们干脆把SignalHandler放到TLS里面好了,每个工作线程有一个SignalHandler。 代码实现: class SignalManager{public:         SignalManager& Instance();  //单实例模式,我们只需要一个Manager来管理所有的SignalHandler就可以了        bool RegisterSignal(int signo, SignalHandler* sighandler); //信号signo使用sighandler实例来处理        …}; 关于RegisterSignal我要说明一下,我们可以为不同的signal使用相同的SignalHandler,这里要注意的一点是RegisterSignal必须在工作线程函数中调用,因为我们需要有100个SignalHandler实例,为同一种Signal为100个工作线程调用100次RegisterSignal,因为SignalHandler是保存在线程局部存储空间里面的。 bool RegisterSignal(int signo, SignalHandler* sighandler){       signal(signo, SignalManager::Dispatch);        if(pthread_once(&SignalManager::m_once,SignalManager::Init)!=0) return false; … 繼續閱讀

发表在 eBay 学习笔记 | 发表评论

青春就是这样流逝的!

我的青春就在这些狗屁烂程序上无情的挥霍了。是的!狗!屁@烂~程##序!为什么说“挥霍”,因为它的行为总是“莫名其妙”,完全看RP的。最最不爽的是当你耐着性子调啊调,打log,重新编译,…,重新运行,等待事故重现…一天一天… 不管怎样,我还是记录下来,说不定是黎明前的黑暗呢。虽然我没有什么信心…整个故事是这样的,按时间顺序排列: 几个月前,我开始做一个小项目,大家都认为只要发点Capacity Ticket,把代码在x86的机器上重新编译一下,然后写个部署脚本就完事了。1.3个TS。 我早早得就干完了,在Dev机器上试了,没有问题。 等到QA测试了,Core Dump! 直接一个P2塞到我嘴巴里。诶,在Dev机器上我编译的时候用了OPT=-g,结果是Dev Build是没有-O3编译的。于是我知道一样的代码转到x86后,就不行了。 同样的代码,同样的编译选项,sparc上是好的,x86是坏的。sparc上是gcc2.4, x86是gcc 4.0。但是在x86上去掉-O3就好了。应为只有-O3的时候Core Dump,看call stack,看purify都没有收获,因为是O3优化的代码,跟源代码根本对不起来。看汇编的话,我没有那个本事,而且是 O3后的汇编,根本就是天书。 我弄了一个多礼拜,搞不定。XuChen说何必和编译器过不去呢。领导怀疑是代码的原因,sparc上不出问题的原因是:我们运气好!于是我开始求救,拉着SJC的人一起看,仍然没有收获。 3个礼拜的人力下去,找不出来。领导终于同意将没有-O3编译的代码roll 到 production。 MIT和PD都烦透了这个烂程序,内存高,程序负荷重。于是我花了5天引入了一个机制,怀着很好的期待,有了这个机制,程序再也不会Looping了,于是就可以下调每一轮的数据处理量,于是内存就下来了,于是可以节省机器了,于是省下了一笔钱…我当时甚至还在想,我做了这个Fix,贡献还蛮大的。 QA在进一步的测试中发现 程序跑着跑着,内存就越来越高,于是另一个P2塞到我鼻子里面,Memory Leak。QA又发现 程序跑跑就会自动退出,QA还发现 程序跑着跑着 时间戳会丢失, log会停止工作,于是P2塞满了我的五官!大哥,我就是在x86上重新编译了一遍啊,我没有改和内存打交道的代码啊… 我最怕这种行为古怪的问题了,而且重现的时候要等,对的,要等,等上个2个小时,才会再次出现。于是我怀疑是32位程序的内存接近4GB的时候就中风了!于是我找啊找,最后发现用connection pool可以把程序在初始化期间耗用的内存降下来,于是那些鬼怪的问题的QA环境不出现了,就算偶然出现,我发现QA的用例和producton不一样。好说歹说之下,将其余P2关掉。剩下Core Dump和Memory Leak! 之间来来回回很多次。在 Tech Lead的干涉下,QA终于Sign Off。 第一次上Production,最主要的SQL就超级慢,知道什么叫超级吗,就是原来只要4分钟就能跑完的任务现在跑了30个小时还没有回来。4天以后,这个问题解决了,原因是一条DB Hint。 第二次上Production,发现程序处理速度比原来慢。MIT的解释是因为下调了每一轮的处理量,内存是下来了,但是平均到每一条记录的时间反而上升,而且我那个搞定Looping的改动,增加了程序逻辑的复杂性(参见7)。这里面牵涉的环节有数据库,网络传输和应用程序,实验证明,增加单轮处理量,确实减少了处理时间。但是这样内存就降不下来,我那个Fix不是白搞了嘛!于是我做了两件事:a)给DBA再发一个Ticket,我要数据库的处理时间,我要知道时间是耗在哪个环节的 b)从pfiles的结果看,我判断很多时间是耗在I/O等待上,于是我把程序改成多线程的了。 将数据库读操作放到单独的线程中理论上应该能够减少总时间,因为并发了,而且CPU … 繼續閱讀

发表在 eBay 学习笔记 | 7条评论

eBay Architecture(24)–Racap

至此,Randy的关于eBay Architectural Principles的介绍都"翻译"完了,我从上个周末开始的,每天写一点,还是花了不少时间的。在重新看这些Slides的时候自己也很有收获,网上的Slides一共25张,除了最后一张Q&A以外,我一共写了24篇BLOG,是严格得按照Randy的PPT写的,一一对应。我应该没有透露不该透露的东西,也希望没有误导大众。最后,Recap,一共就是4句话,整个presentation都是围绕这4句话在谈: Strategy 1: Partition Everything Strategy 2: Async Everywhere Strategy 3: Automate Everything Strategy 4: Remember Everything Fails 不废话了,整个关于eBay Architecture的介绍到此暂告结束。 Thanks Randy~~

发表在 eBay 学习笔记 | 发表评论

eBay Architecture(23)–Everything Fails[Resource Markdown]

Pattern:Failure Detection 前面已经提到过Failure Detection,那次给出的例子是如何利用Central Logging进行检测Application的问题。其实这里的基本思路是一样的,如何检测资源的Availability,比如数据库资源。 Application detects when database or other backend resource is unavailable ordistressed “Resource slow” is often far more challenging than “resource down” (!) 这里用数据库资源作为例子,应用程序发起数据库操作,失败了,但是一次失败很有可能是有偶然因素造成的。于是应用程序试图重试,如果重试一定次数以后还是返回同样的错误,我们有理由怀疑该数据库资源已经unavailable了,于是将该数据库资源标记为"不可用",同时报警。应用程序不会向标记为不可用的数据库发送数据库操作。 Randy特地提到了,资源使用速度非常慢要比资源彻底不可用难处理的多!所以在写应用程序的时候要注意这一点! [注:]我这里倒是有一个问题:资源标记为不可用以后,由谁负责再将资源标记为可用呢,还有就是在资源标记为不可用以后,应用程序的行为是怎样的? Pattern:Graceful Degradation 其核心思想是我们不希望"部分"失效导致"整体"失效。 Application “marks down” the resource Stops making … 繼續閱讀

发表在 eBay 学习笔记 | 发表评论

eBay Architecture(22)–Everything Failes[Feature Wire-on/Wire-off]

Pattern: Rollback 就模式来说,Feature Wire-on/Wire-off还是采用了roll-back的思想。我们看两种情况: 如果现在在新的版本的应用程序中新增了2个Feature,A和B,上线后发现其中有一个Feature工作不正常,或者由于Operation和Business的原因,希望禁用其中的Feature A,如果采用code roll-back的方法,A和B就会同时失效,如何禁用A,而保留B。 如果Pool A和Pool B同时有个新Feature要上线,但是Pool A和Pool B在新版本中的Feature互相依赖,如果完整的roll-out一个Pool需要1天,那无论是先roll pool A还是先roll pool B就不能解决依赖问题。 解决提出的两个问题是非常典型的两个问题,解决的思路就是:将代码部署和Feature部署脱离! Decouples code deployment from feature deployment 在具体实现上,我们为每一个Feature设定一个"软"开关,然后统一在一个地方设置所有Feature的开关状态。 Every feature has on / off state driven by central configuration Allows feature to be immediately … 繼續閱讀

发表在 eBay 学习笔记 | 发表评论