緣起
最近有個分布式限速的需求。支付寶的接口雙11隻允許每秒調用10次。
單機的限速,自然是用google guava的RateLimiter。
http://docs.guava-libraries.googlecode.com/git-history/master/javadoc/com/google/common/util/concurrent/RateLimiter.html
分布式的ReteLimiter,貌似沒有現在的實作方案。不過用memcached或者Redis來實作一個簡單的也很快。
比如上面的要求,每秒鐘隻允許調用10次,則按下面的流程來執行,以memcached為例:
incr alipay_ratelimiter 1 1
如果傳回NOT_FOUND,則
add alipay_ratelimiter 0 1 1
1
即如果alipay_ratelimiter不存在,則設定alipay_ratelimiter的值為1,過期時間為1秒。
如果incr傳回不是具體的數值,則判斷是否大于10,
如果大于10則要sleep等待。
上面是Memcached 文本協定的做法。因為文本協定不允許incr 設定不存在的key。
如果是二進制協定,則可以直接用incr指令設定初始值,過期時間。
memcached二進制協定的bug
上面扯遠了,下面來說下memcached incr指令的bug。
在測試的時間,用XMemcached做用戶端,來測試,發現有的時候,incr函數傳回兩個1。
于是,在指令行,用telnet來測試,結果發現有時候傳回很奇怪的資料:
get alipay_ratelimiter
VALUE alipay_ratelimiter 0 22
END2446744073709551608
明顯END後面跟了一些很奇怪的資料。而且傳回資料的長度是22,而正确的長度應該是1。
正常的傳回應該是這樣的:
get alipay_ratelimiter
VALUE alipay_ratelimiter 0 4
1
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"的資料傳回給使用者,造成了記憶體資料洩露。
修複前的代碼:
it = item_alloc(key, nkey, 0, realtime(req->message.body.expiration),
INCR_MAX_STORAGE_LEN);
if (it != NULL) {
snprintf(ITEM_data(it), INCR_MAX_STORAGE_LEN, "%llu",
(unsigned long long)req->message.body.initial);
修複後的代碼:
snprintf(tmpbuf, INCR_MAX_STORAGE_LEN, "%llu",
(unsigned long long)req->message.body.initial);
int res = strlen(tmpbuf);
it = item_alloc(key, nkey, 0, realtime(req->message.body.expiration),
res + 2);
if (it != NULL) {
memcpy(ITEM_data(it), tmpbuf, res);
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卻提供了相關的函數,而且能正常運作,是為什麼呢?
/**
* "incr" are used to change data for some item in-place, incrementing it.
* The data for the item is treated as decimal representation of a 64-bit
* unsigned integer. If the current data value does not conform to such a
* representation, the commands behave as if the value were 0. Also, the
* item must already exist for incr to work; these commands won't pretend
* that a non-existent key exists with value 0; instead, it will fail.This
* method doesn't wait for reply.
*
* @param key
* key
* @param delta
* increment delta
* @param initValue
* the initial value to be added when value is not found
* @param timeout
* operation timeout
* @param exp
* the initial vlaue expire time, in seconds. Can be up to 30
* days. After 30 days, is treated as a unix timestamp of an
* exact date.
* @return
* @throws TimeoutException
* @throws InterruptedException
* @throws MemcachedException
*/
long incr(String key, long delta, long initValue, long timeout, int exp)
throws TimeoutException, InterruptedException, MemcachedException;
實際上,XMemcached内部包裝了incr和add指令,表明上是調用了incr但實際上是兩條指令:
incr alipay_ratelimiter 1
NOT_FOUND
add alipay_ratelimiter 0 0 1
1
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: