內(nèi)網(wǎng)http接口,高并發(fā),要求響應(yīng)在100ms內(nèi),單機需要支持5000+ QPS。
請求參數(shù)為訂單ID(數(shù)字int類型),業(yè)務(wù)邏輯為判斷本地數(shù)據(jù)庫中訂單是否存在,mysql大概100萬條記錄。
數(shù)據(jù)庫中訂單會隨時增加,每天增加幾百條。
服務(wù)器資源有限,越省資源越好。
php-fpm的框架都試過了,opcache全開最高也就200QPS左右,距離5000QPS相距甚遠(yuǎn)。
求一個高并發(fā)方案,現(xiàn)在打算用webman。redis能不用就不用。
用webman + bitmap完美解決 QPS 11萬+,性能遠(yuǎn)遠(yuǎn)遠(yuǎn)元超預(yù)期,webman神一般的存在,詳情見7樓回復(fù)。
是我我就這樣試試:
public function existsxx(Request $request){
if(Redis::exists($request->get(''oid)){
return 1;
}
// 查詢庫
....
}
固定每秒500 - 1000 請求,數(shù)據(jù)庫基礎(chǔ)數(shù)據(jù)30萬 數(shù)據(jù)庫每天新增幾千數(shù)據(jù)
請求來時判斷數(shù)據(jù)庫中是否存在數(shù)據(jù),數(shù)據(jù)內(nèi)容為:(34 - 64位字符串)
webman 群友給我的建議是將數(shù)據(jù)同步給redis,從redis中判斷(redis 理論每秒支持10萬+查詢并發(fā))
一些開發(fā)群給我的建議是上GO JAVA (當(dāng)然無視這些吊毛打心眼里看不起php)
人啊,要認(rèn)清現(xiàn)實,不要指著個正常的拖拉機要他跑出F1的速度
給結(jié)論,不想上各類緩存手段就堆硬件
MySQL查詢走PK基本是最快的了,select * from tbl where pk = ?
數(shù)據(jù)庫查詢內(nèi)就幾ms,算上網(wǎng)絡(luò)消耗一來一回就二三十ms
我給你算個數(shù)據(jù)
FPM要5000qps,意味著1秒內(nèi)要創(chuàng)建/銷毀5000MySQL連接,問題在哪看到了么
直接查mysql 扛不住呀,redis 是最好的處理方案 bitmap 你可以嘗試一下,這個理論 最省redis內(nèi)存,然后入庫 還是用隊列最好,webman進(jìn)程多開一切,理論 5000qps 沒啥問題 ,最好是 webman 和 redis 在一個機器 他們直接通信基本能做3ms以內(nèi),你可以試試
webman常駐內(nèi)存的,直接走webman內(nèi)存緩存數(shù)據(jù),數(shù)據(jù)不存在時再走mysql,我們試過,性能無敵,架構(gòu)也不用做什么改動。
代碼類似這樣,壓測試下
<?php
namespace app\controller;
use support\Db;
use support\Request;
class IndexController
{
// 緩存的數(shù)據(jù)
protected static $data = [];
public function index(Request $request)
{
$id = $request->get('id');
return json(['result' => static::get($id)]);
}
protected static function get($id)
{
// 初始化緩存
if (!static::$data) {
ini_set('memory_limit', '512M');
static::$data = array_flip(Db::table('orders')->pluck('id')->toArray());
}
// 緩存不存在則查數(shù)據(jù)庫
if (!isset(static::$data[$id])) {
if (Db::table('orders')->find($id)) {
// 訂單存在繼續(xù)緩存
static::$data[$id] = 1;
return true;
}
return false;
}
return true;
}
}
這個方案就是有點費內(nèi)存,但是性能那是真的好,比redis緩存快好幾倍
這種怎么說呢,就是有點耗費內(nèi)存,實際如果redis和webman 通信 內(nèi)網(wǎng) 基本還是redis bitmap比較好一個512M 的 key就夠用了 查詢是 (0)1,5000qps應(yīng)該問題不大
問題是 你每個進(jìn)程 都得存全量數(shù)據(jù)了 100多萬呢,10個進(jìn)程 就相當(dāng)于內(nèi)存中存了1000萬數(shù)據(jù)呀,內(nèi)存估計占用很大
其實就是文件緩存,但是文件是直接寫在內(nèi)存里,dev/shm你可以當(dāng)成RamDisk,因為是單機應(yīng)用,所以也不在乎
主打一個簡單...
讓官方的AI幫忙寫了個php版本的bitmap,完美解決內(nèi)存占用問題,現(xiàn)在每個進(jìn)程占用內(nèi)存28M+(相比主進(jìn)程就多了5M左右),可緩存1000萬訂單id。webman真是牛逼,webman的AI也牛逼...
代碼,各位參考下
<?php
namespace app\controller;
use support\Db;
use support\Request;
class IndexController
{
protected static $bitmap;
public function index(Request $request)
{
$id = $request->get('id');
return json(['result' => static::get($id)]);
}
protected static function get($id)
{
if (!static::$bitmap) {
static::$bitmap = new Bitmap(10000000);
ini_set('memory_limit', '512M');
$data = Db::table('orders')->pluck('id');
foreach ($data as $order_id) {
static::$bitmap->set($order_id, 1);
}
}
return static::$bitmap->get($id);
}
}
class Bitmap {
protected $bitmap;
public function __construct($num) {
$this->bitmap = str_repeat("\x00", ceil($num / 8));
}
public function set($num, $value) {
$byteIndex = intval(($num - 1) / 8);
$bitIndex = ($num - 1) % 8;
$byte = ord($this->bitmap[$byteIndex]);
if ($value) {
$byte |= (1 << $bitIndex);
} else {
$byte &= ~(1 << $bitIndex);
}
$this->bitmap[$byteIndex] = chr($byte);
}
public function get($num) {
$byteIndex = intval(($num - 1) / 8);
$bitIndex = ($num - 1) % 8;
$byte = ord($this->bitmap[$byteIndex]);
return (($byte >> $bitIndex) & 1) == 1;
}
}
注意,使用BloomFilter擴展需要先安裝該擴展。你可以使用pecl命令來安裝擴展,例如pecl install BloomFilter。安裝完成后,你可以在PHP代碼中使用BloomFilter類。
需要注意的是,布隆過濾器是一個概率型數(shù)據(jù)結(jié)構(gòu),因此在判斷元素是否存在時,可能會出現(xiàn)一定的誤判率。誤判率取決于過濾器的大小和哈希函數(shù)的數(shù)量。在使用布隆過濾器時,需要根據(jù)實際情況來選擇適當(dāng)?shù)倪^濾器大小和哈希函數(shù)數(shù)量,以平衡誤判率和空間效率的要求。
這玩意有誤差啊
哦哦,方案可以 就是重啟 還得重新寫 ,寫一個系統(tǒng)重新啟動 ,直接把數(shù)據(jù)全寫進(jìn)去,就完美了,還有哪個查表 可以用省內(nèi)存方式的那種寫法
@longlong
有點沒看明白 上面@six 的方案 是否可以寫成一個公用的class ,然后再各個控制器方法中調(diào)用?
還是說必須放在Controller 里面 protected static function get($id) 定義后調(diào)用
// 初始化緩存
if (!static::$data) {
ini_set('memory_limit', '512M');
static::$data = array_flip(Db::table('orders')->pluck('id')->toArray());
}
其中 Db::table('orders')->pluck('id')->toArray() 假設(shè)orders 表有100萬條數(shù)據(jù),這個語句應(yīng)該要耗時挺久吧?我用think 感覺不利索呢
用webman V5的協(xié)程+redis 輕輕松松幾十萬QPS
static public function httpRequest(string $url, string $method = 'GET', array $headers = [], array $data = [], bool $log = false) : string
{
$start_time = microtime(true);
$options = [
'max_conn_per_addr' => 128, // 每個域名最多維持多少并發(fā)連接
'keepalive_timeout' => 15, // 連接多長時間不通訊就關(guān)閉
'connect_timeout' => 30, // 連接超時時間
'timeout' => 30, // 請求發(fā)出后等待響應(yīng)的超時時間
];
$http = new Client($options);
//$http = \support\Container::get('Workerman\Http\Client');
$postData = in_array('application/json',$headers)?json_encode($data):$data;
$response = $http->request($url, [
'method' => $method,
'version' => '1.1',
'headers' => $headers,
'data' => $postData,
]);
$end_time = microtime(true);
$res = $response->getBody()->getContents();
$logStr = json_encode(['url' => $url,'method' => $method,'response' => json_decode($res),'time' => ($end_time - $start_time) . 's']);
if($log) {
//第三方請求日志
Log::channel(self::$httplog)->info($logStr,$data);
}
return $res;
}
協(xié)程請求第三方接口的代碼
前提是 composer "workerman/workerman": "v5.0.0-beta.5",
"revolt/event-loop": "1.0.1",
"workerman/http-client": "2.0.1",
這三個包
我?guī)湍忝阑?/p>
static public function httpRequest(string $url, string $method = 'GET', array $headers = [], array $data = [], bool $log = false) : string
{
$start_time = microtime(true);
$options = [
'max_conn_per_addr' => 128, // 每個域名最多維持多少并發(fā)連接
'keepalive_timeout' => 15, // 連接多長時間不通訊就關(guān)閉
'connect_timeout' => 30, // 連接超時時間
'timeout' => 30, // 請求發(fā)出后等待響應(yīng)的超時時間
];
$http = new Client($options);
//$http = \support\Container::get('Workerman\Http\Client');
$postData = in_array('application/json',$headers)?json_encode($data):$data;
$response = $http->request($url, [
'method' => $method,
'version' => '1.1',
'headers' => $headers,
'data' => $postData,
]);
$end_time = microtime(true);
$res = $response->getBody()->getContents();
$logStr = json_encode(['url' => $url,'method' => $method,'response' => json_decode($res),'time' => ($end_time - $start_time) . 's']);
if($log) {
//第三方請求日志
Log::channel(self::$httplog)->info($logStr,$data);
}
return $res;
}
老哥這個注釋了,是復(fù)用Client對象會出問題嗎?//$http = \support\Container::get('Workerman\Http\Client');
/*
//生成100萬個文件
$start = microtime(true);
echo "wait for file creation... \r\n";
for($i = 1; $i < 1000000; $i++){
file_put_contents('md5/' . md5($i), '');
}
$all_time = round((microtime(true) - $start) * 1000, 2);
echo $all_time . "all run time: $all_time ms \r\n";
//生成結(jié)束
*/
$start = microtime(true);
$query_num = 50000;
$s = md5(rand(1, 1000000));
for($i = 1; $i < $query_num; $i++){
//$s = md5(rand(1, 1000000)); //放在這里隨機查找,會慢一些
$check_file = 'md5/' . $s;
//$rs =file_get_contents($check_file);
$rs = file_exists($check_file);
}
//總計運行時間(ms)
$all_time = round((microtime(true) - $start) * 1000, 2);
//單次運行時間(ms)
$single_time = round($all_time / $query_num, 4);
//1秒鐘可以檢測次數(shù)
$one_second_check_num = floor(1000 / $single_time);
echo "query num: $query_num \r\n";
echo "all run time: $all_time ms \r\n";
echo "single run time: $single_time ms \r\n";
echo "one second check num: $one_second_check_num\r\n";
先建立一個md5文件夾,把注釋打開生成100萬條訂單數(shù)據(jù)
然后運行測試代碼 直接用文件檢測
單訂單號檢測和隨機訂單號檢測速度差距還是挺大的
但是可以滿足樓主的需求
Linux服務(wù)器上實測也差不多
[bitmap容量*(子進(jìn)程數(shù)-1)]
的內(nèi)存。使用共享內(nèi)存保存bitmap,既節(jié)省內(nèi)存,又無IO。
https://www.php.net/manual/zh/book.shmop.php
何來跟tp的cache一樣?shmop函數(shù)的唯一弊端是最小讀寫單元是8位,寫入時可能需要加鎖,但是鎖字節(jié)即可,粒度很小,按樓主所言每日增加幾百條,即使使用文件鎖也毫無性能問題。
求教
要怎么初始化 才可以在所有進(jìn)程的控制器方法中使用? 包括 redis 消費中 可以調(diào)用判斷是否存在
參考文檔http://wtbis.cn/doc/webman/others/bootstrap.html
初始化定義
public static function start($worker)
{
// Is it console environment ?
$is_console = !$worker;
if ($is_console) {
// If you do not want to execute this in console, just return.
// return;
}
$userService = Container::get(UserService::class);
$userService::$data = ['aaa', 'bbb', 'ccc'];
}
讀取初始化變量
public function json(Request $request,UserService $userService)
{
return json(['code' => 0, 'msg' => 'ok', 'data' => $userService::$data]);
}
定義靜態(tài)變量
class UserService
{
// 緩存的數(shù)據(jù)
public static array $data = [];
}
測試結(jié)果
mac-mini ~ %
mac-mini ~ % curl http://127.0.0.1:8686/index/json
{"code":0,"msg":"ok","data":["aaa","bbb","ccc"]}%
mac-mini ~ %
我有個問題哈,這種寫法的話,對于新增或者刪除的數(shù)據(jù),無法跨進(jìn)程啊。 比如http開了8個進(jìn)程處理訂單號,自定義進(jìn)程4個用于處理新增/刪除的數(shù)據(jù),這種如何更新8個http進(jìn)程中的內(nèi)存數(shù)據(jù)呢。