轉載 Memcached BinaryProtocol incr指令內存泄露的bug

緣起

最近有個分佈式限速的需求。支付寶的接口雙11只容許每秒調用10次。html

單機的限速,天然是用google guava的RateLimiter。java

http://docs.guava-libraries.googlecode.com/git-history/master/javadoc/com/google/common/util/concurrent/RateLimiter.htmlgit

分佈式的ReteLimiter,貌似沒有如今的實現方案。不過用memcached或者Redis來實現一個簡單的也很快。github

好比上面的要求,每秒鐘只容許調用10次,則按下面的流程來執行,以memcached爲例:shell

 

[plain]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. incr alipay_ratelimiter  1 1  
  2. 若是返回NOT_FOUND,則  
  3.   add alipay_ratelimiter  0  1 1  
  4.   1  
  5.   即若是alipay_ratelimiter不存在,則設置alipay_ratelimiter的值爲1,過時時間爲1秒。  
  6. 若是incr返回不是具體的數值,則判斷是否大於10,  
  7. 若是大於10則要sleep等待。  

 

上面是Memcached 文本協議的作法。由於文本協議不容許incr 設置不存在的key。ubuntu

若是是二進制協議,則能夠直接用incr命令設置初始值,過時時間。服務器

 

memcached二進制協議的bug

上面扯遠了,下面來講下memcached incr指令的bug。tcp

在測試的時間,用XMemcached作客戶端,來測試,發現有的時候,incr函數返回兩個1。分佈式

因而,在命令行,用telnet來測試,結果發現有時候返回很奇怪的數據:ide

 

[plain]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. get alipay_ratelimiter  
  2. VALUE alipay_ratelimiter 0 22  
  3. END2446744073709551608  


明顯END後面跟了一些很奇怪的數據。並且返回數據的長度是22,而正確的長度應該是1。

 

正常的返回應該是這樣的:

 

[plain]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. get alipay_ratelimiter  
  2. VALUE alipay_ratelimiter 0 4  
  3. 1  
  4. END  

 

 

抓包分析

 

開始覺得是XMemcached客戶端的bug,也有多是序列化方式有問題。因而調試了下代碼,沒發現什麼可疑的地方。

因而祭出wireshakr來抓包。發現XMemcached發出來的數據包是正常的。並且服務器的確返回了22字節的數據。

那麼這個多是Memcached自己的bug了。這個使人比較驚奇,由於Memcached自己已經開發多年,很穩定了,怎麼會有這麼明顯的bug?

查找有問題的Memcached的版本

檢查下當前的Memcahcd版本,是memcached 1.4.14。

因而去下載了最新的1.4.21版,編繹安裝以後,再次測試。發現正常了。

因而到release log裏查看是哪一個版本修復了:

https://code.google.com/p/memcached/wiki/ReleaseNotes

發現1417版的release note裏有incr相關的信息:

https://code.google.com/p/memcached/wiki/ReleaseNotes1417

Fix for incorrect length of initial value set via binary increment protocol.

查找bug發生的緣由:

因而再到github上查看修改了哪些內容:

https://github.com/memcached/memcached/commit/8818bb698ea0abd5199b2792964bbc7fbe4cd845?diff=split

對比下修改內容,和查看下源代碼,能夠發現,實際上是開發人員在爲incr指令存儲的數據分配內存時,沒有注意邊界,一會兒分配了INCR_MAX_STORAGE_LEN,即24字節的內存,卻沒有正常地設置'\r\n'到真實數據的最後。因此當Get請求拿到數據是,會把22字節 + "\r\n"的數據返回給用戶,形成了內存數據泄露。

修復前的代碼:

 

