繼這篇Blog 解決Nginx和Fpm-Php等內部多進程之間共享數據問題 發完後,進程間共享內存又遇到了新的問題 php
昨天晚上QP同窗上線後,早上看超時報表發現有一臺前端機器訪問QP超時,比其餘前端機器高出了幾個數量級,前端的機器都是同構的 html
難道是這臺機器系統不正常?查看系統狀態也沒有任何異常,統計了一下超時日誌,發現超時都發生在早上QP服務重啓的過程當中,正常狀況下服務重啓時,ClusterMap 會保證流量的正常分配 前端
難道是ClusterMap有問題?去ClusterMap Server端看了一下,一切正常 nginx
難道是訂閱者客戶端有問題嗎?隨便找了一臺正常的機器和有問題的這臺機器對比,查看下日誌也沒有發現問題,使用查詢工具檢查這兩臺機器訂閱者代理寫的共享內存,發現工具讀取共享內存返回的結果不一致,這就更奇怪了,都是相同的訂閱者,一臺機器有問題一臺沒問題 函數
難道Server端給他們的消息不一致?去Server端把訂閱者的機器列表都打了出來,發現了有問題的機器根本不在訂閱者列表裏面,說明這臺機器沒有訂閱,貌似有點線索了,我下線了一臺它訂閱的QP機器驗證,發現共享內部數據沒有更新,pstack一下這個進程,發現內部的更新線程一直在等鎖,致使共享內存數據一直沒法更新,gdb跟進去以後,_lock.data.nr_readers一直爲1,說明一直有一個讀進程佔着鎖致使寫進程沒法進入,遍歷了全部fpm-php的讀進程發現都沒有佔着鎖,這說明在讀進程在得到鎖後沒來得及釋放就掛掉了 工具
如今問題已經確認就是得到讀鎖後進程異常退出致使的,我寫個測試程序復現這個問題 性能
(! 2293)-> cat test/read_shared.cpp 測試
#include SharedUpdateData* _sharedUpdateData = NULL; cm_sub::CMMapFile* _mmapFile = NULL; int32_t initSharedMemRead(const std::string& mmap_file_path) { _mmapFile = new (std::nothrow) cm_sub::CMMapFile(); if (_mmapFile == NULL || !_mmapFile->open(mmap_file_path.c_str(), FILE_OPEN_WRITE) ) { return -1; } _sharedUpdateData = (SharedUpdateData*)_mmapFile->offset2Addr(0); return 0; } int main(int argc, char** argv) { if (initSharedMemRead(argv[1]) != 0) return -1; int cnt = 100; while (cnt > 0) { pthread_rwlock_rdlock( &(_sharedUpdateData->_lock)); fprintf(stdout, "version = %ld, readers = %u\n", _sharedUpdateData->_version, _sharedUpdateData->_lock.__data.__nr_readers); if (cnt == 190) { exit(0); } sleep(1); pthread_rwlock_unlock( &(_sharedUpdateData->_lock)); -- cnt; usleep(100*1000); } delete _mmapFile; }
(! 2293)-> cat test/write_shared.cpp ui
#include SharedUpdateData* _sharedUpdateData = NULL; cm_sub::CMMapFile* _mmapFile = NULL; int32_t initSharedMemWrite(const char* mmap_file_path) { _mmapFile = new (std::nothrow) cm_sub::CMMapFile(); if ( _mmapFile == NULL || !_mmapFile->open(mmap_file_path, FILE_OPEN_WRITE, 1024) ) { return -1; } _sharedUpdateData = (SharedUpdateData *)_mmapFile->offset2Addr(0); madvise(_sharedUpdateData, 1024, MADV_SEQUENTIAL); pthread_rwlockattr_t attr; memset(&attr, 0x0, sizeof(pthread_rwlockattr_t)); if (pthread_rwlockattr_init(&attr) != 0 || pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_SHARED) != 0) { return -1; } pthread_rwlock_init( &(_sharedUpdateData->_lock), &attr); _sharedUpdateData->_updateTime = autil::TimeUtility::currentTime(); _sharedUpdateData->_version = 0; return 0; } int main() { if (initSharedMemWrite("data.mmap") != 0) return -1; int cnt = 200; while (cnt > 0) { pthread_rwlock_wrlock( &(_sharedUpdateData->_lock)); ++ _sharedUpdateData->_version; fprintf(stdout, "version = %ld, readers = %u\n", _sharedUpdateData->_version, _sharedUpdateData->_lock.__data.__nr_readers); sleep(1); pthread_rwlock_unlock( &(_sharedUpdateData->_lock)); -- cnt; usleep(100*1000); } delete _mmapFile; }
不管是讀進程仍是寫進程,獲取鎖後來不及釋放就掛掉都會有這樣的問題 this
問題已經復現,想一想如何用一個好的辦法解決,在網上找了一遍,針對讀寫鎖沒有什麼好的解決辦法,只能在邏輯上本身解決,能想到的是使用超時機制,即寫進程內部增長一個超時時間,若是寫進程到了這個時間仍是不能得到鎖,就認爲死鎖,將讀進程的計數減1,這是一個暴力的解決辦法,不解釋了,若是誰有好的解決辦法指導我下
看下讀寫鎖的代碼,讀寫鎖和互斥鎖相比,更適合用在讀多寫少的場景,若是讀進程須要鎖住時間久,就更合適使用讀寫鎖了,個人應該場景是,讀多寫少,讀寫時間都很是短;暫時認爲互斥鎖和讀寫鎖性能差異應該不大,其實讀寫鎖內部一樣使用了互斥鎖,只不過是鎖的時間比較短,鎖住互斥區,進去看下是否有人正在寫,而後就釋放了,
須要注意的是,讀寫鎖默認是寫優先的,也就是說當正在寫,或者進入寫隊列準備寫時,讀鎖都是加不上的,須要等待
好,那咱們看看互斥鎖可否解決咱們的問題,互斥鎖內部有一個屬性叫Robust鎖
設置鎖爲Robust鎖: pthread_mutexattr_setrobust_np
The robustness attribute defines the behavior when the owner of a mutex dies. The value of robustness could be either PTHREAD_MUTEX_ROBUST_NP or PTHREAD_MUTEX_STALLED_NP, which are defined by the header <pthread.h>. The default value of the robustness attribute is PTHREAD_MUTEX_STALLED_NP. When the owner of a mutex with the PTHREAD_MUTEX_STALLED_NP robustness attribute dies, all future calls to pthread_mutex_lock(3C) for this mutex will be blocked from progress in an unspecified manner.
修復非一致的Robust鎖: pthread_mutex_consistent_np
A consistent mutex becomes inconsistent and is unlocked if its owner dies while holding it, or if the process contain- ing the owner of the mutex unmaps the memory containing the mutex or performs one of the exec(2) functions. A subsequent owner of the mutex will acquire the mutex with pthread_mutex_lock(3C), which will return EOWNERDEAD to indicate that the acquired mutex is inconsistent. The pthread_mutex_consistent_np() function should be called while holding the mutex acquired by a previous call to pthread_mutex_lock() that returned EOWNERDEAD. Since the critical section protected by the mutex could have been left in an inconsistent state by the dead owner, the caller should make the mutex consistent only if it is able to make the critical section protected by the mutex con- sistent.
簡單來講就是當發現EOWNERDEAD時,pthread_mutex_consistent_np函數內部會判斷這個互斥鎖是否是Robust鎖,若是是,而且他OwnerDie了,那麼他會把鎖的owner設置成本身的進程ID,這樣這個鎖又能夠恢復可用,很簡單吧
鎖釋放是能夠解決了,可是經過共享內存在進程間共享數據時,還有一點是須要注意的,就是數據的正確性,即完整性,進程共享不一樣與線程,若是是一個進程中的多個線程,那麼進程異常退出了,其餘線程也同時退出了,進程間共享都是獨立的,若是一個寫線程在寫共享數據的過程當中,異常退出,致使寫入的數據不完整,讀進程讀取時就會有讀到不完整數據的問題,其實數據完整性很是好解決,只須要在共享內存中加一個完成標記就行了,鎖住共享區後,寫數據,寫好以後標記爲完成,就能夠了,讀進程在讀取時判斷一下完成標記
測試代碼見:
(! 2295)-> cat test/read_shared_mutex.cpp
#include SharedUpdateData* _sharedUpdateData = NULL; cm_sub::CMMapFile* _mmapFile = NULL; int32_t initSharedMemRead(const std::string& mmap_file_path) { _mmapFile = new (std::nothrow) cm_sub::CMMapFile(); if (_mmapFile == NULL || !_mmapFile->open(mmap_file_path.c_str(), FILE_OPEN_WRITE) ) { return -1; } _sharedUpdateData = (SharedUpdateData*)_mmapFile->offset2Addr(0); return 0; } int main(int argc, char** argv) { if (argc != 2) return -1; if (initSharedMemRead(argv[1]) != 0) return -1; int cnt = 10000; int ret = 0; while (cnt > 0) { ret = pthread_mutex_lock( &(_sharedUpdateData->_lock)); if (ret == EOWNERDEAD) { fprintf(stdout, "%s: version = %ld, lock = %d, %u, %d\n", strerror(ret), _sharedUpdateData->_version, _sharedUpdateData->_lock.__data.__lock, _sharedUpdateData->_lock.__data.__count, _sharedUpdateData->_lock.__data.__owner); ret = pthread_mutex_consistent_np( &(_sharedUpdateData->_lock)); if (ret != 0) { fprintf(stderr, "%s\n", strerror(ret)); pthread_mutex_unlock( &(_sharedUpdateData->_lock)); continue; } } fprintf(stdout, "version = %ld, lock = %d, %u, %d\n", _sharedUpdateData->_version, _sharedUpdateData->_lock.__data.__lock, _sharedUpdateData->_lock.__data.__count, _sharedUpdateData->_lock.__data.__owner); sleep(5); pthread_mutex_unlock( &(_sharedUpdateData->_lock)); usleep(500*1000); -- cnt; } fprintf(stdout, "go on\n"); delete _mmapFile; }
(! 2295)-> cat test/write_shared_mutex.cpp
#include SharedUpdateData* _sharedUpdateData = NULL; cm_sub::CMMapFile* _mmapFile = NULL; int32_t initSharedMemWrite(const char* mmap_file_path) { _mmapFile = new (std::nothrow) cm_sub::CMMapFile(); if ( _mmapFile == NULL || !_mmapFile->open(mmap_file_path, FILE_OPEN_WRITE, 1024) ) { return -1; } _sharedUpdateData = (SharedUpdateData *)_mmapFile->offset2Addr(0); madvise(_sharedUpdateData, 1024, MADV_SEQUENTIAL); pthread_mutexattr_t attr; memset(&attr, 0x0, sizeof(pthread_mutexattr_t)); if (pthread_mutexattr_init(&attr) != 0 || pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED) != 0) { return -1; } if (pthread_mutexattr_setrobust_np(&attr, PTHREAD_MUTEX_ROBUST_NP) != 0) { return -1; } pthread_mutex_init( &(_sharedUpdateData->_lock), &attr); _sharedUpdateData->_version = 0; return 0; } int main() { if (initSharedMemWrite("data.mmap") != 0) return -1; int cnt = 200; int ret = 0; while (cnt > 0) { ret = pthread_mutex_lock( &(_sharedUpdateData->_lock)); if (ret == EOWNERDEAD) { fprintf(stdout, "%s: version = %ld, lock = %d, %u, %d\n", strerror(ret), _sharedUpdateData->_version, _sharedUpdateData->_lock.__data.__lock, _sharedUpdateData->_lock.__data.__count, _sharedUpdateData->_lock.__data.__owner); ret = pthread_mutex_consistent_np( &(_sharedUpdateData->_lock)); if (ret != 0) { fprintf(stderr, "%s\n", strerror(ret)); pthread_mutex_unlock( &(_sharedUpdateData->_lock)); continue; } } ++ _sharedUpdateData->_version; fprintf(stdout, "version = %ld, lock = %d, %u, %d\n", _sharedUpdateData->_version, _sharedUpdateData->_lock.__data.__lock, _sharedUpdateData->_lock.__data.__count, _sharedUpdateData->_lock.__data.__owner); usleep(1000*1000); pthread_mutex_unlock( &(_sharedUpdateData->_lock)); -- cnt; usleep(500*1000); } delete _mmapFile; }
BTW:咱們都知道加鎖是有開銷的,不只僅是互斥致使的等待開銷,還有加鎖過程都是有系統調用到內核態的,這個過程開銷也很大,有一種互斥鎖叫Futex鎖(Fast User Mutex),Linux從2.5.7版本開始支持Futex,快速的用戶層面的互斥鎖,Fetux鎖有更好的性能,是用戶態和內核態混合使用的同步機制,若是沒有鎖競爭的時候,在用戶態就能夠判斷返回,不須要系統調用,
固然任何鎖都是有開銷的,能不用盡可能不用,使用雙Buffer,釋放鏈表,引用計數,均可以在必定程度上替代鎖的使用