PHP實現Bitmap的探索 - GMP擴展使用

原文地址:https://blog.fanscore.cn/p/22/

一、背景

公司當前有一個用戶羣的系統,核心功能是根據不同的條件組去不同的業務線中get符合條件的uid列表,然後存到redis中的bitmap中。

舉個🌰,如果一個用戶羣中有兩個用戶: 3和7,即[3,7],用bitmap表示那就是:00010001

最後利用redis提供的bitOp命令: bitOp AND \ bitOp XOR \ bitOp OR對各個條件組對應的uid列表bitmap做交併差集計算,得出最終的用戶羣並存儲到redis bitmap中。

二、問題

對於上面描述的系統,如果用戶羣人數較多的那我們就需要執行較多次的setBit {uid} 1命令,而且如果用戶羣中的第一個uid是一個特別大的值比如10億的話,就可能會一次malloc 1000000000/1024/1024/8 ~= 120M的內存,這可能會導致redis卡住一段時間,在高併發的redis實例上執行這個操作是相當危險的。而且可以預想到對於兩個較大的bitmap key執行bitOp也是非常消耗CPU的,應該儘量避免在存儲型的redis實例中做這種十分消耗CPU的計算操作。

三、解決方案

針對上述的問題,可以將bitmap的計算挪到應用程序中來,只將最終統計出來的bitmap存儲到redis中即可。
  如果最終結果用戶羣中的第一個uid是一個特別大的值的話,可以先set 1K再設置2K..3K...這樣緩存的增加bitmap的大小避免redis卡住。

四、PHP實現Bitmap

由於該系統目前是使用的PHP,所以下面記錄下PHP實現Bitmap的”心路歷程“。

由於要操作PHP變量的某一位,所以就要藉助位運算來實現,但是又由於PHP的位運算只能作用在整型數上,所以我們無法使用字符串或者浮點數來實現,所以最先考慮的就是使用整型數組來實現。

爲什麼是數組呢?因爲在64位機器上一個整型變量最多隻能使用64位,又由於PHP的整型是有符號的,所以最高位無法供我們使用,所以一個整型變量能存儲的最大的uid就是63,這真是太雞肋了-_-||,所以只能搞個用多個整型變量了實現了。

OK,到此爲止貌似找到一個看起來不錯的解決方案。但是我們再思考這樣一個問題:假設我們系統中最大的uid是63x100萬=3.6千萬(對主流互聯網公司來說這很正常吧😸),那爲了存儲所有uid,我們需要1百萬個整數纔行,即我們需要一個擁有1百萬個元素的數組,那麼如果我在進程中製造了一個這樣的數組會佔用多少內存呢?會是64 * 1百萬 / 1024 / 1024 / 8 ~= 7.6M嗎?答案是否定的,因爲php數組是由HashTable實現的,這是一個複雜的結構體,除了數組元素佔用的內存外,還有其他的佔用。(這裏先不做展開,有興趣可以自行查看下php數組的實現)
眼見爲實:

<?php
ini_set('memory_limit','4G');
$arr = [];
for ($i = 0; $i < 64 * 1000000; $i++)
{
    $arr[] = PHP_INT_MAX;
}

echo "done\n";
while(1){
}

查看內存佔用

image.png

可以看到大概是1.5G,比我們上面預計的大的多,這太可怕了,必須優化下我們的內存佔用,才能真正在生產環境中使用。

這裏需要提一句,我的機器只有8G,所以程序可能會用到swap分區,而ps命令結果中的RSS不統計swap分區的佔用,在我實際實現中發現ps結果中RSS一列顯示佔用的內存會隨着時間慢慢減少,但是我的程序中arr變量佔用的內存是不可能被回收的,所以推測是物理內存中佔用的部分內存被置換到了swap分區中。如果你要進行這個實驗的話建議關閉swap分區,這樣你能得到一個更準確的結果。

五、繼續優化

基於上面的經驗,如果我們要佔用盡可能小的內存,那我們必須能夠操作一段近乎無限長的內存且不能產生其他額外佔用纔可以。幸運的是PHP給我們提供了這樣一個擴展:GMP,這個擴展可以讓我們使用一個任意長度的整數。OK現在我們擁有了獲得一塊連續的內存而不會產生其他額外佔用的手段,再寫一段代碼使用下並驗證下內存佔用情況:

<?php

$gmp = gmp_init(0);
gmp_setbit($gmp, 64 * 1000000, true);
echo "done\n";
while(1){}

image.png

Awesome,這次只使用了15M的內存。更加興奮的是這個擴展提供了諸如:gmp_andgmp_orgmp_xor這樣進行位運算的函數,極大的方便了我們的使用。

到此爲止我們似乎找到了一個完美的解決方案,但是真的完美嗎?No!其實還可以再優化一下,想象下如果我們有一個用戶羣,裏面只有一個uid:64000000(表示爲數組的話就是:[64000000]),爲了存儲這個用戶我們需要佔用7.6M內存,而這個用戶羣中僅僅只有一個元素,這真是極大的浪費啊!

爲了優化這個問題可以擁抱上面被我們唾棄的數組😸,一個大的bitmap拆分爲一個個小bitmap的數組,這一個個小的bitmap我們限制大小爲1Kw位。
image.png

回到上面的問題,如果我們要存儲[64000000]這個用戶羣的話只需要在數組的第6個元素中設置一個little bitmap: 1即可。這樣我們就由一開始的佔用7.6M內存優化爲了佔用1位內存。

OK,到此爲止我們找到一個還不錯的解決方案😸。

後言

爲了在Mac中安裝GMP擴展又耗費了很多時間,當然,這又是另外一個故事了。有時間我會分享Mac中安裝GMP擴展的過程中我遇到的問題。

參考資料

  1. GNU Multiple Precision
  2. Process Memory Management in Linux
  3. 從源碼看 PHP 7 數組的實現
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章