[cpp]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. it = item_alloc(key, nkey, 0, realtime(req->message.body.expiration),  
  2.                 INCR_MAX_STORAGE_LEN);  
  3.   
  4. if (it != NULL) {  
  5.     snprintf(ITEM_data(it), INCR_MAX_STORAGE_LEN, "%llu",  
  6.              (unsigned long long)req->message.body.initial);  



 

修復後的代碼:

 

[cpp]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. snprintf(tmpbuf, INCR_MAX_STORAGE_LEN, "%llu",  
  2.     (unsigned long long)req->message.body.initial);  
  3. int res = strlen(tmpbuf);  
  4. it = item_alloc(key, nkey, 0, realtime(req->message.body.expiration),  
  5.                 res + 2);  
  6.   
  7. if (it != NULL) {  
  8.     memcpy(ITEM_data(it), tmpbuf, res);  
  9.     memcpy(ITEM_data(it) + res, "\r\n", 2);  



 

爲何這個bug隱藏了這麼久

從測試的版本能夠看到,至少從12年這個bug就存在了,從github上的代碼來看,09年以前就存在了。直到13年12月才被修復。

爲何這個bug隱藏了這個久?多是由於返回的22字節數據裏中間有正確的加了\r\n,後面的纔是多餘的泄露數據,可能大部分解析庫都以"\r\n"爲分隔,從而跳過了解析到的多餘的數據。好比XMemcached就能解析到。

另外,只有混用二進制協議和文本協議纔可能會發現。

估計有很多服務器上運行的memcached版本都是比1.4.17要老的,好比ubuntu14默認的就是1.4.14。

這個bug的危害

不過這個bug的危害比較小,由於泄露的只有20個字節的數據。對於一些雲服務指供的cache服務,即便後面是memcached作支持,也會有一箇中轉的路由。

竊取到有效信息的可能性很小。

 

其它的一些東東

wireshark設置解析memcached協議:

wireshark默認是支持解析memcached文本和二進制協議的,不過默認解析端口是11211,因此若是想要解析其它端口的包,要設置下。

在"Edit", 」Preferences「 裏,找到Memcached協議,就能夠看到端口的配置了。

參考:https://ask.wireshark.org/questions/24495/memcache-and-tcp 

XMemcached的文本協議incr指令的實現:

上面說到Memcached的文本協議是不支持incr設置不存在的key的,可是XMemcached卻提供了相關的函數,並且能正常運行,是爲何呢?

 

[java]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. /** 
  2.  * "incr" are used to change data for some item in-place, incrementing it. 
  3.  * The data for the item is treated as decimal representation of a 64-bit 
  4.  * unsigned integer. If the current data value does not conform to such a 
  5.  * representation, the commands behave as if the value were 0. Also, the 
  6.  * item must already exist for incr to work; these commands won't pretend 
  7.  * that a non-existent key exists with value 0; instead, it will fail.This 
  8.  * method doesn't wait for reply. 
  9.  *  
  10.  * @param key 
  11.  *            key 
  12.  * @param delta 
  13.  *            increment delta 
  14.  * @param initValue 
  15.  *            the initial value to be added when value is not found 
  16.  * @param timeout 
  17.  *            operation timeout 
  18.  * @param exp 
  19.  *            the initial vlaue expire time, in seconds. Can be up to 30 
  20.  *            days. After 30 days, is treated as a unix timestamp of an 
  21.  *            exact date. 
  22.  * @return 
  23.  * @throws TimeoutException 
  24.  * @throws InterruptedException 
  25.  * @throws MemcachedException 
  26.  */  
  27. long incr(String key, long delta, long initValue, long timeout, int exp)  
  28.         throws TimeoutException, InterruptedException, MemcachedException;  



 

實際上,XMemcached內部包裝了incr和add指令,代表上是調用了incr但其實是兩條指令:

 

[plain]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. incr alipay_ratelimiter 1  
  2. NOT_FOUND  
  3. add alipay_ratelimiter 0 0 1  
  4. 1  
  5. STORED  

 

另外,要注意XMemcached默認是文本協議的,只有手動配置,纔會使用二進制協議。

Memcached二進制協議比文本協議要快多少?

據這個演示的結果,二進制協議比文本協議要略快,第14頁:

http://www.slideshare.net/tmaesaka/memcached-binary-protocol-in-a-nutshell-presentation/

 

參考:

https://code.google.com/p/memcached/wiki/MemcacheBinaryProtocol 

二進制協議介紹的ppt:
http://www.slideshare.net/tmaesaka/memcached-binary-protocol-in-a-nutshell-presentation/

 

轉載:http://blog.csdn.net/hengyunabc/article/details/40897421

相關文章
相關標籤/搜索