• nginx 健康检查和负载均衡机制分析

    日期:2012-05-11 | 分类:编程

    nginx 是优秀的反向代理服务器,这里主要讲它的健康检查和负载均衡机制,以及这种机制带来的问题。所谓健康检查,就是当后端出现问题(具体什么叫出现问题,依赖于具体实现,各个实现定义不一样),不再往这个后端分发请求,并且做后续的检查,直到这个后端恢复正常。所谓负载均衡,就是选择后端的方式,如何(根据后端的能力)将请求均衡的分发到后端。此外,当请求某个后端失败时,要将该请求分发到其它后端(redispatch)。这里以ngx_http_upstream_round_robin(简称RR)做为负载均衡模块,以ngx_http_proxy_module(检查proxy)作为后端代理模块。

     

    nginx 的健康检查和负载均衡是密切相关的,它没有独立的健康检查模块,而是使用业务请求作为健康检查,这省去了独立健康检查线程,这是好处。坏处是,当业务复杂时,可能出现误判,例如后端响应超时,这是可能是后端宕机,也可能是某个业务请求自身出现问题,跟后端无关。如果后端宕机,nginx还要在将它标记为不可用之后,仍不时的将业务请求分发给它,以检查后端是否恢复。

     

    nginx 完成客户端请求Header部分的解析,upstream 调用RR模块的peer.get 选择具体的后端。当请求结束,upstream 调用RR模块的peer.free,向RR反馈后端的健康情况。当upstream和后端通信时,出现错误会调用 ngx_http_upstream_next,

    void ngx_http_upstream_next(ngx_http_request_t *r, ngx_http_upstream_t *u, ngx_uint_t ft_type); 第三个参数指明错误类型,共有如下错误类型

     

    #define NGX_HTTP_UPSTREAM_FT_ERROR           0x00000002

    #define NGX_HTTP_UPSTREAM_FT_TIMEOUT         0x00000004

    #define NGX_HTTP_UPSTREAM_FT_INVALID_HEADER  0x00000008

    #define NGX_HTTP_UPSTREAM_FT_HTTP_500        0x00000010

    #define NGX_HTTP_UPSTREAM_FT_HTTP_502        0x00000020

    #define NGX_HTTP_UPSTREAM_FT_HTTP_503        0x00000040

    #define NGX_HTTP_UPSTREAM_FT_HTTP_504        0x00000080

    #define NGX_HTTP_UPSTREAM_FT_HTTP_404        0x00000100

    #define NGX_HTTP_UPSTREAM_FT_UPDATING        0x00000200

    #define NGX_HTTP_UPSTREAM_FT_BUSY_LOCK       0x00000400

    #define NGX_HTTP_UPSTREAM_FT_MAX_WAITING     0x00000800

    #define NGX_HTTP_UPSTREAM_FT_NOLIVE          0x40000000

     

    ngx_http_upstream_next,只要错误类型不是 NGX_HTTP_UPSTREAM_FT_HTTP_404,都认为后端有问题(NGX_PEER_FAILED)

    if (ft_type == NGX_HTTP_UPSTREAM_FT_HTTP_404) {                                                              

        state = NGX_PEER_NEXT;                                                                                   

    } else {                                                                                                     

        state = NGX_PEER_FAILED;                                                                                 

    ngx_http_upstream_next 调用RR的peer.free,RR根据state判断刚才接受请求的后端是否健康。

    if (ft_type != NGX_HTTP_UPSTREAM_FT_NOLIVE) {

        u->peer.free(&u->peer, u->peer.data, state);

    }

     

    ngx_http_upstream_next 如果超过最大重试次数(默认为后端的个数,每试过一个,就减1),或者proxy设置不允许redispatch,则向客户端返回响应status。

    if (u->peer.tries == 0 || !(u->conf->next_upstream & ft_type)) {

        ngx_http_upstream_finalize_request(r, u, status);

    }

    proxy 模块的 proxy_next_upstream 配置,在何种情况下将请求redispatch到下一个后端。

     

    刚刚谈到,只要错误类型不是 NGX_HTTP_UPSTREAM_FT_HTTP_404,都认为后端有问题。这里的错误类型包括,连接后端失败,连接,读写后端超时,后端返回了500,502,504等。这个策略是有待商榷的,尤其是读写后端超时也判断为后端不可用。因为某个业务请求,可能因为自身的原因而导致读写超时。注意,在proxy_next_upstream 中指定timeout,http_504 是不同的,前者表示upstream连接,读写后端超时,后者表示后端返回的http code 是504。

     

    实际上健康检查不是必须的,因为redispatch的存在保证了,就算有后端宕机,客户端仍将收到正确的响应。那么我们考虑关掉健康检查。通过upstream 的server配置的max_fails 参数

     

    RR 的peer.get,如果max_fails 为0,则该后端总是可用的(就算它真有问题)。

    if (peer->max_fails == 0

        || peer->fails < peer->max_fails)

    {   

        break;

     

    因为redispatch的次数,取决于后端的个数,所以后端的个数稍微多一点是有好处的。

     

    下面是一些佐证分析的测试。

     

    upstream test {

        server 127.0.0.1:8060 max_fails=0;

        server 127.0.0.1:8070 max_fails=0;

        server 127.0.0.1:8080 max_fails=0;

        server 127.0.0.1:8090 max_fails=0;

    }

    只有8060,8070是存活的,8080,8090处于不可用状态,这里max_fails=0,关闭了健康检查。

     

    proxy_read_timeout 2;

    读超时设为2S。

     

    proxy_next_upstream error timeout;

    默认当 error 和 timeout发生时,redispatch。

     

    测试请求的sleep参数指定后端的sleep时间,code参数指定后端返回的http code。根据time和sleep时间的对比,判断重试了几个后端。

     

    time curl "http://127.0.0.1:8099/index.php?sleep=3" -vv

    real    0m4.014s

    sleep=3,读超时,重试了2个后端。

     

    修改配置 proxy_next_upstream error;

     

    time curl "http://127.0.0.1:8099/index.php?sleep=3" -vv

    real    0m2.018s

    读超时,不再redispatch,重试了1个后端。

     

    修改配置 proxy_next_upstream error http_504;

     

    time curl "http://127.0.0.1:8099/index.php?sleep=1" -vv

    real    0m1.022s

    这个是正常请求。

     

    time curl "http://127.0.0.1:8099/index.php?sleep=1&code=504" -vv

    real    0m2.023s

    让后端返回504,此时nginx会做redispatch,重试了2个后端

    但是nginx返回给客户端的是502,不是504,因为所有的后端都返回504,nginx认为后端不可用,返回502.

     

    测试健康检查,关掉redispatch。proxy_next_upstream off;

     

    curl "http://127.0.0.1:8099/index.php?sleep=3" -vv

    返回了两次502,两次504。存活的后端返回504,有问题的返回502。

     

    修改 max_fails server 127.0.0.1:8060 max_fails=1; 对8060开启健康检查。

     

    curl "http://127.0.0.1:8099/index.php?sleep=3" -vv

    第一轮4次请求,返回两次502,两次504

    8080和8090有问题,返回502,8060和8070响应超时,返回504,因为8060开启了健康检查,并且返回了504,所以被标记为不可用。

    第二轮4次请求,返回三次502,一次504。8070没有开启健康检查,所以仍然返回504。

     

    根据测试分析,业务请求(sleep 3s,或者 输出 http 504)可以让nginx误以为后端宕了,而这时后端活得好好的。在私有云平台,这个通常不是问题,把超时设大点,不返回5XX错误,可以避免这个问题。但是在公有云平台,这是致命的,因为业务可以编程输出5XX错误。有两种方法应对,一种是关闭健康检查,一种是修改nginx的代码,仅对 NGX_HTTP_UPSTREAM_FT_ERROR 判定为后端有问题。

  • 精确排重和初略排重

    日期:2012-04-25 | 分类:编程

    1. 最简单且现成的排重技术,是std::set。std::set 采用红黑树。红黑树的查找效率是O(log(n))。额外内存占用,三个指针(父节点,左右子树)外加节点颜色,在64位下占用内存25个字节。如果是对uint32坐排重,那么有效内存使用不到14%。

    2. 使用hash,很多stl 都提供了std::hash_set。hash的查找效率是O(1)。额外内存占用,一个指针(假设用开链法解决冲突),在64位下占8个字节。如果是对uint32做排重,那么有效内存使用不到34%。

    这两种方法都是精确排重,并且可以遍历排重结果。我之所以用uint32计算有效内存,是为了说明另一个问题,初略排重,初略排重允许一定的误判,例如统计网站的uip。少统计5%,并不会有很大影响。这时可以用单向散列函数(crc32)将uip(通常是一个cookie)映射为一个uint32的值,这就减少了排重使用的内存。映射有冲突,冲突程度取决于使用的散列函数和集合中元素的个数。在不要求精确的情况下,这种方法很管用。

    3. 使用BloomFilter。BloomFilter有误判率,并且无法删除单个元素。优点是:效率高(多线程里面,在加锁前算出hash序列),内存占用少。统计网站uip,通常每天全部清0,这就没有删除要求,允许误差,BloomFilter的误判率也不是问题。

    从1到3,可以视为一个程序的优化过程,关键是把握需求的特征,考量实现难度,内存使用,效率等指标,选择最合适的算法。
  • 网卡多队列/irqbalanc/cpu亲和度

    日期:2012-04-18 | 分类:编程

    memcached 服务器面临两个问题,1. 单个cpu 的si过高,在75%以上,2. 千兆网卡带宽跑到50%以上。

    如果访问量继续增大,在网卡跑满以前,单个cpu的si将先达到100%,遭遇瓶颈。所以我的想法是,让其它cpu来分担它的中断,没办法提高吞吐量,但是可以避免单cpu的瓶颈。网上给出了两个办法,irqbalance 和 绑cpu 亲和度。不幸的是,这两个办法都不管用。内核版本是 2.6.18,原因是从2.6.13起,内核去除了irqbalance 和 cpu亲和度的支持,因为cpu cache失效和tcp包乱序问题。

    现在的网卡都是支持多队列的,内核自 2.6.35 通过RPS支持网卡多队列,问题完美解决,不需要我们在折腾什么了。多队列网卡 + RPS 简单说,网卡本身分成多个接收队列(中断号不同),根据tcp四元组hash(这个计算由网卡完成,不牵扯到cpu)到不同的队列,四元组保证了tcp包不会乱序,cpu也没有cache失效问题。

    我们的服务器网卡支持多队列,只是内核需要升级,在不重启memcached的情况下升级内核,就算可以,估计也很复杂。通常机器都是有两块网卡的,我一直很好奇,为什么所有的内网通信都只用其中的一块儿。同一个网段,两块网卡ping没有区别,打算将两块网卡都排上用场。
  • 程序两三事(16)

    日期:2012-04-18 | 分类:编程

    1. 想不明白,为什么memcached采用多线程方法。为了性能,使用和mysql类似的多进程多实例就很好呀。多实例增加了连接数的消耗,这是个问题,但不是个大问题。

    2. 调试后端web服务器的一个模块,使用nginx做代理访问和直接访问,curl看到的结果不同。curl完全模拟了nginx发送的HTTP  Header。可以肯定,这是web模块的问题。但为什么通过nginx访问和直接访问,结果不同呢?这涉及到nginx和curl作为客户端访问后端时,他们对http协议的解析不同。nginx 忠于web服务器,返回了所有它从后端读到的数据,而curl做了处理,没有指定Content-Length的非chunked编码,以两个\r\n判断HTTP Body结束,丢且后面的内容。查找这个问题,费了一点时间,下次可以通过tcpdump抓包,直接看原始数据,问题定位更快。

    3. dmidecode 查看服务器硬件信息。

    4. lspci 列出所有的pci设备。

    5. /proc/interrupts 查看网卡中断情况

    6. libmemcached 的操作 memcached_behavior_set(mc, MEMCACHED_BEHAVIOR_DISTRIBUTION, MEMCACHED_DISTRIBUTION_CONSISTENT); 是一个很耗时的操作,根据添加的server的个数,耗时会有不同。在我的机器上,有16个server的调用要7ms左右。这个通常不是问题,但是在libmemcached的php扩展中,这可能是致命的,每次操作memcache都调用这个函数,意味着每个请求有7ms的cpu是无法避免的消耗。7ms相当于php程序的4万次加法。
  • 关于竞争条件,记一次故障处理

    日期:2012-04-18 | 分类:编程

    为了省点内存,最近升级了pvuip统计系统,采用BloomFilter代替std::set。这次升级准备非常充分,旁路3天,运行正常。升级后,就没管它。

    今天早上查看报表,发现自昨天升级之后,就没有统计数据。真是晴天霹雳。日志显示统计没问题,统计数据写入文件也没有报错,但是为什么文件中没有昨天的数据呢?线上gdb,发现写文件的相关函数根本就没有调用(怪不得没有报错呢)。情急之下,只好先重启程序,重启后有统计数据写入了。程序bug是不可能通过重启解决的,问题出在哪呢?

    因为升级前,系统空闲内存很少,所以怀疑重启时写统计数据的线程没有启动成功(多可笑,升级前先停老程序,内存就释放了,跟空闲内存很少有什么关系呢)。查看线程数,不是预期中的101,而是53。线程没有启动?不可能,如果没启动直接就抛异常了。gdb发现,线程启动之后,很快就自动退出了。检查线程执行逻辑,发现了一个竞争条件。

    线程池启动函数,在创建所有的线程后,会将线程池的状态置为Run,在这之前线程池的状态未初始化。而线程池中的线程根据线程池的状态是否为Run,判断线程是否退出。因为线程池要启动很多线程,因为有很多cpu,所以在线程池设置状态为Run之前,很多线程运行,并退出。但是大多数时候,线程池里面的线程不会全部退出,所以程序可以正常运行(并在系统负载重时,因为线程数不足而出问题)。只是昨天比较极端,线程池里面的线程全部退出了。

    教训:1. 注意竞争条件。2. 查看进程状态时,查看线程个数是否和预期一致。3. 查看程序接口/输出/日志,以结果为导向。
  • 4月3日 车耳营-大风口-电线塔穿越

    日期:2012-04-12 | 分类:往昔

    这是一次非常难忘的穿越,经历了生死考验。很久没见嗨哩和玲子了,他们都装备了手台,鉴于玲子同学的背包没办法挂手台,手台就挂在我包上,没有手台壮胆,我是断然不敢胡来的。

    从车耳营村走上去凉水背的公路,我们碰上了久违的看山大爷。大爷表示,封山时期,严禁上山。我表示,同意同意。继续套近乎,我表示我们身上都没有带火,不会引起火灾啥的。大爷铁面无私,就是不让进。我们只好往回走了几步,从边上的小道绕到大爷背后,上山了。

    要不是这次出来,我都不知道桃花开了。通往凉水背的山谷里,仍有厚厚的冰,可以站人跑马。路上碰到从凉水背下来的背水的大妈大爷,就问他们怎么通过看山大爷“防区”的,他们说,你就跟他磨,总可以过去。他们每周都来这里背水,一次背30升左右,这里是纯天然的泉水,煮粥,泡茶都别有滋味。想必是跟看山大爷混熟了。过了凉水背的水源头,切到右边的沟里,往大风口走。

    受厚厚的冰吸引,我们走到了只有冰没有路的地方。我踏过冰,去前面看路。路并不清楚,但是有人系的红绳,根据经验,这通常是有路的。走了100多米,也没看到更清楚的路。此时嗨哩和玲子往回走,找到了去大风口的路。我没打算往回走,我觉着沿着红绳,可以和嗨哩他们会合。我犯的第一个错误是,我知道路应该在右手边,但是我并没有刻意往右边靠。第二个错误是,没有红绳,也没有路时,没有果断回撤。

    走远了,就会有一种错觉,认为继续走“野路”上正路,和往回撤上正路的代价是一样的。越走就越发现这不是人走的路,也许这是路,是动物们走的路,据说这里有野猪。有时候还有似是而非的路,有时则完全是拨开藤枝开路。登上一块突出的大石头,我看见一条清晰的小路,只是我跟这条路隔着一条沟,要走过去是不可能的了。只能继续向上,只要到山顶,就有路了。我此时很忐忑,怕碰到野猪,虽然知道野猪不吃人。看见六道木的时候,我心中充满了希望,六道木只在海拔超过1000米的地方才有,说明我的位置已经很高了。当我奋力“到顶”的时候才发现,这只是一个局部的顶,真正的顶还有300米,登真正的顶之前,我还要先下一段。我绝望了,置身于无边的藤枝中,没有路,只有手台跟嗨哩联系。任何一个闪失,都可能让我长眠于此。不能继续往前走了,可是回撤的路在哪里呢?山都是有沟的,这个沟是下雨流水的地方。我快速找到了这个沟,并沿着沟往下走,沟比较好走,没有藤枝什么的挡道。这条沟肯定没人走过,我一脚踩到树叶上,树叶直没膝盖。我这时主要担心脚崴。沿着沟终于撤到山谷里,心算稍微平静了一下。

    继续沿着山谷,回到了和他们分手的地方。本来打算就此下山的,“野路”已经把我折腾的精疲力尽了。但是嗨哩他们也不打算走了,为了不影响他们,我决定去大风口追他们。终于在2点半左右追上了他们,他们正躺在路边晒太阳呢。继续的路就没什么特别了,只是今天的风特别大,大到跟本无法站立,大到玲子要拽着嗨哩才能走路。

    过了电线塔,才感觉不到风了,耳边没有了那种呼呼的风声,感觉好安静。到346车站,排队等车的人已经有100多米,可是有好几个人并排的100多米呀。我估计此时有人敢插队,都没人可以保证他的人身安全,非被唾沫淹死不可。我们排无座的队,还好,抢到两个车轱辘。还管形象,先坐下歇会再说吧。
  • 我怀念苹果园的煎饼。6点45我出苹果园地铁,花了10分钟排队买,此时离948开车还有5分钟,我跑着去车站的。单说公交,这一定是我坐过的最挤的公交,没有之一。快要清明了,有很多人提前去京郊扫墓,人比以往更多一些,何况,如果不坐头班车,就得等9点的那趟了。

    我没有座位,这并不算什么,因为在接下来的几站,将会有很多人连车都上不了。大山没赶上车,我们建议他打出租追公交,幸运的是出租车司机不知道948的线路,如果出租车追上了我们,大山还是上不了车,车上一点空间都没有了。大山对山有着很深的感情,他碰上了绿野的队伍,跟着他们从灰地上铁陀山,我们最终在山顶汇合,感谢手台。

    我对铁陀山比较畏惧。这是任老师失踪的地方,前年我从灰地上铁陀山,脚第一次负伤,至今未彻底痊愈。今天风很大,可以在1分钟内将一个大汗淋漓的人吹的手脚冰凉。这次,我登上了铁陀山顶,海拔超过1000米的山上很多都有个铁架子。

    这次穿越累计爬升1100米,行走27公里,使用了刚买的山杖,没感觉特别累。
  • calloc 和 malloc + memset 的区别

    日期:2012-04-11 | 分类:编程

    我以前一直以为calloc等于 malloc + memset,直到有一天。我的一个程序,使用calloc一次性分配500M内存,所以我估计程序启动后,内存使用应该在500M左右。但是没有,只有几百k,直到运行一段时间,内存使用才到500M左右,这简直是malloc的行为呀。

    使用malloc 500M的内存,操作系统只是分配了500M的虚拟地址空间,并没有真的内存分配发生,直到你访问相应的内存页,该内存页才会发生真实的内存分配。如果malloc后调用memset,相当于将内存访问了一遍,操作系统真实的分配了500M内存。因为我认为calloc等于malloc + memset,所以我认为calloc 500M内存,操作系统就会真的分配500M内存。

    和snnn讨论这个问题,有个大概的结论,不一定准确。这里有个事实,glibc 分配内存有两个来源,一是程序之前malloc(calloc),后来又free的,这部分内存里面的内容是未知的。二是从操作系统申请来的内存,这部分内存是必然初始化为0的。这样glibc在实现calloc时,如果可以从glibc的内存池分配时,相当于malloc + memset,如果从操作系统分配,相当于malloc(操作系统负责初始化)。如果是情况二,那么calloc了500M内存,相当于malloc了500M内存,操作系统并不会真的分配500M内存。

    这个过程可能还有别的细节。例如,main() { calloc(500 * 1024 * 1024, 1); },top 会显示操作系统分配了500M。main() { calloc(4 * 1024 * 1024, 1); calloc(500 * 1024 * 1024, 1); } 会显示操作系统分配了4M。估计glibc对第一次内存分配有特殊处理。
  • 程序两三事(15)

    日期:2012-04-05 | 分类:编程

    1. 做web服务托管,如果web文件(静态文件,动态脚本)是全冗余的,那么web相当于是无状态的,那么无论是扩容,还是迁移,都会非常容易。这里有个问题,如果有上百G的文件,Web服务蜕化成了小文件存储等价的形式,磁盘就会成为瓶颈(特别是托管的服务大多都有访问时),从这个角度来说,以组为单位做全冗余是比较合适的。

    2. 最近碰到一个core,一个不可能为野指针的指针(r)竟然出现了 Cannot access memory at address 0x7fff91a270a8。为了模拟这个错误,使用gdb跟踪,通过 p r=0 给 r 赋值为0,此时查看堆栈,r 为0,其它变量的值也在预料中。之后,用 gdb 调试 core 文件,就出现了 Cannot access memory at address,很多变量也变成了这样。这样的core文件,看不出什么有效信息。每次都是core到这个位置,可以判断内存写花的可能性不大,既然为野指针,应该就是这个位置访问的指针(ctx)没有初始化或赋了错误的值。就这个core而言,的确是ctx里面的某个指针没有初始化,(r) 是好的。

    3. “业务容器”的隔离性,以数据库为例,称数据库为“业务容器”。如果是企业内部的数据库集群,那么集群管理员很清楚数据库运行的业务,每个业务的上线/下线心中大概都是有数的,集群如果出现系统崩溃(core),也比较容易排查是什么业务导致的。如果是PaaS平台的数据库集群,跟本没法知道集群运行的是什么业务,如果出现系统崩溃(core),所有的PaaS用户都受到影响,并且无法排查是那个新增的业务导致了系统崩溃,问题排查无处下手。“业务容器”的隔离性是一个问题,如何保证某项“业务”导致“业务容器”崩溃,而不影响其它的“业务”。考虑web集群,缓存集群,数据库集群的分组,只能做到集群之间的隔离,集群内的所有“业务”还是受到“业务容器”崩溃的影响。PaaS只能尽量保证“业务容器”不崩溃,但是无法绝对保证。

    4. 提取rpm包中的文件 rpm2cpio memcached.x86_64.rpm  | cpio -id

    5. 正常的http请求,是基于应答式的,这隐含一个问题,对于POST请求,即便服务端要拒绝该POST请求,它也不得不将请求读完(请求体可能非常大),才能向客户端返回拒绝信息。在nginx模块中可以考虑NGX_HTTP_CLOSE代替NGX_HTTP_xxx(xxx是具体的错误),直接跟客户端断开连接。此时,如果客户端请求已经发送完,客户端收到空数据。如果尚未发送完,收到“Broken pipe”错误。
  • “读”程序读什么 (1)

    日期:2012-04-05 | 分类:编程

    这里的“读”指以“读”代码的方式,检查代码是否有问题。

    1. 变量是否初始化。可以用 -Wuninitialized 让编译器帮忙检查变量“使用前”是否初始化,但是这个不能检查出所有的情况,例如,你将变量地址(指针)传给函数,在函数中对地址解引用。变量/内存的初始化,是个很严重的问题。因为未初始化,在测试环境运行良好的程序,可能在线上环境运行错误,甚至改变编译参数都可能导致错误。

    2. 内存是否初始化。当你对“操作内存”后内存中的值有期望时,记得初始化。例如,某些api需要用户提供一个buffer,但api返回时,该buffer并不会以'\0'结尾,此时需要提前初始化。

    3. 指针是否悬空。在对指针解引用前,指针必须初始化。

    4. 数据类型是否越界。例如,用 uint8_t 指示内存的大小,考虑内存大小是否超过256。用 uint32_t 计数流量,流量可能超过40G。

    5. 内存是否越界。

    6. 资源是否释放。例如,内存是否释放,连接是否关闭,文件是否关闭等。

    7. 是否存在死锁。例如,是否解锁,是否加/解锁两次等等。

    8. 多线程环境,变量是否需要锁保护。

    9. 循环边界。以0还是1开始,以<=还是以<结束循环等。

    10. 算法检查循环不变式。
  • memcached 使用的若干问题(1)

    日期:2012-03-30 | 分类:编程

    1. memcached 将内存分成若干大小按比例增加的slab,LRU 是在slab内进行的。

    2. 系统启动时,给各个slab预分配一定的内存。剩下的内存(A)不属于任何slab。当某个slab内存不足时,从(A)中分配,分配之后就永远属于该slab了,如果(A)中没有内存,则该slab上发生LRU。这也是给各个slab预分配内存的原因,防止slab需要内存从(A)中分配时,(A)中无内存可用,预留的内存保证了该slab上有至少有预留内存可用。

    3. memcached 运行一段时间,(A)中内存必然耗尽,如果在此之前,某个slab从来没有存储过数据,那么该slab上可用的内存就是预留内存,这导致如果该slab突然需要存储数据,该slab发生LRU的概率就很大,因为预留内存很小。举例说明:memcached中存储的都是很小的数据,小数据slab很快耗尽了(A)中的内存,如果要存储大数据,大数据所在的slab只有预留内存可用,而预留内存通常很小,导致大数据频繁的LRU,这通常严重影响性能。

    4. 客户端通过hash使用memcached池,存在某个key导致某个memcached实例负载/IO高的可能。例如,某个key存放的是大数据,该key的访问量很大,那么这个key所在memcached网络IO会比其它机器高。

    5. 一个用户反映它的mecached的内存使用率不足50%,但是某个key经常刚set进去就get不出来。这个memcached池有4台memcached组成,我随机挑了两台stats,的确没有LRU发生。我换了另一个key,重现该问题。这是怎么回事?我想了很久,最后老老实实的在每台机器上stats,发现有一台机器的内存使用率很高,这台机器有LRU,我和用户的key都落在了这台机器上。因为其它3台机器最近重启过,这导致整体的内存使用率看起来很低。

    6. 我觉得redis也免不了这些问题。
  • 程序两三事(14)

    日期:2012-03-23 | 分类:编程

    1. 对优先保证可用的功能,此时拒绝访问的条件,要尽可能的严格。特别是在升级系统的时候,否则条件稍一宽松,原本正常访问的应用,就不能正常访问了。

    2. 升级系统的第一原则是原有用户原有功能不受影响,新功能有问题,可以容忍。在新功能开发中,要牢记这一点,例如可以通过增加配置选项,关闭新功能。

    3. 留心cron配置,它可能导致严重系统性能波动。

    4. 留心core配置,频繁的core出大文件,可能导致磁盘IO高。

    5. 如果nginx配置--with-http_realip_module,通过 r->connection 获取的就不是 nginx 对端的IP,realip模块将真实的对端IP保存在类型为ngx_http_realip_ctx_t的request ctx 中。要想在自己的模块中获取真实的对端IP,复制 ngx_http_realip_ctx_t 定义到自己的模块,extern ngx_module_t ngx_http_realip_module; 通过 ngx_http_get_module_ctx(r, ngx_http_realip_module); 获取。

    6. 选择文件系统不能只关注速度,故障恢复时间也很重要。要考虑在大数据量的情况下,故障恢复时间是否可以接受。

    7. 选择数据库,考虑速度(读写速度,数据不在内存时的速度,checkpoint发生时的速度,有节点故障时的速度,某些节点负载高时的速度),故障恢复时间,备份(冷备,热备),性能是否存在严重波动(写磁盘,数据归并,主从同步,从负载高),内存不足情况下的性能。

    8. getcwd 有系统调用和libc两个版本,libc 版本有个问题,char *getcwd(char *buf, size_t size); 在成功返回时,buf 不是'\0'结尾的字符串。

    9. readlink /proc/self/cwd(注意没有最后的/),返回的是当前进程的cwd。
  • 德胜门集合,大队人马坐872,我和博乂坐345快。345快的发车间隔很短。上车前看地图有些大意,345终点站可以倒昌32,但是345快不可以。必须在昌平南大街倒车到昌32起点。我们也没有坐过,下了345快后,一直找新华书店,据地图说那里可以坐昌32。走了10几分钟,没找到新华书店,倒是看见个早餐店,还是吃饭要紧。饭后,看了看边上的站牌,有昌32路,非常高兴。车站等车的一个阿姨说,昌32路,7点,8点各一趟,8点后是半个小时一趟。当时正好是8点,等了片刻,车就到了。在车上看到很多伙伴。

    这次活动有40多人参加,车上有9个,也就是说坐872到常陵的还有30多个,公交车已经很满了,这30多人指定上不全了。果然如此,为了不耽误行程,没上车的人就租了个小面。据说,司机是从被窝里叫出来的。

    望宝川是长寿村,估摸着跟风水有关。今天雾很大,百米之外就看不清了。从银山塔林下来将近11点,雾似乎更大了。这个地方栗子树很多,离子成熟的时候,很多人来这捡栗子。我第一次知道原来栗子的最外面还有个壳,并且是带刺的。我们还偶尔能拣着几个别人漏下的栗子。往东水峪走的时候,有点不妙,具体说是迷路了,走上一条不是路的路。迷途知返,耽误了不少时间。大雾天,稍微不留神,就错过了正确的路口。

    这是我见过最大的雾,空气里可以攥出水来。本来计划是A队在东水峪上莽山走十三陵水库,由于天气糟糕,A队取消,一起B队。走到常陵的时候,很多人就坐872回去了。最后走完全程的有10个人。夕阳中的十三陵水库,特别漂亮。很多人背着沉重的相机,准备拍照呢,天气不作美,啥也没看到。十三陵水库边上有一对老外在露营,这天实在不适合。

    坐朋友的车,在昌平吃了饭,朋友送我们回的家。到家时,雨已经下的不小了。第二天望窗外,银装素裹。此行,爬升700米,行走27公里。
  • 岢罗屯-戒台寺-千灵山一日穿越

    日期:2012-03-10 | 分类:往昔

    这次穿越的起点苛罗屯离苹果园很近,7点半从苹果园出发,8点20左右就到了。强度不大,累计上升1300米,走了12公里。值得纪念的几个地点。

    娼妓桥,建于明朝初年,迄今仍有石桥的一段桥身残存。这座桥是全国的妓女积资修建。据说,是妓女们为了赎罪,免遭来世的报应,修建了这座桥。

    石佛村,有19尊摩崖石刻佛,一尊大佛。19尊小佛完好,大佛局部损害,现在看到的是修复后的,修复痕迹明显。有趣的是19尊小佛不是一字排开,而是三五一群,错落有致的刻在山崖上,有的甚至很隐蔽。我们费了很大功夫才找全这19尊佛。

    娼妓桥和石佛村距离戒台寺很近,娼妓桥位于通往戒台寺的庞谭古道,石佛村位于通往戒台寺的芦谭古道上。估计受戒台寺佛家影响很大。

    戒台寺是隋代开皇年间,距今1400多年的历史。戒台寺以“戒坛,奇松,古洞”著称于世,戒坛始建于辽代,与福建泉州的开元寺,浙江杭州的昭庆寺共称全国三大戒坛。值得一去。

    千灵山是戒台寺的后山,是高僧们修行的地方。有古塔,佛洞等。
  • 这就是传说中的拖延症吧,2月26日的穿越,3月9日才写记录。

    7点在六里桥北里坐到燕化东岭的901快,到大董村换乘房32到泗马沟。在大董村等了40分钟车,还是赶在9点前到达泗马沟。公交距离还算可以,不算太远。

    泗马沟也是穿越者的老朋友了,通常都是从左手上到红螺三险,这次是从右手上去猫耳山。一开始就是一个1000多米的直拔,赶在11点之前到达山顶,从山顶到三盆水,路非常难走,有的路段贴着悬崖,有的要手脚并用上。也难怪去年清明,一队人被困在猫耳山,靠直升机救援。从山上下来都7点半了。累计爬升1450米,行程23公里。

    我一直很奇怪,北京周边的这些小山村,村里也没什么经济基础,却很有钱,他们是怎么发家致富的。这次算是有点眉目了。靠山吃山。山上有很多的采石场,随便找个山头,几炮炸下来,一车车的石头就是一沓沓的钱。房山的采石场很多,门头沟的煤矿很多。都是来钱的项目。
  • 2月19日 八达岭-居庸关徒步

    日期:2012-02-25 | 分类:往昔

    7点半从西直门坐S2到八达岭车站,依次游览岔道城,八达岭博物馆,青龙桥火车站,水关长城,关沟,居庸关。步行16公里,途中无上升。

    岔道城是一个古老的村子,09年被评为北京最美乡村,是明代军事设施遗址。现存的城墙,已经部分毁坏,从毁坏的剖面可以看出城墙的构造过程。村子边上有万人坑,是日本人做的恶事。万人坑左侧路边沟里是出京古道,慈禧逃离京城就是走的这里。

    八达岭博物馆是了解长城历史,文化最好的地方。有历代出土文物,全国各地的长城照片。参观八达岭长城前,先参观八达岭博物馆是一个不错的选择,博物馆是免费的。

    青龙桥车站是詹天佑设计的“人字形”铁路,列车变轨的地方,有詹天佑塑像和詹天佑夫妇的墓。詹天佑塑像底座写着“詹公天佑”,能被人称为“詹公”,莫大的荣耀。

    水关长城是由戚继光督建,迄今400年历史,长城边上有詹天佑故居,其实一个很小的三合院。我们在这里吃的泡面。

    关沟72景,现在好多景点都找不到了。我们一路大概看了有20来处吧。

    徒步终点居庸关,一座天桥横跨八达岭高速,关口几个大字,天下第一关名副其实。

    这次徒步,感觉脚上的伤还没好。自然调养已经不行了,需要搓红花油,加快康复进程。
  • 程序两三事(13)

    日期:2012-02-24 | 分类:编程

    1. PaaS平台的核心是多租户资源使用的限制,隔离,统计。

    2. 传统上,操作系统以用户(操作系统用户)和进程的方式对资源做限制,隔离,统计。而PaaS平台不能把多租户对应操作系统用户,也不能把多租户对资源的使用对应一个进程,就算复用进程,资源调度的粒度还是太大了。

    3. 锁是跟临界区相对应的,定义锁的时候,要明白的第一点就是这个锁保护的是那个临界区。锁保护的临界区越小越好。

    4. 所有的程序都是一个状态机。状态机有两个要素,一个是状态之间如何转移,一个是状态之间转移的驱动力。在编写事件驱动的服务器时,“事件”(有新的连接到来,套接字可读可写)是驱动状态转移的动力。根据逻辑将套接字加入“事件”列表,“事件”发生时,根据“事件”做相应的操作。一切从epoll(或其它“事件”检测接口)开始,最后回到epoll。

    5. 上述理解是有缺陷的,这导致我无法理解nginx的subrequest是如何工作的。main request 在执行时,可以执行一个subrequesgt,subrequest也是非阻塞的,也就是说 main request 要挂起,直到subrequest完成。这有点像操作系统的进程挂起,保存堆栈,放弃cpu,等待被唤醒。保存 main request 的“堆栈”好理解,它的堆栈就是一个request结构体,这个结构体保存了请求信息和当它恢复执行时从哪里开始执行。“放弃cpu”也能理解,就是“被唤醒”无法理解。进程是被操作系统唤醒的,在nginx里面谁来充当“操作系统”唤醒 main request 呢?只能是epoll,在我的思维里,epoll是事件发生器,可是main request挂起时,注销了所有事件,epoll显然无法唤醒它。这是一个很错误的观点。nginx是怎么做的呢?其实很简单,除了“事件”,“应用逻辑”本身也是驱动力。在 sub request 开始的时候,将subrequest和connection绑定,在请求结束的时候,判断当前请求是不是main request,如果不是,将main request和connection绑定,通过ngx_http_run_posted_requests(c)恢复main subrequest的执行。我之前的思路一直时,sub request结束时,再将 main request 加入到“事件”列表,通过“事件”驱动重新执行,可是监听什么“事件”呢?无解。今天搞清了subrequest的原理,有一种顿悟的感觉。
  • 程序两三事(12)

    日期:2012-02-23 | 分类:编程

    1. nginx 配置 proxy_buffer_size,指定了从 upstream 读取响应的第一个buffer的大小,该大小决定了响应header的大小。默认等于proxy_buffers的配置。

    2. php 配置 memcached 做 session,session.save_path 是IP:PORT,这点和memcache不同,memcache是tcp://IP:PORT。

    3. 当函数最后一个动作是调用另外一个函数时,我们称这种调用尾调用。尾调用之后程序不需要在栈中保留关于调用者的任何信息。由于尾调用不需要使用栈空间,那么尾调用递归的层次可以无限制的。

    4. accept4 是 Linux 2.6.28 支持的系统调用,accept4 支持 O_NONBLOCK 标志,返回的fd就是非阻塞的,不需要再调用fcntl函数。

    5. 使用 ioctl(FIONBIO) 设置fd为非阻塞,相比 fcntl(F_SETFL, O_NONBLOCK) 少一次系统调用,后者要在调用前调用 fcntl(F_GETFL)。

    6. 如果系统上存在一个库的A,B两个版本,A放在标准位置,B在非标准位置。要想在程序运行时加载B,有两种方法,1. 运行前 export LD_LIBRARY_PATH="path_of_B:$LD_LIBRARY_PATH";2. 链接时 -Wl,--rpath -Wl,path_of_B

    7. export LD_LIBRARY_PATH="path_of_B"; sudo prog_use_B; 此时prog_use_B加载的是标准位置的A,因为sudo切换用户,LD_LIBRARY_PATH 设置的环境变量只对sudo前的用户生效。

    8. 为程序将来升级考虑,if-else 语句要审慎,如果确信程序的两种状态,if-else 就不如 if-elseif-else{abort}好,明确判断第二种状态,如果出现第三种抛异常。或者将断言加在if-else之前。
  • 使用libcurl POST数据和上传文件

    日期:2012-02-10 | 分类:编程

    为了具有通用性,将文件的内容读到了fc变量中,fclen是fc的长度。fc也可以是任何其它内容。curl 是 libcurl句柄。演示省略了很多显而易见的步骤。

    1. 普通的post请求,这里用curl_easy_escape对fc做了编码
    std::string data("req=plain");
    data.append("&file=");
    char *efc = curl_easy_escape(curl, fc, fclen);
    data.append(efc)
    curl_free(encoded);

    curl_easy_setopt(curl, CURLOPT_URL, PURGE_URL);
    curl_easy_setopt(curl, CURLOPT_POST, 1L);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
    curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, data.size());

    2. multipart/formdata请求
    struct curl_httppost *formpost = 0;
    struct curl_httppost *lastptr  = 0;
    curl_formadd(&formpost, &lastptr, CURLFORM_PTRNAME, "reqformat", CURLFORM_PTRCONTENTS, "plain", CURLFORM_END);
    curl_formadd(&formpost, &lastptr, CURLFORM_PTRNAME, "file", CURLFORM_PTRCONTENTS, fc, CURLFORM_CONTENTSLENGTH, fclen, CURLFORM_END);

    curl_easy_setopt(curl, CURLOPT_URL, URL);
    curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);
    curl_easy_perform(curl);
    curl_formfree(formpost);

    3. multipart/formdata请求,不把文件读入fc,其它步骤相同
    curl_formadd(&formpost, &lastptr, CURLFORM_PTRNAME, "file", CURLFORM_FILE, "/path/filename", CURLFORM_END);

    4. 通过put上传文件
    curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
    curl_easy_setopt(curl, CURLOPT_PUT, 1L);
    curl_easy_setopt(curl, CURLOPT_READDATA, fp);   // FILE *fp = fopen("/path/filename");
    curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, fsize);  // fsize = sizeof /path/filename

    5. 发送自己的Header
    struct curl_slist *slist = 0;
    slist = curl_slist_append(slist, "Blog-X-User: username");
    slist = curl_slist_append(slist, "Blog-X-Signature: signature");
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist);
    curl_slist_free_all(slist);
  • 程序两三事(11)

    日期:2012-02-08 | 分类:编程

    1. 要想在apache模块中获取对端IP(直接和apache相连),要考虑mod_extract_forwarded的影响。mod_extract_forwarded干了一件很“讨厌”的事情,它把request_rec的remote_ip字段保存为远端IP(真正的远端IP,而不是代理的IP,在有代理的情况下),而把和apache直接相连的(代理)机器的IP保存在subprocess_env中。

    2. 在apache模块中可以使用r->request_config在请求中保持数据,但是要注意 r->main 和 r->prev。apache 可能使用 ap_internal_redirect,ap_run_sub_req 重新生成了request。此时要获取最初保存在r->request_config中的数据,需要向前遍历r->prev和r->main,获取正确的r。

    3. 因为static 函数是文件范围的,在该文件外,函数是不可见的,这个给单元测试带来麻烦,可以通过#include <xxxx.c> 的方式,这样static函数在你的单元测试代码中就可见了。

    4. 分布式系统中,保证各节点数据的强一致性带来另一个问题,一个节点速度慢会严重拖慢整个系统的速度,系统速度取决于最慢的那个节点。

    5. chown -R daemon.daemon /data0/tmp,这条命令执行快吗?答案是取决于/data0/tmp下面的文件数量,如果有上千万的文件,那么这条命令可能要执行半天。记住,磁盘,是最可能引起阻塞的地方。

    6. 使用clock_gettime函数,记得链接librt.so。
  • 选锁准则

    日期:2012-02-07 | 分类:编程

    1. 优先选 pthread 系列的锁,pthread锁运行在用户态,相比信号量,文件锁等运行在内核态的锁,pthread 速度更快。但是作为进程锁使用时,pthread锁在进程异常退出时,不会释放已经占有的锁,而信号量和文件锁会。

    2. 注意锁的粒度。pthread 提供了互斥锁、读写锁。读写比例很大时,选择读写锁。文件锁提供了更小级别的锁,可以到字节级别,但是只能作进程锁用。也可以以pthread_mutex加pthread_cond为基础,实现更细粒度的锁。

    3. 注意持有锁的冲突和持有锁的时间长短。如果线程A加锁时,经常碰到别的线程持有锁,而不得不等待,则称锁的冲突比较大。如果线程A持有锁,但是很快就释放了,则称持有锁的时间比较短。持有锁时间越长,锁冲突越大,越影响并发。相比锁冲突,持有锁的时间对并发影响更大。考虑减小锁的粒度,减少持有锁的时间长度。

    4. 测试自己实现的锁,要考虑到持有锁的时间长度,仅仅一个简单的加法来测试锁是不行的,除非你的线上应用就是持有锁,然后做个加法。测试锁时要测试不同并发(线程个数)下锁的表现。
  • 2个线程执行这段代码,sum的最终结果是多少?for (int i = 0; i < 1000000; ++i) sum++;

    答案是取决于编译器的参数,如果-O0编译答案是小于2000000的某个数,如果-O2编译答案是2000000。O2编译能得到正确结果,O0却不能。我们能得出i++在O2编译下具有原子性,从而不需要锁保护的结论吗?

    用objdump -d查看汇编代码发现,在O0下cpu老老实实的执行了1000000次加法,而在O2下cpu只加了一次1000000,这就是编译器优化,因为只加了两次1000000,所以结论总是2000000。但编译器不会总是这么好运,碰到这么容易优化的情况,所以还是老老实实的用锁吧。
  • 记一次磁盘IO高故障排查经历

    日期:2012-02-03 | 分类:编程

    最初是用户反馈codefs部署代码速度很慢。用dstat 1观察磁盘IO在10~20M左右。iostat -x 1 10观察util接近100%,说明已经达到磁盘性能极限,同时显示数据写入集中在sda5。

    最初怀疑是codefs的问题,但是dstat观察显示磁盘IO很高,网络IO很低。因为codefs写入磁盘的数据来自网络,网络IO远小于磁盘IO,说明不是codefs的问题。同时strace -f -p pid -e trace=write也显示没有写入,更确定不是codefs的问题。剩下只有httpd进程了。我们的httpd禁用了磁盘IO,更不可能是它导致的,并且strace -f -p pid -e trace=write显示只有少量的磁盘IO。但是,停掉httpd进程后,磁盘IO马上就降下来。两者相互矛盾。用mount命令查看/tmp分区挂在sda5,于是lsof | grep tmp发现只有APC的临时文件处于打开状态,但是不知道APC用临时文件干什么。禁用APC之后,磁盘IO降了下来,基本确认是APC的问题。

    APC是php加速器,完全使用内存,怎么会涉及到磁盘呢?原来APC有个选项apc.mmap_file_mask指定的是tmp分区下的文件。看到这个选项的名字,马上就明白是怎么回事了。APC通过mmap的方式使用共享内存,mmap映射到磁盘,而在mmap修改文件时,操作系统会将修改的内容刷到磁盘。APC开了1G的共享内存,大量的小应用导致APC经常的修改mmap的内容,从而导致频繁的将mmap的内容刷到磁盘。解决办法是将apc.mmap_file_mask设置为/dev/shm下的文件,/dev/shm是tmpfs,tmpfs是内存磁盘,没有刚才mmap刷磁盘的问题。

    磁盘IO高是前天就发现的现象,直到今天才引起足够重视并解决。首先被很多的现象所迷惑,1. 最近修改了codefs,而codefs是写磁盘的,怀疑它有道理,但是从它身上没找出问题。2. 前天就发现httpd停掉后,磁盘IO降下来,应该怀疑是它的问题,但这个又和strace的结果矛盾,就没有进一步查找。3. strace只盯着write系统调用,误导了我们,其实很多系统调用都会写磁盘writev,mmap等。其次是排查方法有问题,其实最直接的方法是查看挂载在sda5上的tmp分区,用lsof观察有哪些文件打开,通过文件名获取更详细的信息。最后是,用户不再反馈codefs部署慢,就自我麻痹,不再深究这个问题,逃避现实。
  • 程序两三事(10)

    日期:2012-01-19 | 分类:编程

    1. 线上的程序要保留所有的断言(assert),只要不是显著影响性能。
    2. 将多写机制放到客户端,提高memcached的可用性,但要留心不同客户端的互操作性。
    3. unix 下任何东西都是文件,只要是文件访问的时候就会设置last access time,pipe也不例外。对pipe而言,这个开销可能超过读pipe本身。通过fcntl(fds[0], F_SETFL, O_NOATIME)指示pipe在读的时候不更新atime。(摘自Erlang非业余研究)
    4. g++编译错误,expected primary-expression before '.' token。错误写法 util.scramble,正确写法 util::scramble。util是namespace。
    5. 对调用了setuid的linux程序,如果想让程序生成core,仅仅调用setrlimit相关函数是不够的,必须在setuid之后调用prctl(PR_SET_DUMPABLE, 1, 0, 0, 0)。
    6. nginx 根据upstream返回的http code做健康检查和redispatch,在云平台,因为租户可以控制upstream返回的http code,所以这种方式不是很适合。
    7. 不仅升级系统要考虑逐台升级,在开发系统时,就要考虑逐台可升级性。
    8. 在 post 请求在 ngx_http_internal_redirect 之后,request_length 统计出来的数据不包括body部分。
    9. struct 在32/64下的对齐差异(不是指数据类型的字节数问题,当然这个是问题),为此需要使用 #pragma pack() 指令。
    10. nginx 有多个自己开发的filter模块,希望最先执行的在configure时放到最后。
    11. 为优化系统使用了一个“精妙”的算法,在系统开发时,最好保留原来不怎么“精妙”的算法,以防止“精妙”算法存在问题时,可以切换到不怎么“精妙”的算法。在新近的一个系统中,我给命令加了选项,可以选择使用哪个算法,新算法运行很久之后,出了一个问题,短时间找不出原因,为此启用命令的选项,采用老的算法,保证系统平稳运行。
  • 自从11月19日,在延庆遭遇了一场雪,得了2011年最重的感冒,直到昨天我都没再爬过山。其间,感冒一次,遇雪一次,加班两次,偷懒若干,一晃就进入了2012,人真的会变懒的。当你在晨光中醒来,望着暖暖的室内阳光,真的不想出门,只想就这么一直待在这温暖的阳光中。外面也有阳光,但是那里的阳光稀薄,一丝的风就足以吹散阳光带来的暖意。我承认,这些是自我欺瞒的借口,虽然真实,但仍然是借口。

    周四的时候,上班路上碰到博乂,博乂说,咱们走一个大强度的穿越吧,从圈门到百望山。我们曾经从圈门走到香山(还是缩水版的,因为没走风口庵,少走至少一个小时),当时我是较紧牙关才走下来的。当时是夏天,我可以怪天气。这次是冬天,适合大强度穿越,我以为只要有体力,应该是没问题的。你知道吗?这段距离至少40公里。

    早上,中关园982,南坞倒992,终点站西辛房下车,走一站地到圈门。早上6点出门,7点40就到了,距离集合时间8点10分早了半个小时。天下着小雪,门头沟的雪比海淀的要大。我们在附近的小店吃早饭,等待集合。不知是天气缘故,还是人的缘故,直到8点50人才到齐,出发。强度大,天气糟,出发晚,真不像是能完成全程的样子。

    也许,很多人根本就没打算走全程,除了我和博乂,只有6个人一直不懈的往前赶。我承认,我走的慢,任凭我以最快的速度,我仍然赶不上他们6个。10点半到风口庵,11点半到小狗之家,13点40到水闸。不到2点到三家店,我们8个人在饭店每人吃碗杂酱面,2点半,继续往香山走。上午我喝了半罐红牛,吃了4块牛肉,1个面包,中午又一碗杂酱面,这些东西没白费,虽然步伐慢,但是体力还可以,没有觉得很累。走到新望京的时候都4点半了,这时一个男的单独下撤,两个女生往香山下撤,两男一女继续往百望山方向,我和博乂计划继续往百望山方向走,走不动就下撤,不一定非走到百望山不可。我不得不佩服那两男一女,名副其实的强驴,马不停蹄的狂奔了7个小时以后,他们还能以小跑的速度奔向百望山。我和博乂走的也不慢,直到好汉波,我们的速度也很快。

    5点半过了好汉坡后,我觉得脚腱的地方有点疼,就停下来沾了一块创可贴,我以为是鞋磨脚,因为之前爬山就疼过。但是我错了,我一迈大步,脚腱就疼。我开始悟到,是身体抵抗不住了。但已无退路,只能走到四棵树,从植物园下山。如果按照之前的速度,也就1个半小时的事情,但这次用了将近2个半小时。慢慢的,膝盖也开始怠工。我们平时很自然的走路,不会注意走路时各个关节是如何配合的,当某个部位出问题,才发现它们配合的太好了,少一个都不行。先是脚腕和大腿发力,膝关节肌肉收缩,这样迈出一步。现在脚腱伤了,膝关节疼痛,主要靠髋部的大腿,腿像钟摆一样向前摆,费力走不快。

    这是煎熬的两个半小时,好在有博乂陪伴。这次穿越由豪迈演变成了自虐,我后悔没有从香山下撤,后悔之前没有坚持爬山,并发誓今年都不再走超过20公里的线路。8点到车站,我一屁股就坐到地上,去他妈的形象,我只想休息。
    晚上回到家,只洗了脚,倒在床上,不知不觉就睡着了,一直到12点,才开始脱了衣服,再睡。我没有力气洗澡,真的没有力气。今天又休息一天,才感觉自己又活过来了。今天中午,博乂特意打电话,问身体情况,非常感谢,如果不是我拖后腿,他可以走到百望山的。这次穿越,虽然没有走完,大概也走了有33公里的样子。

    回顾一下,我感情上并不觉得累,但是身体上却体现出无可置疑的过度疲劳,身体的症状只能用疲劳解释,大概身体的疲劳时一种自我保护,免的感情作出出格的事情。跟那3位强驴比,我差的很多,先天的身体素质和后天的锻炼都有关系,总之是不能强求的,不想再有第二次这样的经历,惨痛的自虐。
  • 四司-燕羽山-柳沟穿越

    日期:2011-11-19 | 分类:往昔

    今天跟随逍遥群,四司-燕羽山-柳沟穿越,行走12公里,上升600米。7点20德胜门坐919快至终点,包车前往四司。按照传统,包7人座小面每人10块。但是甘肃庆阳的事故实在太大了,死了21个可怜的孩子,于是天朝严查超载,开出的3辆小面被拦下两辆。我们打4人座的轿车,每人15块钱,26公里的路程,这个价格还是比较合适的。我听说了甘肃的事故,我故意不去了解详情,我伤不起。我知道会很惨,但我还是低估了惨烈的程度,如果有关部门稍微作为一点,21个欢乐的生灵就不会消失。我从不奢望职权部门能有多好,我只希望他们别太过分,有点底线就行,我还是失望了。

    下了车,清冽的风吹来,我的单裤仿佛不存在一般,腿碰到裤子都觉得裤子冰。还好包里有一件褂子,赶紧穿上。在爬山前穿衣服,这还是第一次。走到一个水坑,发现结了厚厚的冰。继续进山,先是地面上有零星的霜,继续走,发现那些零星的不是霜,而是雪。最后的地方有2公分之厚。延庆已经下了雪,向秋裤投降是我唯一的选择。

    2点下山,到燕羽山居吃牛肚宴。牛肚宴是和豆腐宴齐名的延庆特产。谈不上有多好,10几道普通的凉菜,烙玉米饼,玉米窝头,饺子,每人一个羊蹄,两个牛肚干锅,26块钱一位,还想有多好呢,是不是。大家最喜欢吃炸油饼,玉米饼和饺子。最后博乂带的青萝卜受到大家的普遍欢迎。

    “我亲自受伤了”,博乂诙谐的说。上周他目睹了京的摔倒,这次刚爬,他的小腿肌肉拉伤,拄着拐杖顽强行走。这是旧伤复发,爬山的人几乎每个人都有那么点伤。在回来的公交上,大家纷纷对他表示照顾,我帮你背包吧,我帮你拿拐杖吧,我帮你拿钱包吧,我们就是这么“热心”。在回来的地铁上,看到两位聋哑人兜售物品,我对博乂开玩笑说,“你也可以挥挥手中的拐杖对他们说,咱们是同行”,博乂说,“我腿受伤,体会到我们应该关怀残疾人”。旁边一位乘客说,“只要不是脑残就行,残疾人也可以找份工作”。这位乘客打扮像民工,我对他肃然起敬。是呀,他们这样名为兜售,实为行乞,实在不是手脚健全的人该做的事情。

    早上,坐919前,我们去饭店吃早饭。博乂说“老板要鸡蛋”,我说“我带了”。博乂说“老板要包子”,我说“我带了”,博乂说“老板要咸菜”,我说“我带了”,老板惊愕,两位是来消遣在下的吗?哈哈,没这么夸张了,但我的确带了鸡蛋包子咸菜。
  • 椴树岭-百泉山穿越

    日期:2011-11-18 | 分类:往昔

    上周六跟随逍遥群,椴树岭-百泉山穿越,行走15公里,上升700米。7点东直门坐916快车,至怀柔迎宾路路下车,转916外环至于家园下车,转去汤河口936的车椴树岭下车。去汤河口的936路比较有意思,因为后半段在山区行走,要求乘客必须有座位,所以严格排队,很多中间站要上车进山的就没戏了,因为起点已经坐满了。我估计远郊的乘客都很恨我们这些爬山的,因为我们抢了他们的公交。

    因为泉很多,百泉山故名。刚到椴树岭,就遇见一条小河沿着公路流淌。靠近椴树岭,又是一股溪流从山谷流出。一路沿溪行走,走过水源头没多久,翻过椴树岭,又是一条小溪,这条小溪流到了百泉山公园。椴树岭估计有很多椴树,我一棵也不认识。岭下碰到一位80岁的大爷上山背柴,身体真棒。是不是喝了这泉水的缘故。

    翻过椴树岭,就看到从百泉山森林公园走到此处的游客,有个美女,真漂亮。百泉山门票26,并且出门要检查门票,这可难为我们了。我们只是碰巧要路过景区,又不是要来景区玩的,没有买门票的道理。来之前领队已经打听了逃票路线。在翠叶亭的后面有一条小道,沿着小道上山,翻过山头可以上公路,上了公路往右,就逃成功了。听起来跟越狱似的。这条小道很不明显,博乂先是探了一下,没有走太远,我探了另一条路,没法走。最后大家商量,沿着博乂的那条路继续前进。我在前面探路,当我看到矿泉水瓶的时候,我确定这是一条路。探路很顺利,到山顶,钻过两道铁丝网,向右走,可以看到公路,左手边有下山的路。

    我飞奔下山,再次看到小河流淌,只有风和哗哗的水声,周身是枯黄的芦苇。我很高兴,胆哥随后也下来了。我们俩沉浸在逃票的喜悦中,同时其它队友正经历一场困局。中队错过了左手下撤的路,再次走到了公园,被人拦住索要门票,他们拒绝了,并选择回撤,从另一条路下山,途中一名队友摔倒,脸先着地,“破了相了”,另有一名队友走失,直到我们乘车返回,我们才和他取得联系。只有后队顺利和我们会合。

    现在北京很多景区都私有化,承包给了开发商,开发商追求利润,采取出门索票的方式对付驴友。驴友们与之斗志斗勇,双方互有胜负。一次,某领队从百泉山出,遇索票,领队明确表示不买票,请来110后,没买票就出去了。第二次,该领队再次用此计,景区拒绝请110,双方僵持不下,买半票了事。
  • 程序两三事(9)

    日期:2011-11-08 | 分类:编程

    1. 要有单独的线程负责处理心跳同步,如果让业务线程(处理正常业务的线程)处理心跳,那么业务线程的阻塞(写磁盘)可能导致心跳丢失。
    2. 如果程序基本无阻塞(包括文件读,且文件已进入页缓存),此时设置线程(进程)数等于cpu个数,否则视阻塞的情况,可以适当多于cpu个数。
    3. 再次提醒自己,strace 和 pstack 是强大的观察程序运行状态的工具。
    4. 系统需求理解透彻了,只需要设计完备的接口,根本不用考虑调用者如何使用,用户自然会觉得好用。
    5. A,B两台机器通过haproxy访问后面的两台apache,A机器OK,B机器错误。越过haproxy直接访问apache,A,B机器都OK。问题原因:haproxy新增一台配置错误的apache,而haproxy采用的hash的负载均衡方式。
    6. 系统出问题,要回滚版本或重启,做这些前要了解当前的系统状态和程序状态。系统状态包括top,netstat,iostat等,程序状态包括10s钟的strace,连续10次的pstack。
    7. locale 设置影响 printf 的运行,例如locale为en_US.UTF-8,*.s 打印二进制数可能报错。通过检查printf的返回值为-1,和errno发现错误原因。
    8. client 能收到500错误,说明后端的httpd是正常的,如果httpd挂了,proxy应该返回502。可以推断用户的php代码有语法错误,或者用header函数输出了500,最不可能的情况是httpd服务器自己产生500。
    9. 要访问文件,除了要求文件权限,还要求文件所在目录/父目录的权限。sudo -s user cmd,测试 user 用户的 cmd 权限。
  • 驷马沟-三险-坡峰岭穿越

    日期:2011-11-02 | 分类:往昔

    上周六,跟逍遥队从驷马沟-三险-坡峰岭。累计行走15多公里,上升1000多米。
    穿过兵营,上坡峰岭时,路过李莲英的行宫。行宫规模很小,10多间房子的样子,现在已经完全破落。行宫内有两个石碑,上写“万古流芳”,不管好人坏人,这点追求还是有的。
    这是第三次走这条线,虽然每次路线不是完全一致,这条线基本记路了。最近爬山听说的事故多了,自己也变得谨慎了。
  • 程序两三事(8)

    日期:2011-11-02 | 分类:编程

    1. mysql的编码问题。创建表时指定了数据的编码(没有指定用默认),假定为utf8,这个是存储在磁盘上的数据的编码。客户端和mysql服务器通信也指定了编码(set names),假定为latin1,客户端从服务端得到的数据编码就是latin1。最好的方法是指定通讯编码和数据编码相同。
    2. c语言中 char 可能是 signed char,c = (c % ('~' - ' ' + 1)) + ' '; 这行的意思是返回一个可打印字符,但是 c 是char,导致模运算可能返回负值,正确方法是:c = ((unsigned char)c % ('~' - ' ' + 1)) + ' ';
    3. 考虑压缩和网络协议优化,减少带宽消耗。
    4. 通过 strace -ttt -p pid 查看正在运行程序的系统调用的时间点,可以为辅助查找系统慢的原因。
    5. 不是只有cpu占用率70-100%,才叫高,对于一个读取mysql,然后将数据写入mc的应用,cpu 30%就叫高了。同理内存。
    6. 观看程序运行状态的关键是“cpu/内存/io”是不是在一个合适的区间。例如,占10M内存的程序,也可能是有问题,它可能只应该占1M,这个是可以预估的。
    7. php的某些操作是很慢的,当数据量上万后,array_merge是个很耗时的操作。
    8. 单线程php写入memcached小数据,在1000/秒左右,估计其它语言测试也差不多。