php curl_multi 本身bug,导致接口业务从webman转golang

webman_fans

如题,业务需要同一时间请求多个第三方,abc
1> 如果a先返回,判断结果,正确的话,就将结果返回下游。业务结束。
2> 如果a返回错误,就看第二快返回的结果,如果c第二快返回,结果正确,就把c结果返回下游。业务结束。

之前使用 curl_multi 由于这函数本身的bug,一直循环,导致 超时。
理论上说,有几个第三方,就发送几次请求,而这个函数,会重复多次。

只能选天然支持并发的语言。比如go。

但依然不甘心,这些好用的框架,为啥不出个这种功能,类似go的协程呢??

期待大佬解决。

2425 2 0
2个回答

walkor 打赏

curl_multi 不会有bug,可能你用法不对。
我记得这个问题你发过帖子,下面有人回复了curl_multi用法,类似下面这样,你试下

<?php
$urls = array(
   "http://lxr.php.net/",
   "http://www.php.net/",
);
$mh = curl_multi_init();
foreach ($urls as $i => $url) {
    $conn[$i] = curl_init($url);
    curl_setopt($conn[$i], CURLOPT_RETURNTRANSFER, 1);
    curl_multi_add_handle($mh, $conn[$i]);
}

$result = '';

do {
    $status = curl_multi_exec($mh, $active);
    curl_multi_select($mh);
    $info = curl_multi_info_read($mh);
    if (false !== $info) {
       $result = curl_multi_getcontent($info['handle']);
       // 这里获得某个curl的结果,如果结果ok就break,不ok就等下一个结果
       //$is_ok = your_check($result);
       $is_ok = 1;
       if ($is_ok) {
           break;
       }
    }
} while ($status === CURLM_CALL_MULTI_PERFORM || $active);

foreach ($urls as $i => $url) {
    curl_close($conn[$i]);
}

