问题描述

最近将IOS书城容器化,切换流量后。正常的业务测试了一般,都没发现问题。线上的错误监控系统也没有报警,以为迁移工作又告一段落了,暗暗的松了一口气。紧接着,报警邮件来了,查看发现是一个苹果支付相关接口调用的curl错误,错误码为"56",错误描述为:“Failure with receiving network data”接收网络数据失败。

机器 : 192.168.1.1
当前URL : /xxx/recharge/apple?xxxxxxxxxxxxx
接口URL : http://192.168.1.2:18000/third/apple/pay
错误信息 : Failure with receiving network data.
错误码 : 56
type : curl
时间 : 2016-09-19 22:09:37 +0800 从09-19 21:20:15到09-19 22:09:38共计错误:11

问题分析

整体的业务流程:用户使用苹果支付,客户端拿到用户支付后用户返回的code,传给php,php 使用curl post提交给用户中心,用户中心拿到code后请去苹果支付的接口验证是否合法。

怀疑方向:

1、code有超过4000个字节长度,而curl post提交超过1024个字节后,会发送100-continue,将请求分为2步。

2、书城服务器与接口服务器之间的网络问题

3、libcurl的BUG(PHP这边的HTTP Clinet使用的libcurl库封装的)

4、php7存在相关的bug

5、docker当前版本存在bug

怀疑验证:

1、 通过本地向用户中心支付接口发起请求,打印头信息,头信息确实返回了两次,第一次为"100-continue"。鸟哥在官网的文章中提过类似问题,在curl代码中增加设置项,将"Expect"设为空,然后测试,发现不会分步骤了,本地测试代码没有报错。然后提交上线。

 curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:'));

curl设置Expect

发现错误并没有解决,报错依然存在。还原之前的代码,排除掉怀疑1

2、将抓网卡数据包的命令给运维,让其帮忙加一下任务,监听网卡流量信息。当发现问题后,停掉脚本,发给我们。我们根据错误邮件报警时间与网卡流量中的记录进行对照

找出具体的数据包信息。命令如下:

 tcpdump -i team0 host 192.168.7.154 and port 29000 -w /tmp/apple_pay.cap

tcpdump 抓包

后我就等着发报警邮件,发现报警之后,让运维终止掉脚本,然后通过"wireshark"分析,对比一下正常的和有问题的:

     (正常情况)    
                          
        (有问题的)
           
     可以从序列号11-12看到有问题的tcp连接在超过2秒后,client端(1.1)自动发送了FIN。服务端进行了确认,但是序列号14看到,大概4s后服务端才返回数据,这个时候客户端已经不接受数据了。
    这个时候又引入了一个怀疑点,跟书城服务器的网络参数配置有关。
   
    3、通过命令"sysctl -a | grep tcp" 查看linux内核配置的tcp相关的参数。经过各种查资料发现没有一个是跟当前2秒中断对应的上的。排除掉怀疑2
 
   4、php请求apple/pay接口是这设置的10S超时,为什么2S就返回了呢,会不会是libcurl定时器的BUG呢?
       带着问题继续找答案,php一般请求服务接口代码如下。
 

 $ch = curl_init();//初始化curl
curl_setopt($ch, CURLOPT_URL, "http://192.168.7.154:29000/third/apple/pay");//设置curl请求URL
curl_setopt($ch, CURLOPT_TIMEOUT, 10);//设置超时
curl_setopt($ch, CURLOPT_POSTFIELDS, array(k=>v));//设置POST请求的数据
$data = curl_exec($ch);//执行请求,并获取响应数据
curl_close($ch); //关闭

php curl请求设置

继续深入到  curl_exec php源码中 https://github.com/php/php-src/blob/2a71140d54e956f11acbe33f2681d27086874d2e/ext/curl/interface.c#L3016

 PHP_FUNCTION(curl_exec)
{
CURLcode error;
zval *zid;
php_curl *ch;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "r", &zid) == FAILURE) {
return;
}
if ((ch = (php_curl*)zend_fetch_resource(Z_RES_P(zid), le_curl_name, le_curl)) == NULL) {
RETURN_FALSE;
}
_php_curl_verify_handlers(ch, 1);
_php_curl_cleanup_handle(ch);
error = curl_easy_perform(ch->cp);
........................
........................
........................
}

php 内核中curl_exec的实现

发现PHP的curl_exec函数最终调用 libcurl中的curl_easy_perform函数,线上服务的libcurl是7.29.0版本,继续看libcurl源码

ibcurl 提供的C函数API大概如下,可以看出来php的curl只是对libcurl做了一个简单的封装。libcurl API列表

 //libcurl 提供的C函数API大概如下
curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_URL, "http://xxx/");
res = curl_easy_perform(curl);
curl_easy_cleanup(curl);

libcurl 调用的基本函数

curl 设置定时器的流程大概如下:

curl_easy_perform    curl_multi_wait    Curl_poll

 int Curl_wait_ms(int timeout_ms)
{
if(!timeout_ms)
return 0;
if(timeout_ms < 0) {
SET_SOCKERRNO(EINVAL);
return -1;
}
pending_ms = timeout_ms;
initial_tv = curlx_tvnow();
do {
#if defined(HAVE_POLL_FINE)
r = poll(NULL, 0, pending_ms);
#else
pending_tv.tv_sec = pending_ms / 1000;
pending_tv.tv_usec = (pending_ms % 1000) * 1000;
r = select(0, NULL, NULL, NULL, &pending_tv);
#endif /* HAVE_POLL_FINE */
if(r != -1)
break;
error = SOCKERRNO;
if(error && error_not_EINTR)
break;
pending_ms = timeout_ms - elapsed_ms;
if(pending_ms <= 0)
break;
} while(r == -1); if(r)
r = -1;
return r;
}

libcurl 中的Curl_wait_ms函数

整个libcurl的超时机制都没有问题,基本排除怀疑3。

   
   5、难道是PHP的BUG,最新刚刚升级到PHP7。
   在docker里面测试 s.php测试代码如下:
 sleep(5);
echo "asdadadasdsad";

php设置sleep

以http的方式请求这个php

[root@BJ-M5-PHP-7-225 apad]# time curl "http://127.0.0.1:8046/s.php"
asdadadasdsad
real 0m2.010s
user 0m0.004s
sys 0m0.005s

很奇怪的问题出现了 real 0m2.010s ,明明是sleep 5 秒为啥只执行了2 秒就结束了,而且还返回了数据。

php s.php 执行正常

php -S 127.0.0.1:8046(php内置的web server) 执行正常

python以web server的方式

 #!/usr/bin/env python
# -*- coding: utf-8 -*-
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import time
from tornado.options import define, options
define("port", default=8888, help="run on the given port", type=int)
class MainHandler(tornado.web.RequestHandler):
def get(self):
time.sleep(5)
self.write("Hello, world") def main():
tornado.options.parse_command_line()
application = tornado.web.Application([
(r"/", MainHandler),
])
http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(options.port)
tornado.ioloop.IOLoop.current().start() if __name__ == "__main__":
main()

执行正常

nginx sleep 5 执行正常

   location /sub1 {
echo_sleep 5;
echo "hello world";
}

上面测试均在docker 1.10.3容量里面进行, 在物理机器上并无此问题。经过上面的测试发现,在docker 1.10.3只有以PHP-FPM运行时,才会出现此问题。

猜想可能是fpm的问题,看了一下php-fpm.conf的配置信息。发现了request_slowlog_timeout=2s,重大发现,唯一一个2s有点重合的点。立即把修改了10s,结果测试sleep 5 正常了。

request_slowlog_timeout是记录FPM方式执行php的慢日志时间,超过设置的时间就会有慢日志记录。很好奇为什么超过request_slowlog_timeout执行的php会出现问题,物理机为什么是正常?带着问题继续看FPM。

经过分析fpm源码,发现了使用request_slowlog_timeout的流程。在fpm work 进程处理请求时,master进程做健康检查,其中就有slowlog_timeout。

fpm_pctl_check_request_timeout源码

 if (child->slow_logged.tv_sec == 0 && slowlog_timeout &&
proc.request_stage == FPM_REQUEST_EXECUTING && tv.tv_sec >= slowlog_timeout) { str_purify_filename(purified_script_filename, proc.script_filename, sizeof(proc.script_filename));
child->slow_logged = proc.accepted;
child->tracer = fpm_php_trace;//记录执行慢的php栈调用的回调函数
fpm_trace_signal(child->pid);//调用ptrace函数,追踪进程
....................
}

fpm_pctl_check_request_timeout 慢日志记录

pm_trace_signal源码

 //开始追踪进程
