內(nèi)網(wǎng)http接口,高并發(fā),要求響應(yīng)在100ms內(nèi),單機(jī)需要支持5000+ QPS。
請求參數(shù)為訂單ID(數(shù)字int類型),業(yè)務(wù)邏輯為判斷本地?cái)?shù)據(jù)庫中訂單是否存在,mysql大概100萬條記錄。
數(shù)據(jù)庫中訂單會隨時(shí)增加,每天增加幾百條。
服務(wù)器資源有限,越省資源越好。
php-fpm的框架都試過了,opcache全開最高也就200QPS左右,距離5000QPS相距甚遠(yuǎn)。
求一個(gè)高并發(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í)判斷數(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)實(shí),不要指著個(gè)正常的拖拉機(jī)要他跑出F1的速度
給結(jié)論,不想上各類緩存手段就堆硬件
MySQL查詢走PK基本是最快的了,select * from tbl where pk = ?
數(shù)據(jù)庫查詢內(nèi)就幾ms,算上網(wǎng)絡(luò)消耗一來一回就二三十ms
我給你算個(gè)數(shù)據(jù)
FPM要5000qps,意味著1秒內(nèi)要?jiǎng)?chuàng)建/銷毀5000MySQL連接,問題在哪看到了么
直接查mysql 扛不住呀,redis 是最好的處理方案 bitmap 你可以嘗試一下,這個(gè)理論 最省redis內(nèi)存,然后入庫 還是用隊(duì)列最好,webman進(jìn)程多開一切,理論 5000qps 沒啥問題 ,最好是 webman 和 redis 在一個(gè)機(jī)器 他們直接通信基本能做3ms以內(nèi),你可以試試
webman常駐內(nèi)存的,直接走webman內(nèi)存緩存數(shù)據(jù),數(shù)據(jù)不存在時(shí)再走mysql,我們試過,性能無敵,架構(gòu)也不用做什么改動(dòng)。
代碼類似這樣,壓測試下
<?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;
}
}
這個(gè)方案就是有點(diǎn)費(fèi)內(nèi)存,但是性能那是真的好,比redis緩存快好幾倍
這種怎么說呢,就是有點(diǎn)耗費(fèi)內(nèi)存,實(shí)際如果redis和webman 通信 內(nèi)網(wǎng) 基本還是redis bitmap比較好一個(gè)512M 的 key就夠用了 查詢是 (0)1,5000qps應(yīng)該問題不大
問題是 你每個(gè)進(jìn)程 都得存全量數(shù)據(jù)了 100多萬呢,10個(gè)進(jìn)程 就相當(dāng)于內(nèi)存中存了1000萬數(shù)據(jù)呀,內(nèi)存估計(jì)占用很大
其實(shí)就是文件緩存,但是文件是直接寫在內(nèi)存里,dev/shm你可以當(dāng)成RamDisk,因?yàn)槭菃螜C(jī)應(yīng)用,所以也不在乎
主打一個(gè)簡單...
讓官方的AI幫忙寫了個(gè)php版本的bitmap,完美解決內(nèi)存占用問題,現(xiàn)在每個(gè)進(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擴(kuò)展需要先安裝該擴(kuò)展。你可以使用pecl命令來安裝擴(kuò)展,例如pecl install BloomFilter。安裝完成后,你可以在PHP代碼中使用BloomFilter類。
需要注意的是,布隆過濾器是一個(gè)概率型數(shù)據(jù)結(jié)構(gòu),因此在判斷元素是否存在時(shí),可能會出現(xiàn)一定的誤判率。誤判率取決于過濾器的大小和哈希函數(shù)的數(shù)量。在使用布隆過濾器時(shí),需要根據(jù)實(shí)際情況來選擇適當(dāng)?shù)倪^濾器大小和哈希函數(shù)數(shù)量,以平衡誤判率和空間效率的要求。
這玩意有誤差啊
哦哦,方案可以 就是重啟 還得重新寫 ,寫一個(gè)系統(tǒng)重新啟動(dòng) ,直接把數(shù)據(jù)全寫進(jìn)去,就完美了,還有哪個(gè)查表 可以用省內(nèi)存方式的那種寫法
@longlong
有點(diǎn)沒看明白 上面@six 的方案 是否可以寫成一個(gè)公用的class ,然后再各個(gè)控制器方法中調(diào)用?
還是說必須放在Controller 里面 protected static function get($id) 定義后調(diào)用
我覺得@six 的是不是最佳方案也應(yīng)該是在啟動(dòng)項(xiàng)目時(shí),把x表內(nèi)所有數(shù)據(jù)給緩存到內(nèi)存中去?
最好是 啟動(dòng)就把數(shù)據(jù)緩存到內(nèi)存中 ,webman就能實(shí)現(xiàn),里面有一個(gè)隨著 進(jìn)程啟動(dòng)執(zhí)行的類
// 初始化緩存
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ù),這個(gè)語句應(yīng)該要耗時(shí)挺久吧?我用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, // 每個(gè)域名最多維持多少并發(fā)連接
'keepalive_timeout' => 15, // 連接多長時(shí)間不通訊就關(guān)閉
'connect_timeout' => 30, // 連接超時(shí)時(shí)間
'timeout' => 30, // 請求發(fā)出后等待響應(yīng)的超時(shí)時(shí)間
];
$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",
這三個(gè)包
我?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, // 每個(gè)域名最多維持多少并發(fā)連接
'keepalive_timeout' => 15, // 連接多長時(shí)間不通訊就關(guān)閉
'connect_timeout' => 30, // 連接超時(shí)時(shí)間
'timeout' => 30, // 請求發(fā)出后等待響應(yīng)的超時(shí)時(shí)間
];
$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;
}
老哥這個(gè)注釋了,是復(fù)用Client對象會出問題嗎?//$http = \support\Container::get('Workerman\Http\Client');
/*
//生成100萬個(gè)文件
$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)); //放在這里隨機(jī)查找,會慢一些
$check_file = 'md5/' . $s;
//$rs =file_get_contents($check_file);
$rs = file_exists($check_file);
}
//總計(jì)運(yùn)行時(shí)間(ms)
$all_time = round((microtime(true) - $start) * 1000, 2);
//單次運(yùn)行時(shí)間(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";
先建立一個(gè)md5文件夾,把注釋打開生成100萬條訂單數(shù)據(jù)
然后運(yùn)行測試代碼 直接用文件檢測
單訂單號檢測和隨機(jī)訂單號檢測速度差距還是挺大的
但是可以滿足樓主的需求
Linux服務(wù)器上實(shí)測也差不多
[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位,寫入時(shí)可能需要加鎖,但是鎖字節(jié)即可,粒度很小,按樓主所言每日增加幾百條,即使使用文件鎖也毫無性能問題。
求教
要怎么初始化 才可以在所有進(jìn)程的控制器方法中使用? 包括 redis 消費(fèi)中 可以調(diào)用判斷是否存在
參考文檔http://www.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 ~ %
我有個(gè)問題哈,這種寫法的話,對于新增或者刪除的數(shù)據(jù),無法跨進(jìn)程啊。 比如http開了8個(gè)進(jìn)程處理訂單號,自定義進(jìn)程4個(gè)用于處理新增/刪除的數(shù)據(jù),這種如何更新8個(gè)http進(jìn)程中的內(nèi)存數(shù)據(jù)呢。