swoole入門abc

1. 入門abc

1.1 github帳號添加

  • 第一步依然是配置git用戶名和郵箱
git config user.name "用戶名"
git config user.email "郵箱"
  • 生成ssh key時同時指定保存的文件名
ssh-keygen -t rsa -f ~/.ssh/id_rsa.github -C "email"
  • 新增並配置config文件php

    touch ~/.ssh/confightml

在config文件裏添加以下內容(User表示你的用戶名)laravel

Host *.github.com
IdentityFile ~/.ssh/id_rsa.github
User test
  • 上傳key到github
pbcopy < ~/.ssh/id_rsa.github.pub
  eval "$(ssh-agent -s)"
  ssh-add ~/.ssh/id_rsa.github
  • 測試ssh key是否配置成功git

    ssh -T git@github.comgithub

  • 添加文件算法

git remote add origin git@github.com:zhuanxuhit/laravel-doc.git
git push -u origin master

1.2 case:Rate Limit

以rate limit爲例來使用swoole開發。json

1.2.1 最簡單的隔離算法

思想很簡單,計算相鄰兩次請求之間的間隔,當速率大於Rate的時候,就拒絕請求。服務器

技能分析:swoole

  1. swoole的swoole_http_server功能,監聽端口,等待客戶端請求
  2. 註冊回調,當請求到來的時候,處理請求

代碼示例:網絡

<?php namespace Swoole\Rate;

class Simple {

    protected $http;

    protected $lastTime;

    protected $rate = 0.1;
    /**
     * Simple constructor.
     *
     * @param $port
     */
    public function __construct($port)
    {
        $this->http = new \swoole_http_server('0.0.0.0',$port);
        $this->http->on('request',array($this,'onRequest'));
    }

    public function onRequest( \swoole_http_request $request, \swoole_http_response $response )
    {
        $lastTime = $this->lastTime;
        $currentTime = microtime(true);

        if(($currentTime-$lastTime)<1/$this->rate){
           // deny
        }
        else {
            $this->lastTime = $currentTime;
            // access
        }
    }

    public function start()
    {
        $this->http->start();
    }
}

$simple = new Simple(9090);
$simple->start();

分析上面的代碼,咱們發現會有什麼問題?若是兩個請求同時進來,都讀到了lastTime,沒有被拒絕,可是這兩個請求自己是已經請求過快了。

這個疑問產生的緣由是對於swoole的網絡處理模型不是很清晰,若是請求是串行處理的,那不會有什麼問題?可是若是請求是併發處理,那多個請求可能讀到的是同一個時間戳,致使瞬間併發很大,出現問題。

首先來解決第一個問題:swoole是什麼

swoole 是一個網絡通訊框架,首要解決的問題是什麼?通訊問題,以後就是高性能這個話題了,高性能主要從3個方面考慮

1) I/O調度模型:同步阻塞I/O(BIO)仍是非阻塞I/O(NIO)。

2) 序列化框架的選擇:文本協議、二進制協議或壓縮二進制協議。

3) 線程調度模型:串行調度仍是並行調度,鎖競爭仍是無鎖化算法。

swoole在IO模型上是使用異步阻塞IO實現,調度模型則是採用Reactor,簡單說就是有一個線程專門負責IO操做,當關心事件發生的時候,進行回調函數處理,具體分析見下一章。

經過修改代碼,使用ab test工具,我可以簡單的模擬上面的討論到的併發問題:

public function onRequest( \swoole_http_request $request, \swoole_http_response $response )
    {
        $lastTime = $this->lastTime;
        $currentTime = microtime(true);
        $pid =($this->http->worker_pid);
        if(($currentTime-$lastTime)<1/$this->rate){
            
            echo "deny worker_pid: $pid lastTime:$lastTime currentTime:$currentTime\n";
        }
        else {
            $this->lastTime = $currentTime;
           
            echo "accept worker_pid: $pid lastTime:$lastTime currentTime:$currentTime\n";
        }
    }

測試腳本

ab -n10 -c5 http://0.0.0.0:9090/

測試結果:

accept worker_pid: 45674 lastTime: currentTime:1463470993.3306
accept worker_pid: 45675 lastTime: currentTime:1463470993.331
accept worker_pid: 45671 lastTime: currentTime:1463470993.3318
accept worker_pid: 45672 lastTime: currentTime:1463470993.3322
accept worker_pid: 45673 lastTime: currentTime:1463470993.333
deny worker_pid: 45674 lastTime:1463470993.3306 currentTime:1463470993.3344
deny worker_pid: 45673 lastTime:1463470993.333 currentTime:1463470993.3348
deny worker_pid: 45675 lastTime:1463470993.331 currentTime:1463470993.3351
deny worker_pid: 45671 lastTime:1463470993.3318 currentTime:1463470993.3352
deny worker_pid: 45672 lastTime:1463470993.3322 currentTime:1463470993.3354

能夠很顯然的看到,併發請求來的時候,讀到的lastTime都是未設置過的

模擬出併發問題後,這個關於swoole中有進程模型也很好測試出來:

public function serverInfoDebug()
    {
        return json_encode(
            [
                'master_id' => $this->http->master_pid,//返回當前服務器主進程的PID。
                'manager_pid' => $this->http->manager_pid,//返回當前服務器管理進程的PID。
                'worker_id' => $this->http->worker_id,//獲得當前Worker進程的編號,包括Task進程
                'worker_pid' => $this->http->worker_pid,//獲得當前Worker進程的操做系統進程ID。與posix_getpid()的返回值相同。
            ]
        );
    }

