• Email:
  • Feeds

  • Posts Tagged ‘memcached’


    Twitter“鲸鱼”故障技术剖析

    很多人都熟悉Twitter访问故障时候那条白色的鲸鱼。今年新推出的Twitter Engineering Blog讲述了Twitter白鲸技术故障的原因及解决思路。这是到目前为止Twitter公开的最底层的一篇技术资料。
    http://engineering.twitter.com/2010/02/anatomy-of-whale.html

    当Web Server发生503错误后,Twitter配置了一个前端鲸鱼的显示页面。Twitter对鲸鱼页面有监控体系,当每秒超过100个鲸鱼就会引起报警。

    为什么在单位时间内会有大量的”fail whale”呢?Twitter成立了一个小组来专门分析此原因。

    1. 分析背景资料

    “分析性能问题不是一门科学,而是一门艺术”。

    鲸鱼页面实际上是对HTTP 503错误的前端展示,503错误通常是调用后台请求超时产生,为了避免用户长时间等待,Twitter的前端(Tim: 也可能是HTTP反向代理)给请求加了超时,避免用户无限制的等待。超时通常是由于单位时间内访问的用户数过大,也有可能是后台某个服务突然变慢造成。
    由于Twitter网站每个时刻都有海量的数据流过,因此要简单的定位并解决此问题并不容易。

    2. Web page请求分解

    Twitter的页面请求后端分成2个阶段,在Twitter内部称为IO phase及CPU phase。IO phase指通过网络服务获取用户的关注关系及相关的Tweets。第2阶段为CPU phase,指将数据聚合、排序及按用户请求的条件输出。IO及CPU各自在1天内消耗的时间如下。

    从图上看到,latency增大时IO是主要瓶颈。IO对应于Network service,因此可以判断是某个网络服务性能降级造成。

    3. 深度分析

    理想情况是网络服务在应答相同参数的请求消耗时间应该基本相同。但实际情况并非如此,我们大胆假设某一网络服务性能下降厉害,于是我们就从统计分析中去寻找这个服务,我们看到Memcached的统计图表如下

    4. Memcached 竟然是鲸鱼故障的直接原因

    可提高的空间及解决思路

    1. 从上图看,Memcached在 latency高峰的性能比低谷相差一倍,因此最简单的判断是增加硬件即可提高50%的性能。
    2. 另外一种思路就是优化Memcached程序,判断程序热点和瓶颈并进行优化。

    分析

    1. 通过 Google perf-tools project 工具来分析, http://code.google.com/p/google-perftools/ http://github.com/tmm1/perftools.rb
    2. 通过自己些的一段分析代码来监控 http://github.com/eaceaser/ruby-call-graph
    3. 通过上面工具的call graph来分析热点和瓶颈

    最后分析数据Memcached请求分布比例如下

    get         0.003s
    get_multi   0.008s
    add         0.003s
    delete      0.003s
    set         0.003s
    incr        0.003s
    prepend     0.002s
    
    get         71.44%
    get_multi    8.98%
    set          8.69%
    delete       5.26%
    incr         3.71%
    add          1.62%
    prepend      0.30%
    

    结论:从上面数据来看,调用热点和瓶颈主要集中在Get操作

    因此回头取看Twitter页面执行流程代码,找出优化方法见注释。

    get(["User:auth:missionhipster",              # 将昵称转换成uid
    get(["User:15460619",                         # 获取user object(用于检查密码)
    get(["limit:count:login_attempts:...",        # 防止密码字典攻击
    set(["limit:count:login_attempts:...",        # 大部分情况不需要, bug
    set(["limit:timestamp:login_attempts:...",    # 大部分情况不需要, bug
    get(["limit:timestamp:login_attempts:...",
    get(["limit:count:login_attempts:...",        # 重复调用,可记住
    get(["limit:count:login_attempts:...",        # 重复调用
    get(["user:basicauth:...",                    # 防止解密的优化
    get(["limit:count:api:...",                   # 请求数限制
    set(["limit:count:api:...",                   # 设置请求数,大部分情况不需要,为什么?
    set(["limit:timestamp:api:...",               # 大部分情况不需要, bug
    get(["limit:timestamp:api:...",
    get(["limit:count:api:...",                   # 重复调用
    get(["home_timeline:15460619",                # home_timeline业务调用
    get(["favorites_timeline:15460619",           # favorites_timeline业务调用
    get_multi([["Status:fragment:json:74736693",  # multi_get所有tweets内容
    

    上面这段代码将17个请求优化成10个,部分重复调用通过本地cache避免,另外一些没必要的调用直接删除。通过一个简单的优化性能就提高了42%。

    结论

    1. 在前文2010年的技术架构建议中提过Cache已经是Web 2.0系统核心元素。从Twitter的故障案例来看Memcached竟然成为了瓶颈并导致了Twitter服务的不稳定。由于在social应用中cache核心化的设计,“RAM is the new disk”,在cache广泛使用后也变得调用成本增加,需要考虑进行系统的规划减少不必要的调用。避免开发人员在代码中随意使用cache
    2. 如何定位瓶颈,可以借鉴Google perf-tools项目及上面其他分析工具的思路。
    3. Twitter页面执行流程值得参考
    4. 整个故障流程分析图如下

    Twitter系统运维经验

    最近看到的另外一个介绍Twitter技术的视频[Slides] [Video (GFWed)],这是Twitter的John Adams在Velocity 2009的一个演讲,主要介绍了Twitter在系统运维方面一些经验。 本文大部分整理的观点都在Twitter(@xmpp)上发过,这里全部整理出来并补充完整。

    Twitter没有自己的硬件,都是由NTTA来提供,同时NTTA负责硬件相关的网络、带宽、负载均衡等业务,Twitter operations team只关注核心的业务,包括Performance,Availability,Capacity Planning容量规划,配置管理等,这个可能跟国内一般的互联网公司有所区别。

    1. 运维经验

    * Metrics

    Twitter的监控后台几乎都是图表(critical metrics),类似驾驶室的转速表,时速表,让操作者可以迅速的了解系统当前的运作状态。联想到我们做的类似监控后台,数据很多,但往往还需要浏览者做二次分析判断,像这样满屏都是图表的方法做得还不够,可以学习下这方面经验。 据John介绍可以从图表上看到系统的瓶颈-系统最弱的环节(web, mq, cache, db?)
    根据图表可以科学的制定系统容量规划,而不是事后救火。Twitter operation dashboard

    * 配置管理

    每个系统都需要一个自动配置管理系统,越早越好,这条一整理发到Twitter上去之后引起很多回应。

    * Darkmode

    配置界面可以enable/disable 高计算消耗或高I/O的功能,也相当于优雅降级,系统压力过大时取消一些非核心但消耗资源大的功能。

    * 进程管理

    Twitter做了一个”Seppaku” patch, 就是将Daemon在完成了n个requests之后主动kill掉,以保持健康的low memory状态,这种做法据了解国内也有不少公司是这样做。

    * 硬件

    Twitter将CPU由AMD换成Xeon之后,获得30%性能提升,将CPU由双核/4核换成8核之后,减少了40%的CPU, 不过John也说,这种升级不适合自己购买硬件的公司。

    2. 代码协同经验

    * Review制度

    Twitter有上百个模块,如果没有一个好的制度,容易引起代码修改冲突,并把问题带给最终用户。所以Twitter有一强制的source code review制度, 如果提交的代码的svn comment没有”reviewed by xxx”, 则pre-commit脚本会让提交失败, review过的代码提交后会通过自动配置管理系统应用到上百台服务器上。 有@xiaomics同学在Twitter上马上就问,时间成本能否接受?如果有紧急功能怎么办?个人认为紧急修改时有两人在场,一人修改一人review也不是什么难事。

    * 部署管理

    从部署图表可以看到每个发布版本的CPU及latency变化,如果某个新版本latency图表有明显的向上跳跃,则说明该发布版本存在问题。另外在监控首页列出各个模块最后deploy版本的时间,可以清楚的看到代码库的现状。

    * 团队沟通

    Campfire来协同工作,campfire有点像群,但是更适合协同工作。对于Campfire就不做更多介绍,可参考Campfire官方说明。

    3. cache

    • Memcache key hash, 使用FNV hash 代替 MD5 hash,因为FNV更快。
    • 开发了Cache Money plugin(Ruby), 给应用程序提供read-through, write-through cache, 就像一个db访问的钩子,当读写数据库的时候会自动更新cache, 避免了繁琐的cache更新代码。
    • “Evictions make the cache unreliable for important configuration data”,Twitter使用memcache的一条经验是,不同类型的数据需放在不同的mc,避免eviction,跟作者前文Memcached数据被踢(evictions>0)现象分析中的一些经验一致。
    • Memcached SEGVs, Memcached崩溃(cold cache problem)据称会给这种高度依赖Cache的Web 2.0系统带来灾难,不知道Twitter具体怎么解决。
    • 在Web层Twitter使用了Varnish作为反向代理,并对其评价较高。

    Memcached数据被踢(evictions>0)现象分析

    很多同学可能熟知Memcached的LRU淘汰算法,它是在slab内部进行的,如果所有空间都被slabs分配,即使另外一个slab里面有空位,仍然存在踢数据可能。你可以把slab理解为教室,如果你的教室满了,即使别的教室有空位你的教室也只能踢人才能进人。

    mc

    本文介绍的却是另外一种现象。今天监控发现线上一memcached发生数据被踢现象,用stats命令看evictions>0,因为以前也出现过此问题,后来对这个参数增加了一个监控,所以这次主动就发现了。由于给memcached分配的内存远大于业务存储数据所需内存,因此初步判断是“灵异现象”。

    第一步,netstat查看所有连接,排除是否被一些未规划的client使用,经排查后断定无此可能。

    第二步,用tcpdump抽样检查set的指令,排除是否有忘记设cache过期时间的client,初步检查所有典型的业务都有expire time。

    第三步,Google,未果

    第四步,看源代码,了解evictions计数器增加时的具体细节,oh, no…

    in items.c, memcached-1.2.8,

    125         for (search = tails[id]; tries > 0 && search != NULL; tries--, search=search->prev) {
    126             if (search->refcount == 0) {
    127                 if (search->exptime == 0 || search->exptime > current_time) {
    128                     itemstats[id].evicted++;
    129                     itemstats[id].evicted_time = current_time - search->time;
    130                     STATS_LOCK();
    131                     stats.evictions++;
    132                     STATS_UNLOCK();
    133                 }
    134                 do_item_unlink(search);
    135                 break;
    136             }
    137         }

    从源代码发现踢数据只判断一个条件,if (search->refcount == 0),这个refcount是多线程版本计数用,在当前服务器未启用多线程情况下,refcount应该始终为0,因此初步判断memcached是从访问队列尾部直接踢数据。

    为了证实想法,设计以下场景:

    1. 部署一个memcached测试环境,分配比较小的内存,比如8M
    2. 设置1条永远不过期的数据到memcached中,然后再get一次,这条数据后续应该存在LRU队尾。
    3. 每隔1S向memcached set(并get一次) 1,000条数据,过期时间设为3秒。
    4. 一段时间后,stats命令显示evictions=1

    按我以前的理解,第2步的数据是永远不会被踢的,因为有足够过期的数据空间可以给新来的数据用,LRU淘汰算法应该跳过没过期的数据,但结果证实这种判断是错误的。以上业务的服务器发生被踢的现象是由于保存了大量存活期短的key/value,且key是不重复的。另外又有一业务保存了小量不过期的数据,因此导致不过期的数据惨遭被挤到队列踢出。

    本来这个问题就告一段落了,但在写完这篇文章后,顺便又看了新一代memcached 1.4.1的源代码,很惊喜发现以下代码被增加。

    items.c, memcached 1.4.1

    107     /* do a quick check if we have any expired items in the tail.. */
    108     int tries = 50;
    109     item *search;
    110
    111     for (search = tails[id];
    112          tries > 0 && search != NULL;
    113          tries--, search=search->prev) {
    114         if (search->refcount == 0 &&
    115             (search->exptime != 0 && search->exptime < current_time)) {
    116             it = search;
    117             /* I don't want to actually free the object, just steal
    118              * the item to avoid to grab the slab mutex twice ;-)
    119              */
    120             it->refcount = 1;
    121             do_item_unlink(it);
    122             /* Initialize the item block: */
    123             it->slabs_clsid = 0;
    124             it->refcount = 0;
    125             break;
    126         }
    127     }

    重复进行上述测试,未发生evictions。

    9/8 Update: 注意到L108的tries=50没有?试想把测试第2步设置51条不过期数据到cache中,情况会怎样?因此新版的Memcached也同样存在本文描述问题。

    几条总结:

    • 过期的数据如果没被显式调用get,则也要占用空间。
    • 过期的不要和不过期的数据存在一起,否则不过期的可能被踢。
    • 从节约内存的角度考虑,即使数据会过期,也不要轻易使用随机字符串作为key,尽量使用定值如uid,这样占用空间的大小相对固定。
    • 估算空间大小时候请用slab size计算,不要按value长度去计算。
    • 不要把cache当作更快的key value store来用, cache不是storage。