一、问题背景

最近在工作上遇到了一个问题,客户端说我们服务的返回长尾太长了,经常40+ms。找了一个trace发现,我们服务端记录这个trace从开始到返回,总共才花了6ms。多试了几个trace发现有同样的现象,这就很奇怪了。

二、问题分析

1. 请求链路

首先思考一下一个请求的整个链路都包含哪些阶段

如图所示,对于客户端来说,他们的耗时记录是在最上面两个过程中,但是服务端是最下面函数处理前后,两者差着序列化和tcp传输,所以猜测是不是这两个地方导致的问题。

2. tcp传输

我们的rpc框架用的是thrift 0.9.2,比较成熟了,所以优先考虑的是不是tcp传输过程中特别慢。如果是tcp的话最好的方法就是抓包了,在服务器上使用命令:

sudo tcpdump host 100.69.93.36 -i any -Xvv  -w dump.pcap

获取到超时的请求和返回,用本地wireshark的Flow Graph下面的tcp flows来查看tcp的过程:

第二部分,省略了中间一部分数据传输:

可以看到,虽然传输的报文段很多,开始直接达到了客户端告诉我们的最大窗口值,但是网络传输时花费的时间并不长。只有两个地方时间较长(图中红框中的),比较诡异,分别是

  1. 服务端返回请求响应ACK之后,到第二个ACK过程(19ms)
  2. FIN之前的ACK到FIN(74ms)
    分别分析,第一个情况其实就是服务端中函数处理时间,长点就长点吧。第二个是什么情况,花了这么久?我们知道FIN请求是在断开连接之后才会启动的,我们的例子中客户端是在请求服务端后立即就释放了,那么很有可能就是客户端在socket到反序列化过程中时间特别长。
2. 反序列化

我们用的是thrift,他的反序列化其实就是在recv_Func()中,截取一部分代码:

   if (ftype == ::apache::thrift::protocol::T_LIST) {{this->coords.clear();uint32_t _size0;::apache::thrift::protocol::TType _etype3;xfer += iprot->readListBegin(_etype3, _size0);this->coords.resize(_size0);uint32_t _i4;for (_i4 = 0; _i4 < _size0; ++_i4){xfer += this->coords[_i4].read(iprot);}xfer += iprot->readListEnd();}isset_coords = true;} else {xfer += iprot->skip(ftype);}

发现针对list的情况,他是for循环顺序解析的,每个coords又是一个结构体,会继续递归进行read。在这前后我们加一个时间戳打印函数,发现每次coord的解析都需要0.1ms!!那我们返回500个coord的时候,顺序反序列化岂不是就消耗了50ms。
客户端请求的时候是批量请求的,可以理解为二维数组,那么外围又是一个顺序解析的list,里面顺序解析coord,必然会很慢,至此原因就找到了。

3. 问题验证

既然是大list引发的问题,那么我们就先减少一下list的数量,发现果然耗时降到了20ms。

4. 更多疑问
  1. 为什么只有反序列化慢呢?
    后面经过验证,发现之前序列化也很慢,只是上面的例子中可能是客户端性能较差(docker容器),导致反序列化慢的情况放大了。而且经过网上的benchmark测试,thrift序列化的时间更久。
  2. 为什么会慢?
    我们使用的是TBinaryProtocol通过查看thrift源码,每次调用自带数据类型时,read的时候会调用:
template <class Transport_>
uint32_t TBinaryProtocolT<Transport_>::readI64(int64_t& i64) {union bytes {uint8_t b[8];int64_t all;} theBytes;this->trans_->readAll(theBytes.b, 8);i64 = (int64_t)ntohll(theBytes.all);return 8;
}

我们使用的是TFramedTransport,trans_->readAll()会调用下面的代码:

  /*** Shortcutted version of readAll.*/uint32_t readAll(uint8_t* buf, uint32_t len) {uint8_t* new_rBase = rBase_ + len;if (TDB_LIKELY(new_rBase <= rBound_)) {std::memcpy(buf, rBase_, len);rBase_ = new_rBase;return len;}return apache::thrift::transport::readAll(*this, buf, len);}
// return中readAll代码
/*** Helper template to hoist readAll implementation out of TTransport*/
template <class Transport_>
uint32_t readAll(Transport_ &trans, uint8_t* buf, uint32_t len) {uint32_t have = 0;uint32_t get = 0;while (have < len) {get = trans.read(buf+have, len-have);if (get <= 0) {throw TTransportException(TTransportException::END_OF_FILE,"No more data to read.");}have += get;}return have;
}return have;
}//trans.read函数
/*** Fast-path read.** When we have enough data buffered to fulfill the read, we can satisfy it* with a single memcpy, then adjust our internal pointers.  If the buffer* is empty, we call out to our slow path, implemented by a subclass.* This method is meant to eventually be nonvirtual and inlinable.*/uint32_t read(uint8_t* buf, uint32_t len) {uint8_t* new_rBase = rBase_ + len;if (TDB_LIKELY(new_rBase <= rBound_)) {std::memcpy(buf, rBase_, len);rBase_ = new_rBase;return len;}return readSlow(buf, len);}

可以看到,这里有个memcpy内存拷贝,外面还有一个while循环,应该就是这个地方比较耗时了。

三、解决方案

