Archive for the ‘网络编程’ Category

线上存储服务崩溃问题分析记录

上周我们的存储服务在某个线上项目频繁出现崩溃,花了几天的时间来查找解决该问题。在这里,将这个过程做一下记录。

加入调试信息

由于问题在线上发生,较难重现,首先想到的是能不能加上更多的信息,在问题出现时提供更多的解决思路。

首先,我们的代码里,在捕获到进程退出的信号比如SIGABRT、SIGSEGV、SIGILL等信号时,会打印出主线程的堆栈,用于帮助我们发现问题。

但是在崩溃的几次情况中,打印出来的信息并不足以帮助我们解决问题,因为打印的崩溃堆栈只有主线程,猜测是不是在辅助线程中发生的异常,于是采取了两个策略:

  1. ulimit命令打开线上一台服务器的coredump,当再次有崩溃发生时有core文件产生,能够帮助发现问题。
  2. 加入了一些代码,用于在崩溃的时候同时也打印出所有辅助线程的堆栈信息。

在做这两部分工作之后,再次发生崩溃的情况下,辅助线程的堆栈并无异常,core文件由于数据错乱也看不出来啥有用的信息来。

复现问题

由于第一步工作受挫,接下来我的思路就在考虑怎么能在开发环境下复现这个问题。

我们的存储服务在其他项目上已经上线了有一段时间了,但是并没有出现类似的问题。那么,出现问题的项目,与其他已经上线的服务有啥不同,这里也许是一个突破口。

经过咨询业务方,该业务的特点是:

  • 单条数据大:有的数据可能有几KB,而之前的项目都只有几百字节。
  • 读请求并发大,而其他业务是写请求远大于读请求。

由于我们的存储服务兼容memcached协议,出现问题时也是以memcached协议进行访问的,所以此时我的考虑是找一个memcached压测工具,模拟前面的数据和请求特点来做模拟压测。

最后选择的是twitter出品的工具twemperf,其特点是可以指定写入缓存的数据范围,同时还可以指定请求的频率。

有了这个工具,首先尝试了往存储中写入大量数据量分布在4KB~10KB的数据,此时没有发现服务有core的情况出现。

然后,尝试构造大量的读请求,果然出现了core情况,重试了几次,都能稳定的重现问题了。

有了能稳定重现问题的办法,总算给问题的解决打开了一个口子。

首次尝试

此时,可以正式的在代码中查找问题的原因了。

来大概说明一下该存储服务的架构:

  1. 主线程负责接收客户端请求,并且进行解析。
  2. 如果是读请求,将分派给读请求处理线程,由这个线程与存储引擎库进行交互,查询数据。此时该线程数量配置为2。
  3. 存储引擎库负责存储落地到磁盘的数据,类似leveldb,只不过这部分是我们自己写的存储引擎。
  4. 在读线程从存储引擎中查询数据返回后,将把数据返回给主线程,由主线程负责应答客户端。

再谈同步/异步与阻塞/非阻塞

几年前写过一篇描写同步/异步以及阻塞/非阻塞的文章,今天回头来看bug不少,于是需要重新整理一下原来的描述.

同步/异步
首先来解释同步和异步的概念,这两个概念与消息的通知机制有关.

shared_ptr和对象管理

异步的分布式编程常遇到一个问题。比如某个客户端连接上来之后,发送的请求业务逻辑处理是需要服务器再连接到另外一个服务器做查询的,那么有可能出现的情况是,这个回复的时间太长或者各种其他原因,客户端在处理完毕之前主动关闭连接了,导致从另一台服务器处理完毕的时候,该连接指针已经失效。

这个问题很常见,尤其在业务复杂,异步的环境下更容易出现了。

简单的来理解,需要一个机制,在处理完毕的时候查询到该连接指针是不是有效的。

我想的第一个策略是使用shared_ptr管理这个连接,然后用一个该shared_ptr的weak_ptr来观察该指针是否还有效。然而shared_ptr是一种具备传染性的策略:某个指针一旦某处使用shared_ptr管理,那么所有该指针出现的地方也需要使用shared_ptr了。我不认为这是一个好办法。

咨询了另一个我比较信服的朋友,给的方法是不再传递指针,而是使用ID。换言之,ID和这个指针是一一对应的关系,处理完毕之后要向该连接回复的时候,首先根据ID来查询该连接是不是还有效,然后再做后面的事情。

这个思路我认同。不过细化起来还有几点需要考虑的。ID如何分配,是使用连接的FD么?这样带来的问题是,操作系统实际上会很快复用刚才关闭的FD,会有一定的风险。我最后使用的策略是分配ID的地方保存一个计数,每次加一,使用STL的map来管理ID和指针的对应关系,在新分配一个ID之前,虽然将原来的计数器加一了,但是也要判断是不是原来这个ID已经有对应的指针与之绑定了。

第二个问题是,管理ID对应关系的管理器是全局只有一个吗?如果只有一个,那么在多线程环境下势必需要加锁操作。由于我现在做的这个服务器是多线程,每个线程绑定了一个epoll,所以策略变为每个线程一个ID管理器,同时ID中带上线程的信息,比如使用低两位保存线程的ID,这样就可以根据ID识别出来这个ID属于哪个线程管理的。于是由于给线程发消息的消息队列有先后顺序,比如前一个消息说删除这个ID,后一个消息给这个ID发送消息,那么后一个消息就不会被处理,因为ID已经被删除了。

我个人觉得这种方案,比直接在各种线程里使用指针,同时使用shared_ptr来管理的方案,要好的多。

网络编程相关文章收集

1) epoll学习笔记

2) epoll为什么这么快

3) epoll相关资料整理

4) connect的两种出错情况

5) 带超时机制的DNS解析API

6)