標籤:
10.7 Imagine a web server for a simplified search engine. This system has 100 machines to respond to search queries, which may then call out using processSearch(string query) to another cluster of machines to actually get the result. The machine which responds to a given query is chosen at random, so you can not guarantee that the same machine will always respond to the same request. The method processSearch is very expensive. Design a caching mechanism for the most recent queries. Be sure to explain how you would update the cache when data changes.
這道題說假設有一個簡單搜尋引擎的網路伺服器,系統共有100個機子來響應檢索,可以用processSearch(string query)來得到其他機子上的結果,每台機子響應檢索是隨機的,不保證每個機子都會響應到同一個請求。processSearch方法非常昂貴,設計一個緩衝機制來應對近期檢索。根據書中描述,我們先來做一些假設:
1. 與其說根據需要調用processSearch,倒不如設定所有的檢索處理髮生在第一個被調用的機子上。
2. 我們需要緩衝的檢索是非常大量的。
3. 機器之間的調用很快。
4. 檢索的結果是一個有序的URL鏈表,每個URL由50個字元的標題和200個字元的概要組成。
5. 最常訪問的檢索會一直出現的緩衝器中。
系統需求:
主要需要實現下列兩個功能:
1. 高效尋找當給定了一個關鍵字時
2. 新資料會代替舊資料的位置
我們還需要更新和清楚緩衝當搜尋結果改變了。由於一些檢索非常的常見病永久的在緩衝器中,我們不能等緩衝器自然失效。
步驟一:設計單個系統的存存器
我們可以混合使用鏈表和雜湊表來實現,我們建立一個鏈表,當某個節點被訪問了,自動將其移到開頭,這樣鏈表的末尾就是最老的資料。我們用雜湊表來建立檢索和鏈表中節點的映射,這樣不僅可以讓我們高效的返回緩衝的結果,而且可以把節點移到鏈表前段,參見代碼如下:
class Node {public: Node *pre; Node *next; vector<string> results; string query; Node(string q, vector<string> res) { results = res; query = q; }};class Cache {public: const static int MAX_SIZE = 10; Node *head, *tail; unordered_map<string, Node*> m; int size = 0; Cache() {} void moveToFront(Node *node) { if (node == head) return; removeFromLinkedList(node); node->next = head; if (head != nullptr) { head->pre = node; } head = node; ++size; if (tail == nullptr) { tail = node; } } void moveToFront(string query) { if (m.find(query) == m.end()) return; moveToFront(m[query]); } void removeFromLinkedList(Node *node) { if (node == nullptr) return; if (node->next != nullptr || node->pre != nullptr) { --size; } Node *pre = node->pre; if (pre != nullptr) { pre->next = node->next; } Node *next = node->next; if (next != nullptr) { next->pre = pre; } if (node == head) { head = next; } if (node == tail) { tail = pre; } node->next = nullptr; node->pre = nullptr; } vector<string> getResults(string query) { if (m.find(query) == m.end()) return vector<string>(); Node *node = m[query]; moveToFront(node); return node->results; } void insertResults(string query, vector<string> results) { if (m.find(query) != m.end()) { Node *node = m[query]; node->results = results; moveToFront(node); return; } Node *node = new Node(query, results); moveToFront(node); m[query] = node; if (size > MAX_SIZE) { for (unordered_map<string, Node*>::iterator it = m.begin(); it != m.end(); ++it) { if (it->first == tail->query) m.erase(it); } removeFromLinkedList(tail); } }};
步驟二: 擴充到多個機子
對於多個機子,我們有許多中選擇:
選擇1:每個機子有自己的緩衝器,這種方法的好處是快速,因為沒有機子間的調用,但是缺點是不高效
選擇2:每個機子都有緩衝器的拷貝,當新項目添加到緩衝器,發送給所有的機器,設計目的是為了讓常用檢索存在於所有機子上,缺點是緩衝器的空間有限,無法儲存大量資料
選擇3:每個機器儲存緩衝器的一部分,當機器i需要得到一個檢索的結果,它需要找出哪個機子有這個結果,併到那個機子上取結果。但是問題是機子i怎麼知道那個機子上有結果,我們可以用雜湊表來建立映射,hash(query)%N,能快速知道哪個機子有想要的結果。
步驟三:更新結果當內容改變
有些檢索很常用,所以會永遠存在緩衝器中,我們需要一些機制能更新結果,當其內容改變了,緩衝器中的結果頁應該相應變換,主要有下列三種情況:
1. URL的內容改變了
2. 當網頁的排行改變了,那麼結果的順序也變了
3. 對於特定的檢索有了新的頁面
對於1和2,我們建立單獨的雜湊表告訴我們哪一個檢索和哪個URL之間有映射,這個可以在不同的機子上分別完成,但是可能會需要很多資料。另外,如果資料不需要即時重新整理,我們也可以周期性的來更新緩衝器。對於3,我們可以實現一個自動逾時機制,我們設定一個時間段,如果在這個時間段裡沒有檢索,不管它之間被檢索的有多頻繁,我們都清除它,這樣保證了所有資料都會被周期性的更新。
步驟四:進一步地增強
一個最佳化是當某個檢索特別頻繁時,比如一個檢索佔了1%的比重時,我們與其讓機器i發送請求給機器j,倒不如在機器i上將結果存在自己的緩衝器中。
再有就是我們將檢索根據雜湊值分別不同機器,而不是隨機分配。
另外一個最佳化就是之前提到的自動逾時Automatic Time Out機制,就是x分鐘後自動清除資料,但是有時候我們對不同的資料希望設定不同的x值,這樣每一個URL都有一個逾時值基於此頁面過去被更新的頻率。
[CareerCup] 10.7 Simplified Search Engine 簡單的搜尋引擎