thrift协议我们没法直接改动,这样我们可以通过如下几种方案处理:

  1. 通过缩小返回的大小来处理,但是有的时候业务不允许。
  2. 客户端批量请求改为并行请求,不把时间耗费在list、map等数据结构的串行反序列化上。
  3. 既然是list和map引发的问题,那我们可以不用list和map,把list和map先使用pb等方式序列化成二进制流或者字符串,保证反序列化的时候不用串行遍历。

thrift 大list序列化过慢引发的客户端耗时记录和服务端不一致问题相关推荐

  1. 大淘宝服务端技术干货沉淀和总结

    网络基础 TCP三次握手 三次握手过程 客户端--发送带有SYN标志的数据包--服务端 一次握手 Client进入syn_sent状态 服务端--发送带有SYN/ACK标志的数据包--客户端 二次握手 ...

  2. C# Socket服务端与客户端通信(包含大文件的断点传输)

    步骤: 一.服务端的建立 1.服务端的项目建立以及页面布局 2.各功能按键的事件代码 1)传输类型说明以及全局变量 2)Socket通信服务端具体步骤:   (1)建立一个Socket   (2)接收 ...

  3. 知物由学 | 从应用端到服务端,设备指纹生成算法大变革

    设备指纹是用来标识手机或者浏览器的唯一 ID.基于这个 ID,我们能够精确定位一个设备,将使用该设备的全部数据进行关联.结合更加多样化的数据,黑产设备的识别准确性也将大幅提升. 设备指纹从何而来?一般 ...

  4. java服务端开发 php_PHP使用thrift做服务端开发

    php中文网最新课程 每日17点准时技术干货分享 php使用thrift做服务端开发 thrift采用接口描述语言定义和创建服务,用二进制格式传输数据,体积更小.效率更高,对于高并发.数据量大和多语言 ...

  5. jQuery中的ajax、jquery中ajax全局事件、load实现页面无刷新局部加载、ajax跨域请求jsonp、利用formData对象向服务端异步发送二进制数据,表单序列化(异步获取表单内容)

    jQuery中使用ajax: 在jQuery中使用ajax首先需要引入jQuery包,其引入方式可以采用网络资源,也可以下载包到项目文件中,这里推荐下载包到文件中:市面上有多个版本的jQuery库,这 ...

  6. PostgreSQL 30天 培训视频(SQL基础,备份恢复,HA,服务端编程,大数据,内核,应用案例)

    Postgres2015全国用户大会将于11月20至21日在北京丽亭华苑酒店召开.本次大会嘉宾阵容强大,国内顶级PostgreSQL数据库专家将悉数到场,并特邀欧洲.俄罗斯.日本.美国等国家和地区的数 ...

  7. 服务端thrift 使用非阻塞式IO报异常Got an IOException in internalRead!

    参考:https://blog.csdn.net/z13192905903/article/details/103181204 参考:https://blog.csdn.net/xmtblog/art ...

  8. postgresql最全整理资料,PostgreSQL 30天 培训视频(SQL基础,备份恢复,HA,服务端编程,大数据,内核,应用案例)

    转载自:http://blog.163.com/digoal@126/blog/static/16387704020141229159715/ 希望通过这些视频帮到一些朋友, 同时对视频中的错误点烦请 ...

  9. 客快物流大数据项目(一百零七):物流信息查询服务接口开发解决方案

    文章目录 物流信息查询服务接口开发解决方案 一.业务需求 二.系统架构演变 1.​​​​​​​集中式架构 2.​​​​​​​​​​​​​​垂直拆分 3.分布式服务 ​​​​​​​4.面向服务架构(SO ...

最新文章

  1. session存储在redis/memcache/mysql
  2. 在游戏运营行业,Serverless 如何解决数据采集分析痛点?
  3. hazelcast入门教程_Hazelcast入门指南第5部分
  4. android sdk 8.1.0,OneAPM版本更新:Android SDK 1.0.8
  5. Flink-org.apache.flink.streaming.api.watermark.Watermark
  6. 微信小程序商城完整代码
  7. SCSI代码分析(5)SCSI驱动编程模式
  8. FreeMarker模板引擎实现页面静态化
  9. 高等数学教材上册复习
  10. 中国最令人崩溃的25个姓氏,排名第1位的,打死都想不到
  11. 作为零基础的新手,如何自学Java和JavaEE开发技术?
  12. 视频号计划,撑得起微博下一个十年?
  13. WT588F02B-8S(芯片代码C001_01)语音芯片在化妆品/保健品/食品行业保质期和使用期得应用解决方案
  14. 如何将pdf转换成ppt文件
  15. excel图表美化:用散点标记制作不一样的折线图
  16. java count sql_SQL优化之SELECT COUNT(*)
  17. 解析计算机科学与技术论文引言,计算机科学与技术专业毕业论文写作指导
  18. 微信小程序使用wx.downloadFile和wx.saveFile在真机调试和体验版没问题,线上却下载失败
  19. js 前端实现国际化配置
  20. 圣朱妮佩洛| San Junipero(4)

热门文章

  1. 又到红包季,QQ红包强势出击欲何为?
  2. html简单的任务管理系统实现,使用禅道来进行项目任务管理
  3. 2019年北京画室排名前十位(考中戏比较好的画室)
  4. 在非华为电脑上使用华为多屏协同
  5. 防御DDOS攻击终极指南
  6. 控制结构(强化):11.牛友
  7. SQL实现上一篇和下一篇
  8. python乒乓球比赛abc、xyz_小练习20160201:称量乒乓球
  9. Linux测试IP是否联通(两种方法)
  10. c# 实现的命令提示窗口玩快艇骰子游戏程序