int fpm_trace_signal(pid_t pid){
if (0 > ptrace(PTRACE_ATTACH, pid, 0, 0)) {
zlog(ZLOG_SYSERROR, "failed to ptrace(ATTACH) child %d", pid);
return -1;
}
return 0;
}
//关闭追踪
int fpm_trace_close(pid_t pid){
if (0 > ptrace(PTRACE_DETACH, pid, (void *) 1, 0)) {
zlog(ZLOG_SYSERROR, "failed to ptrace(DETACH) child %d", pid);
return -1;
}
traced_pid = 0;
return 0;
}
//获取栈调用信息
int fpm_trace_get_long(long addr, long *data){
errno = 0;
*data = ptrace(PTRACE_PEEKDATA, traced_pid, (void *) addr, 0);
if (errno) {
zlog(ZLOG_SYSERROR, "failed to ptrace(PEEKDATA) pid %d", traced_pid);
return -1;
}
return 0;
}

获取php栈调用的函数

关键性函数来了ptrace

ptrace解释:

ptrace 提供了一种机制使得父进程可以观察和控制子进程的执行过程,ptrace 还可以检查和修改该子进程的可执行文件在内存中的镜像及该子进程所使用的寄存器中的值。这种用法通常来说,主要用于实现对进程插断点和跟踪子进程的系统调用。是的你没有想错,strace、gdb就是通过它实现的。

为啥说ptrace是关键性函数呢?看如下两种strace跟踪系统调用。

 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], SA_RESTORER, 0x7fc8f5ee7670}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
nanosleep({5, 0}, {2, 499689873}) = ? ERESTART_RESTARTBLOCK (Interrupted by signal)
--- SIGSTOP {si_signo=SIGSTOP, si_code=SI_USER, si_pid=1879, si_uid=0} ---
--- stopped by SIGSTOP ---
--- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=1879, si_uid=0} ---
restart_syscall(<... resuming interrupted call ...>) = 0 //请注意这行
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], SA_RESTORER, 0x7fc8f5ee7670}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0

正常的strace追踪

 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], SA_RESTORER, 0x7fc8f5ee7670}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
nanosleep({5, 0}, {2, 499689873}) = ? ERESTART_RESTARTBLOCK (Interrupted by signal)
--- SIGSTOP {si_signo=SIGSTOP, si_code=SI_USER, si_pid=1879, si_uid=0} ---
--- stopped by SIGSTOP ---
--- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=1879, si_uid=0} ---
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], SA_RESTORER, 0x7fc8f5ee7670}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0

异常的strace追踪,在docker 1.10.3中

看出来区别没?注意正常strace追踪的第8行,restart_syscall(<... resuming interrupted call ...>),重新恢复中断的调用。

PHP的慢日志实现方式是这样的:

1、调用ptrace的PTRACE_ATTACH命令,master会进程通过向子进程发送SIGSTOP信号,此时子进程会变成TASK_TRACED状态,被追踪状态。

即:ptrace(PTRACE_ATTACH, pid, 0, 0)); 这行代码

2、 调用ptrace的PTRACE_PEEKDATA命令,来获取子进程的栈调用信息。

即:ptrace(PTRACE_PEEKDATA, traced_pid, (void *) addr, 0); 这行代码

3、 调用ptrace的PTRACE_DETACH命令,结束追踪。master会进程通过向子进程发送 PTRACE_CONT信号,此时子进程会变成TASK_RUNING状态。

即:ptrace(PTRACE_DETACH, pid,(void *) 1, 0); 这行代码

    问题在于 docker 1.10.3 中 收到 SIGCONT信号后,并没有执行restart_syscall,恢复进程执行的上下文,改变了进程运行时上下文。
    因此子进程受到SIGSTOP到SIGCONT这段时间内,正在被执行的函数由于运行时上下文导致执行异常。后面的代码继续执行。

sleep(5);
      echo "asdadadasdsad";

php中的这两行代码,在执行到sleep(5)的过程中,触发了fpm的慢日志记录,进程被暂停,等到恢复时,由于docker 1.10.3BUG,进程上下文被改变导致sleep执行出问题,但是后面的echo 继续执行。

6、将docker版本从1.10.3升级到1.11.2,通过步骤5的测试,发现问题不存在。

结论

1、罪魁祸首是docker1.10.3的SIGCONT信号处理BUG,而且fpm slowlog配置"request_slowlog_timeout=2s"调用了ptrace正好发送了SIGCONT信号。

2、php curl接口之前设置的超时时间为10秒,而由于支付接口请求苹果支付那边的时间比较长,会出现很多超时现象

问题解决办法

1、目前先通过关闭fpm的slowlog来解决(临时)

2、后续会全量升级docker版本(永久)