// 最快的正确结果
echo $result;
  • webman_fans 2022-03-10

    这样会使服务器cpu占用率100%

  • webman_fans 2022-03-10

    CPU使用率
    100%

  • webman_fans 2022-03-10

    为了避免100%,就需要人为设置usleep休眠。

  • webman_fans 2022-03-10

    但实际业务,对时效性要求很严,比如250毫秒。

    这样大多数数据,就超时了。

  • webman_fans 2022-03-10

    还有一点,即使就一个url, do 循环内也会循环好多次。

    按道理,有几个url,就循环几次。

    这就属于curl_multi 函数本身的bug了吧。

  • webman_fans 2022-03-10

    所以,这种方法实际并不可行。

  • walkor 2022-03-10

    等下,忘记加multi_select了

  • webman_fans 2022-03-10

    为啥不支持类似swool协程呢?
    想确定,swool协程可以实现这种需求吗,
    golang的话,for循环就自带异步请求了。
    这样就可以根据url数量来使用多个 单次curl请求了。

  • nitron 2022-03-10

    想用协程就用Swoole做event-loop. workerman/webman可以用swoole当event-loop,
    Fiber比较新,8.1才支持,要跟进需要时间

  • walkor 2022-03-10
    <?php
    $urls = array(
       "http://lxr.php.net/",
       "http://www.php.net/",
    );
    $mh = curl_multi_init();
    foreach ($urls as $i => $url) {
        $conn[$i] = curl_init($url);
        curl_setopt($conn[$i], CURLOPT_RETURNTRANSFER, 1);
        curl_multi_add_handle($mh, $conn[$i]);
    }
    
    $result = '';
    
    do {
        $status = curl_multi_exec($mh, $active);
        curl_multi_select($mh);
        $info = curl_multi_info_read($mh);
        if (false !== $info) {
           $result = curl_multi_getcontent($info['handle']);
           // 这里获得某个curl的结果,如果结果ok就break,不ok就等下一个结果
           //$is_ok = your_check($result);
           $is_ok = 1;
           if ($is_ok) {
               break;
           }
        }
    } while ($status === CURLM_CALL_MULTI_PERFORM || $active);
    
    foreach ($urls as $i => $url) {
        curl_close($conn[$i]);
    }
    
    // 最快的正确结果
    echo $result;
  • walkor 2022-03-10

    加了一行 curl_multi_select($mh); 再试下

  • webman_fans 2022-03-10

    好的

  • webman_fans 2022-03-10

    加上之后依旧是100%,大佬

  • webman_fans 2022-03-10

    这个已经放弃了。webman使用swool协程,可以实现这种需求吗,之前没用过workerman,请大佬贴代码。。

  • webman_fans 2022-03-10

    这都是基于workerman,写的。之前没有用过这个。有没有webman版本的呀

  • walkor 2022-03-10

    加上 curl_multi_select($mh); 按道理不会消耗太多cpu才对。

    https://www.workerman.net/page/update
    webman 1.2.5 有 event-loop设置,config/server.php 里设置成设置'event_loop' => Workerman\Events\Swoole::class 则是用swoole作为底层,可以使用swoole的协程。我对swoole不熟悉,不清楚怎么和webman配合做的你的需求。

    你这个接口可能要用worekrman来做了。

  • webman_fans 2022-03-10

    嗯,其实需求就是 同一时间请求多个上游,看谁先返回,判断返回结果。

    由于整个过程需要300毫秒之内完成。所以 需要根据上游的数量来 同时发出 curl 动作。然后对比结果。

    如果for循环的话,需要等待全部上游都返回结果值。这样就超时了。

    我对swool协程也不是很清楚。不确定能不能实现这个需求??

    curl_multi 原本是最适合的。但可能是本身存在的bug,导致 cpu 一直100% 。

  • walkor 2022-03-10

    打印下 curl_multi_select($mh); 的结果,看下都有什么值。

  • nitron 2022-03-10

    中间用usleep(1)来释放CPU, curl_multi这个不是BUG,官方有说明,是本身执行逻辑就如此

    https://bugs.php.net/bug.php?id=61240
    https://bugs.php.net/bug.php?id=61141
  • webman_fans 2022-03-10

    好的,如果使用workderman的话,用swool协程可以不用使用 curl_multi了吗

  • nitron 2022-03-10
    我对swoole不熟悉

    建议看下swoole的文档

  • walkor 2022-03-10

    @nitron 好吧。看来curl_multi有些时候还是要配合sleep。
    @webman_fans ,用webman自定义进程写这个接口,用不到协程。等下我写个demo给你。

  • nitron 2022-03-10

    @walkor 没办法,大多建议usleep 100,但是题主这个要求300ms的,这个usleep值就有点太高,部分设置成1的似乎效果也还行

  • webman_fans 2022-03-10

    @walkor 自定义进程的话,可以通过程序实现吗,比如有2个上游,我就在这次请求种自定义两个进程,来分别用单次的curl 请求;

    如果有3个上游,我就自定义3个进程来请求。

    这种需求,可以实现吗?

  • walkor 2022-03-10

    不用那么麻烦,试下下面这个我写的例子。

  • webman_fans 2022-03-10

    如果我对swool和协程也不熟悉,不确定能不能实现。

    反正现在的curl_multi 坑很多。

    1是使用usleep, 本来就要求实效性很紧张的,有些需要200毫秒之内返回,还要减掉处理其他逻辑的时间,所以留给curl的时间就不多了。

    2 do { 循环内,会有几千次的无效循环,才会为true,才会进行下一步},这就造成无谓的浪费了。

    3 按理说,几个上游url,就请求几次。

    4 这属于函数本身的bug了。

walkor 打赏

安装workerman/http-client

composer require workerman/http-client

新建 process/Api.php

<?php

namespace process;

use Workerman\Connection\TcpConnection;
use Workerman\Protocols\Http\Request;
use Workerman\Psr7\Response;

class Api
{
    /**
     * @var \Workerman\Http\Client
     */
    protected $http;

    public function __construct()
    {
        $this->http = new \Workerman\Http\Client();
    }

    public function onMessage(TcpConnection $connection, Request $request)
    {
        $connection->sended = false;
        $urls = array(
            "http://lxr.php.net/",
            "http://www.php.net/",
        );
        foreach ($urls as $url) {
            $this->http->get($url, function(Response $response) use ($connection) {
                $body = (string)$response->getBody();
                $is_ok = 1; // 根据body判断是否ok
                if (!$is_ok) return; // 不ok就return
                if (!$connection->sended) { // 已经发送过结果了,不用再发了
                    $connection->sended = true;
                    $connection->send($body); // 给客户端发送结果
                }
            });
        }
    }
}

