原文地址: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){
}
查看記憶體占用

可以看到大概是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){}

Awesome,這次只使用了15M的記憶體,更加興奮的是這個擴展提供了諸如:gmp_and、gmp_or、gmp_xor這樣進行位運算的函式,極大的方便了我們的使用,
到此為止我們似乎找到了一個完美的解決方案,但是真的完美嗎?No!其實還可以再優化一下,想象下如果我們有一個用戶群,里面只有一個uid:64000000(表示為陣列的話就是:[64000000]),為了存盤這個用戶我們需要占用7.6M記憶體,而這個用戶群中僅僅只有一個元素,這真是極大的浪費啊!
為了優化這個問題可以擁抱上面被我們唾棄的陣列??,一個大的bitmap拆分為一個個小bitmap的陣列,這一個個小的bitmap我們限制大小為1Kw位,

回到上面的問題,如果我們要存盤[64000000]這個用戶群的話只需要在陣列的第6個元素中設定一個little bitmap: 1即可,這樣我們就由一開始的占用7.6M記憶體優化為了占用1位記憶體,
OK,到此為止我們找到一個還不錯的解決方案??,
后言
為了在Mac中安裝GMP擴展又耗費了很多時間,當然,這又是另外一個故事了,有時間我會分享Mac中安裝GMP擴展的程序中我遇到的問題,
參考資料
- GNU Multiple Precision
- Process Memory Management in Linux
- 從原始碼看 PHP 7 陣列的實作
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/187953.html
標籤:其他