啓動成功後會建立worker_num+2個進程。主進程+Manager進程+worker_num個Worker進程。

完整地址:
https://github.com/zhuanxuhit/php-recipes/blob/master/app/SwooleAbc/Rate/Simple.php

那回到上面的問題,怎麼解決併發問題呢?在C++等語言中,很好解決這個問題,使用鎖,互斥訪問。

寫到這的時候,發現個問題,發如今回調中,每一個worker在處理onRequest函數的時候,this都是一個新的,爲何呢?由於worker進程都是由Manager進程fork()出來的,天然數據是新的一份了。

如今要解決的問題變爲:如何在swoole中實現多個進程的數據共享功能

能夠看到https://github.com/swoole/swoole-src/issues/242
其中建議,能夠經過使用swoole提供的swoole_table來作,代碼以下:

public function onRequest( \swoole_http_request $request, \swoole_http_response $response )
    {
        $currentTime = microtime(true);
        $pid =($this->http->worker_pid);
        $this->table->lock();
        $lastTime = $this->table->get( 'lastTime' );
        $lastTime = $lastTime['lastTime'];
        if(($currentTime-$lastTime)<1/$this->rate){
            $this->table->unlock();
            //deny
        }
        else {
            $this->table->set( 'lastTime', [ 'lastTime' => $currentTime] );
            $this->table->unlock();
            // access
        }
    }

再次測試,可以發現很好的知足了要求。

1.2.2 最清晰的吊桶算法

隔離算法的問題很明顯,使用ab -n2 -c2 http://0.0.0.0:9090/,同時併發2個請求就被拒絕了,所以只計算了相鄰兩次的間隔,而沒有關注1s內的請求,所以一個改進思路就是以s爲key,記錄時間戳下來。下面是實現

public function onRequest( \swoole_http_request $request, \swoole_http_response $response )
    {
        $currentTime = time();
        $this->table->lock();
        $count = $this->table->get( (string)$currentTime );
//        (new Dumper)->dump($count);
        if($count){
            $count = $count['count'];
        }
        else {
            $count = 0;
        }

        if($count >$this->rate){
            $this->table->unlock();
            // deny
        }
        else {
            $this->table->set( (string)$currentTime, [ 'count' => $count + 1] );
            $this->table->unlock();
            //accept
        }
    }

因爲每s的數據都記錄了,沒有過時,致使數據不斷增加,有問題,並且因爲1s是分割的,不是連續的,必然會形成最開始腦圖中的bad case。

因而就有了下面的第三個方法:最精確的隊列算法

1.2.3 最精確的隊列算法

思路上就是將請求入隊,記錄請求的時間,這樣就能夠判斷任意連續的多個請求,其是不是在1s以內了

首先看下這個算法思路:假設rate=5,當請求到來的時候,獲得當前請求編號,而後減5獲得index,而後判斷兩次請求之間的時間間隔,是否大於1s,若是大於則accept,不然deny

n-5 n-4 n-3 n-2 n-1 n n+1 n+2 n+3 n+4 n+5

如今來的請求是n,則去n-5,爲何是減5,所以rate是5,則當qps爲6的時候就deny,所以須要判斷n-5到n這6個請求的間隔!

算法有了,下面就是在swoole中怎麼實現隊列的問題了,這個隊列還須要在進程間共享。

咱們可使用swoole_table來實現,另外還須要一個計數器,給每一個請求編號,實現以下:

// 每一個請求過來後的是否判斷經過,這個操做必需要單點,串行,因此也就是說必需要加速
        $this->table->lock();
        $count = $this->counter->add(1);
        $bool = true;
        $currentCount = $count + 1;
        $previousCount = $count - $this->rate;
        if($currentCount<=$this->rate){
            $this->table->set( $count, [ 'timeStamp' => $currentTime ] );
            $this->table->unlock();
        }
        else {
            $previousTime = $this->table->get( $previousCount );
            if ( $currentTime - $previousTime['timeStamp'] > 1 ) {
                $this->table->set( $currentCount, [ 'timeStamp' => $currentTime ] );
                $this->table->unlock();
            } else {
                // 去除 deny
                $bool = false;
                $this->counter->sub( 1 );
                $this->table->unlock();
            }
        }

上面有一個核心點,以前一直沒有注意到的:對全部請求的處理都是須要互斥的,便是一個單點,處理完後才能轉發給真正的業務邏輯進行處理。

所以能夠將Rate的邏輯抽離出來,做爲一個服務提供,這個之後講服務化的時候再作的。

1.2.4 最傳統的令牌算法

令牌算法相似小米搶購,放量出來必定的票,當人想進來搶的時候,必須有F碼才能進行搶購,而票的放出是按必定速率產生的。

上面算法實現時,須要用到swoole的定時器功能,須要在OnWorkerStart的回調的時候使用

public function onWorkerStart(\swoole_server $server, $worker_id)
    {
        if($worker_id ==0){
            $server->tick( 1000/$this->rate, [$this,'addTicket'] );
        }
    }

而請求到來的時候,就是經過getTicket獲取資格,沒票的時候,直接返回false

完整的github地址:
https://github.com/zhuanxuhit/php-recipes/tree/master/app/SwooleAbc/Rate
Rate limit的介紹:http://www.bittiger.io/classpage/hfjPKuZaLxPLyL5iN
gitbook地址:
https://zhuanxuhit.gitbooks.io/swoole-abc/content/chapter1.html

原文地址:https://www.jianshu.com/p/7c16cb61b59d

相關文章
相關標籤/搜索