在docker以FPM-PHP运行php,慢日志导致的BUG分析的更多相关文章

  1. CentOS 6.4在运行XFS时系统crash的bug分析

    最近有一台CentOS 6.4的服务器发生多次crash,kernel version 是Linux 2.6.32-431.29.2.el6.x86_64.从vmcore-dmesg日志内容及cras ...

  2. 使用 Docker 搭建 Java Web 运行环境

    黄勇的博客 Docker 是 2014 年最为火爆的技术之一,几乎所有的程序员都听说过它.Docker 是一种“轻量级”容器技术,它几乎动摇了传统虚拟化技术的地位,现在国内外已经有越来越多的公司开始逐 ...

  3. 转:使用 Docker 搭建 Java Web 运行环境

    原文来自于:http://www.codeceo.com/article/docker-java-web-runtime.html Docker 是 2014 年最为火爆的技术之一,几乎所有的程序员都 ...

  4. .NET遇上Docker - Docker集成Cron定时运行.NETCore(ConsoleApp)程序.md

    配置项目的Docker支持 对于VS中Docker的配置,依旧重复一些废话. 给项目添加Docker支持,VS2015可以直接使用Docker for VS插件,VS2017在安装时选择容器支持.VS ...

  5. 利用远程服务器在docker容器搭建pyspider运行时出错的问题

    This system supports the C.UTF-8 locale which is recommended. You might be able to resolve your issu ...

  6. Docker在Linux上运行NetCore系列(五)更新应用程序

    转发请注明此文章作者与路径,请尊重原著,违者必究. 本篇文章与其它系列文章不同,为了方便测试,新建了一个ASP.Net Core视图应用. 备注:下面说的应用,只是在容器中运行的应用程序. 查看现在运 ...

  7. Docker在Linux上运行NetCore系列(一)配置运行DotNetCore控制台

    转发请注明此文章作者与路径,请尊重原著,违者必究. 系列文章:https://www.cnblogs.com/alunchen/p/10121379.html 本篇文章操作系统信息 Linux:ubu ...

  8. 使用 Docker 搭建 Java Web 运行环境(转)

    原文 http://www.importnew.com/21798.html Docker 是 2014 年最为火爆的技术之一,几乎所有的程序员都听说过它.Docker 是一种“轻量级”容器技术,它几 ...

  9. Docker学习笔记之运行和管理容器

    0x00 概述 容器是基于容器技术所建立和运行的轻量级应用运行环境,它是 Docker 封装和管理应用程序或微服务的“集装箱”.在 Docker 中,容器算是最核心的部分了,掌握容器的操作也是 Doc ...

随机推荐

  1. MySQL 分区表原理及使用详解

    今天统计数据的时候发现一张表使用了表分区,借此机会记录一下. 1. 什么是表分区? 表分区,是指根据一定规则,将数据库中的一张表分解成多个更小的,容易管理的部分.从逻辑上看,只有一张表,但是底层却是由 ...

  2. css样式-表格优化

    1.表格的初步优化 index.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8&qu ...

  3. LeetCode::Remove Duplicates from Sorted List II [具体分析]

    Given a sorted linked list, delete all nodes that have duplicate numbers, leaving only distinct numb ...

  4. 移动web开发框架研究

    纯粹的总结一下移动web开发框架,移动web开发框架有jQuery Mobile .Sencha Touch等等,他们都来源于web开发,是成熟的框架.jQuery Mobile出自于jQuery家族 ...

  5. HttpClient get返回String类型 JAVA

    public static String httpGet(String url) { // get请求返回结果 String strResult = ""; try { Defau ...

  6. js字符串转json

    1,eval方式解析,这是最早的解析方式了.如下: 代码如下: function strToJson(str){ var json = eval('(' + str + ')'); return js ...

  7. python 下的数据结构与算法---3:python内建数据结构的方法及其时间复杂度

    目录 一:python内部数据类型分类 二:各数据结构 一:python内部数据类型分类 这里有个很重要的东西要先提醒注意一下:原子性数据类型和非原子性数据类型的区别 Python内部数据从某种形式上 ...

  8. zip命令的使用

    zip命令可以用来将文件压缩成为常用的zip格式.unzip命令则用来解压缩zip文件. 1. 我想把一个文件abc.txt和一个目录dir1压缩成为yasuo.zip: # zip -r yasuo ...

  9. PhoneGap移动开发框架

    phonegap是一个跨平台的移动app开发框架,可以把html css js写的页面打包成跨平台的可以安装的移动app,并且可以调用原生的几乎所有的功能,比如摄像头,联系人,加速度等    看到一篇 ...

  10. 完整的Ajax实例

    写在前面的话: 用了很久的Asp.Net Ajax,也看了段时间的jquery中ajax的应用,但到头来,居然想不起xmlHttpRequest的该如何使用了. 以前记的也不怎么清楚,这次就重新完整的 ...