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文件
touch ~/.ssh/config
在config文件里添加如下內容(User表示你的用戶名)
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是否配置成功
ssh -T git@github.com
添加文件
git remote add origin git@github.com:zhuanxuhit/laravel-doc.git
git push -u origin master
1.2 case:Rate Limit
以rate limit為例來使用swoole開發。
1.2.1 最簡單的隔離算法
思想很簡單,計算相鄰兩次請求之間的間隔,當速率大于Rate的時候,就拒絕請求。
技能分析:
- swoole的swoole_http_server功能,監聽端口,等待客戶端請求
- 注冊回調,當請求到來的時候,處理請求
代碼示例:
<?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