配置 config/process.php

return [
    // 这里省略了其它配置...

    'my_api' => [
        'handler' => \process\Api::class,
        'listen' => 'http://0.0.0.0:1234',
        'count' => cpu_count(),
    ]
];

重启webman

在nginx里加一个转发配置

nginx加一个配置,将原有api请求路径的转发到 1234 端口

location /your/api/path {
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header Host $host;
      proxy_pass http://127.0.0.1:1234;
}
  • webman_fans 2022-03-10

    大佬,是这样的,我可以在程序里直接调这个方法吗,因为,上游返回之后,不是直接返回客户端的,还需要进行一些逻辑处理,才会返给客户端。

  • webman_fans 2022-03-10

    还需要回到原来的主程序中去。拼接其他的参数,才能返回客户端。

  • webman_fans 2022-03-10

    @walkor

  • webman_fans 2022-03-10

    请求上游接口的时候,也需要从主程序里,组织参数,post请求。

  • walkor 2022-03-10

    那你就在你原来程序里curl调用这个1234端口

  • webman_fans 2022-03-10

    怎么这么像 rpc,只不过是本地的过程调用。。

  • nitron 2022-03-10

    然后中间又多了一个curl调用的消耗-__,- ,反正这种涉及第三方系统调用的需求,只要中间网络有问题就GG

  • webman_fans 2022-03-10

    foreach ($urls as $url) 我看这里用了 循环,这里的循环是需要等到上一个返回之后,才会执行下一个吗,还是异步执行的呢。

  • webman_fans 2022-03-10

    @walkor

  • walkor 2022-03-10

    异步的

  • nitron 2022-03-10

    这个httpclient是异步的

  • webman_fans 2022-03-10

    嗯,调用这个方法,除了curl,webman本身有什么机制可以更快的调用吗

  • walkor 2022-03-10

    一般来说网络调用用什么方法速度都差不多,瓶颈不在方法,在网络。

  • webman_fans 2022-03-10

    因为,这个文件就在我本地。我这么直接调用它。

  • webman_fans 2022-03-10

    在app/controller里调用 process里的函数

  • webman_fans 2022-03-10

    或者能不能做成通用函数,写进 app/function里

  • webman_fans 2022-03-10

    @walkor @nitron

  • walkor 2022-03-10

    它属于不同进程,不能文件调用。性能最好的办法就是把逻辑都写在这个自定义api进程里。

  • nitron 2022-03-10

    直接用你原来程序里curl调用这个1234端口,本掉调用网络消耗可以忽略不计

  • evilk 2022-03-10

    不能在程序里,直接调用这个function,只能curl到1234端口

  • nitron 2022-03-10

    总之结论就是

    跨网络的第三方系统调用,对响应时间影响最大的是你与第三方系统之间的网络状况
  • webman_fans 2022-03-10

    好的,尝试一下

  • webman_fans 2022-03-10

    大佬们,config里的 count 这个值,跟 urls 的个数是相关的吗,需要调成一样的吗,还是两个无关的量。

  • webman_fans 2022-03-10

    比如有10个上游链接,config里的这个count值,需要改成10吗

  • nitron 2022-03-10

    count是开启的进程数

  • walkor 2022-03-10

    和url数量无关,它是进程数,cpu的1-3倍都行。

  • webman_fans 2022-03-10

    好的,多谢大佬们

  • webman_fans 2022-03-10

    大佬,还有一个问题,foreach 中请求,默认的超时时间,是多少

  • webman_fans 2022-03-10

    怎么自定义这个超时时间

  • webman_fans 2022-03-10

    比如自定义 260毫秒

  • webman_fans 2022-03-11

    @walkor

  • webman_fans 2022-03-11

    比如自定义post的请求数据大小

  • walkor 2022-03-11

    http-client没有设置post数据大小的地方。post数据大小你可以自己计算,用 strlen(http_build_query($post))。

    $options = [
        'connect_timeout'   =>0.26, // 单位秒,0.26就是260毫秒
        'timeout'           => 0.26,
    ];
    $this->http = new \Workerman\Http\Client($options );

    设置超时这么设置

年代过于久远,无法发表回答
×
🔝