spymemcached深入分析

本文摘自:http://my.oschina.net/astute/blog/93492, 感謝作者

一、簡介

spymemcached 是一個 memcache 的客戶端, 使用 NIO 實現

分析 spymemcached 需要了解 NIOmemcached使用,memcached協議,參考資料中列出了有用的資源連接。

NIONew I/O的縮寫,Java裏邊大家一般稱爲異步IO,實際上對應Linux系統編程中的事件驅動IOevent-driven IO),是對 epoll 的封裝。其它的IO模型還包括同步,阻塞,非阻塞,多路複用(selectpoll)。阻塞/非阻塞是 fd 的屬性,同步會跟阻塞配合,這樣的應用會一直 sleep,直到IO完成被內核喚醒;同步非阻塞的話,第一次讀取時,如果沒有數據,應用線程會立刻返回,但是應用需要確定以什麼樣的策略進行後面的系統調用,如果是簡單的while循環會導致CPU 100%,複雜的類似自旋的策略增加了應用編程的難度,因此同步非阻塞很少使用。多路複用是Linux早期的一個進程監控多個fd的方式,性能比較低,每次調用涉及3次循環遍歷,具體分析見 http://my.oschina.net/astute/blog/92433 event-driven IO,應用註冊 感興趣的socket IO事件(READ,WRITE),調用wait開始sleep,當條件成立時,如數據到達(可讀),寫緩衝區可用(可寫),內核喚醒應用線程,應用線程根據得到的socket執行同步的調用讀/寫 數據。


二、協議簡介

memcachded服務器和客戶端之間採用 TCP 的方式通信,自定義了一套字節流的格式。本文分析的文本協議的構建和其它文本協議類似,mc裏面分成命令行和數據塊行,命令行裏指明數據塊的字節數目,命令行和數據塊後都跟隨\r\n。重要的一點是服務器在讀取數據塊時是根據命令行裏指定的字節數目,因此數據塊中含有\r\n並不影響服務器讀塊操作。數據塊後必須跟隨\r\n

存儲命令

發送

<command name> <key> <flags> <exptime> <bytes> [noreply]\r\n

cas <key> <flags> <exptime> <bytes> <cas unique> [noreply]\r\n

<data block>\r\n

command name = "set", "add", "replace", "append" or "prepend"

flags - 32位整數 server並不操作這個數據 get時返回給客戶端

exptime - 過期時間,可以是unix時間戳或偏移量,偏移量的話最大爲30*24*60*60, 超過這個值,服務器會認爲是unix時間戳

bytes - 數據塊字節的個數

響應

<data block>\r\n

STORED\r\n - 成功

NOT_STORED\r\n - addreplace命令沒有滿足條件

EXISTS\r\n - cas命令 表明item已經被修改

NOT_FOUND\r\n - cas命令 item不存在

獲取命令

發送

get <key>*\r\n

gets <key>*\r\n

<key>* - 空格分割的一個或多個字符串

響應

VALUE <key> <flags> <bytes> [<cas unique>]\r\n

<data block>\r\n

VALUE <key> <flags> <bytes> [<cas unique>]\r\n

<data block>\r\n

END\r\n

本文以 get 操作爲例;key = someKey  value=abcdABC中文

以字節流的形式最終發送的數據

[103, 101, 116, 32, 115, 111, 109, 101, 75, 101, 121, 13, 10, 0]

103 101 116 - "get"

32 - "" 空格

115 111 109 101 75 101 121 - someKey

13 10 - \r\n

接收到的數據

VALUE someKey 0 13

61 62 63 64 41 42 43 E4 B8 AD E6 96 87\r\n

END\r\n

刪除命令

發送

delete <key> [noreply]\r\n

響應

DELETED\r\n - 成功刪除

NOT_FOUND\r\n - 刪除的條目不存在

其它命令

詳見參考資料 mc 協議

三、spymemcached中的重要對象
簡介

spymc的客戶端,因此spy中所有對象需要基於它要完成的 功能 和 mc服務器的通信協議來進行設計。最重要的MemcachedClient表示mc集羣的client,應用中單例即可。spy中的每一個mc節點,用MemcachedNode表示,這個對象內部含有一個channel,網絡連接到mc節點。要根據key的哈希值查找某個mc節點,spy中使用NodeLocator,默認locatorArrayModNodeLocator,這個對象內部含有所有的MemcachedNodespy使用的hash算法都在對象DefaultHashAlgorithm中,默認使用NATIVE_HASH,也就是String.hashCode()locatorclient中間還有一個對象,叫MemcachedConnection ,它表示到mc集羣的連接,內部持有locatorclent內部持有MemcachedConnection(mconn)spy使用NIO實現,因此有一個selector,這個對象存在於mconn中。要和服務器進行各種操作的通信,協議數據發送,數據解析,spy中抽象爲Operation,文本協議的get操作最終實現爲net.spy.memcached.protocol.ascii.GetOperationImpl。爲了實現工作線程和IO線程之間的調度,spy抽象出了一個 GetFuture,內部持有一個OperationFuture

TranscodeService執行字節數據和對象之間的轉換,spy中實現方式爲任務隊列+線程池,這個對象的實例在client中。

對象詳解

SpyObject - spy中的基類 定義 Logger

MemcachedConnection 表示到多臺 mc 節點的連接

MemcachedConnection 詳細屬性

    shouldOptimize - 是否需要優化多個連續的get操作 --> gets 默認true

    addedQueue - 用來記錄排隊到節點的操作

    selector - 監控到多個 mc 服務器的讀寫事件

    locator - 定位某個 mc 服務器

GetFuture 前端線程和工作線程交互的對象

    --> OperationFuture

ConnectionFactory 創建 MemcachedConnection 實例;創建操作隊列;創建 OperationFactory;制定 Hash 算法。

DefaultConnectionFactory 默認連接工廠

DefaultHashAlgorithm - Hash算法的實現類

MemcachedNode 定義到 單個memcached 服務器的連接

    TCPMemcachedNodeImpl - 

        AsciiMemcachedNodeImpl - 

        BinaryMemcachedNodeImpl - 

TCPMemcachedNodeImpl 重要屬性

    socketAddress - 服務器地址

    rbuf - 讀緩衝區 默認大小 16384

    wbuf - 寫緩衝區 默認大小 16384

    writeQ - 寫隊列

    readQ - 讀隊列

    inputQueue - 輸入隊列 memcachclient添加操作時先添加到 inputQueue

    opQueueMaxBlockTime - 操作的最大阻塞時間 默認10

    reconnectAttempt - 重連嘗試次數 volatile

    channel - socket 通道

    toWrite - 要向socket發送的字節數

    optimizedOp - 優化後的Operation 實現類是OptimizedGetImpl

       sk - channel註冊到selector後的key

    shouldAuth - 是否需要認證 默認 false

    authLatch - 認證需要的Latch

    reconnectBlocked - 

    defaultOpTimeout - 操作默認超時時間 默認值 2.5

    continuousTimeout - 連續超時次數

    opFact - 操作工廠

MemcachedClient 重要屬性

    mconn - MemcachedConnection 

    opFact - 操作工廠

    transcoder - 解碼器

    tcService - 解碼線程池服務

    connFactory - 連接工廠

Operation 所有操作的基本接口

    BaseOperationImpl

        OperationImpl

            BaseGetOpImpl - initialize 協議解析 構建緩衝區

                GetOperationImpl

OperationFactory 爲協議構建操作 比如生成 GetOperation

    BaseOperationFactory

        AsciiOperationFactory - 文本協議的操作工廠 默認的操作工廠

        BinaryOperationFactory - 二進制協議的操作工廠

OperationFactory 根據 protocol handlers 構建操作

    BaseOperationFactory

        AsciiOperationFactory - 支持 ascii protocol

        BinaryOperationFactory - 支持 binary operations

NodeLocator 根據 key hash 值查找節點

    ArrayModNodeLocator - hash 值和節點列表長度取模,作爲下標,簡單的數組查詢

    KetamaNodeLocator - Ketama一致性hash的實現

Transcoder 對象和字節數組之間的轉換接口

    BaseSerializingTranscoder

        SerializingTranscoder - 默認的transcoder

TranscodeService 異步的解碼服務,含有一個線程池

FailureMode - node失效的模式

    Redistribute - 節點失效後移動到下一個有效的節點  默認模式

    Retry - 重試失效節點 直至恢復

    Cancel - 取消操作

四、整體流程
初始化

客戶端執行new MemcachedClient(new InetSocketAddress("192.168.56.101", 11211))。初始化 MemcachedClient,內部初始化MemcachedConnection,創建selector,註冊channelselector,啓動IO線程。

線程模型

初始化完成後,把監聽mc節點事件的線程,也就是調用select的線程,稱爲IO線程;應用執行 c.get("someKey"),把應用所在的線程稱爲工作線程。工作線程通常由tomcat啓動,負責創建操作,加入節點的操作隊列,工作線程通常有多個;IO線程負責從隊列中拿到操作,執行操作。

工作線程

工作線程最終會調用asyncGet,方法內部會創建CountDownLatch(1), GetFutureGetOperationImpl(持有一個內部類,工作線程執行完成後,最終會調用 latch.countDown()),選擇mc節點,操作op初始化(生成寫緩衝區),把op放入節點等待隊列inputQueue中,同時會把當前節點放入mc連接(mconn)addedQueue屬性中,最後喚醒selector。最終工作線程在latch上等待(默認超時2.5秒)IO線程的執行結果。

IO線程

IO線程被喚醒後

1、handleInputQueue()。移動OperationinputQueuewriteQ中。對添加到addedQueue中的每一個MemcachedNode分別進行處理。這個函數會處理所有節點上的所有操作,全部發送到mc服務器(之前節點上就有寫操作的才這麼處理,否則只是註冊寫事件)。

2、循環過程中,如果當前node中沒有寫操作,則判斷writeQreadQ中有操作,在SK上註冊讀/寫事件;如果有寫操作,需要執行handleWrites函數。這個函數內部首先做的是填充緩衝區fillWriteBuffer():從writeQ中取出一個可寫的操作(remove掉取消的和超時的),改變操作的狀態爲WRITING,把操作的數據複製到寫緩衝區(寫緩衝區默認16K,操作的字節數從十幾字節到1M,這個地方有複雜的處理,後面會詳細分析,現在只考慮簡單情況),複製完成後把操作狀態變爲READING,從writeQremove當前操作,把操作addreadQ當中,這個地方會再去複製pending的操作;、發送寫緩衝區的內容,全部發送完成後,會再次去填充緩衝區fillWriteBuffer()(比如說一個大的命令,一個緩衝區不夠)。循環,直到所有的寫操作都處理完。ƒ、判斷writeQreadQ是否有操作,更新sk註冊的讀寫事件。get操作的話,現在已經註冊了讀事件。

3、selector.select()

4、數據到達時,執行handleIO(sk),處理讀事件;執行channel.read(rbuf);執行readFromBuffer(),解析數據,讀取到END\r\n將操作狀態置爲COMPLETE

五、初始化詳細流程

1、默認連接工廠爲 DefaultConnectionFactory。接着創建TranscodeService(解碼的線程池,默認線程最多爲10),創建AsciiOperationFactory(支持ascii協議的操作工廠,負責生成各種操作,比如 GetOperationImpl),創建MemcachedConnection,設置操作超時時間(默認2.5秒)。

2、DefaultConnectionFactory創建MemcachedConnection詳細過程:創建reconnectQueueaddedQueue,設置shouldOptimizetrue,設置maxDelay30秒,設置opFact,設置timeoutExceptionThreshold1000(超過這個值,關閉到 mc node 的連接),打開 Selector,創建nodesToShutdown,設置bufSize16384字節,創建到每個node的 MemcachedNode(默認是AsciiMemcachedNodeImpl,這一步創建SocketChannel,連接到mc節點,註冊到selector,設置sk爲剛註冊得到的SelectionKey),最後啓動 MemcachedConnection 線程,進入事件處理的循環代碼 

while(running) handleIO()

六、核心流程代碼
1、工作線程

一切從工作線程調用 c.get("someKey") 方法開始
基本流程是:創建操作(Operation),操作初始化,查找節點,把操作加入節點的等待隊列,喚醒IO線程,然後工作線程在Future上等待IO線程的執行結果

1 // 默認等待2.5秒
2 return asyncGet(key, tc).get(2500, TimeUnit.MILLISECONDS)
01 // 內部類GetOperation.Callback,是工作線程和IO線程交互的類
02 // IO線程得到所有的操作響應數據後,調用gotData方法
03 // IO線程接收到END\r\n後,調用receivedStatus和complete方法
04 public <T> GetFuture<T> asyncGet(final String key, final Transcoder<T> tc) {
05     final CountDownLatch latch = new CountDownLatch(1);
06     final GetFuture<T> rv = new GetFuture<T>(latch, operationTimeout, key);
07     Operation op = opFact.get(key, new GetOperation.Callback() {
08         private Future<T> val = null;
09         public void receivedStatus(OperationStatus status) {
10             rv.set(val, status);
11         }
12         public void gotData(String k, int flags, byte[] data) {
13             val = tcService.decode(tc, new CachedData(flags, data, tc.getMaxSize()));
14         }
15         public void complete() {
16             latch.countDown();
17         }
18     });
19     rv.setOperation(op);
20     mconn.enqueueOperation(key, op);
21     return rv;  // 最終會在rv上調用get方法
22 }
01 // 向節點中隊列中添加操作 1、查找節點 2、放入隊列
02 // 查找節點,根據key的hash值,對節點數取模
03 protected void addOperation(final String key, final Operation o) {
04     MemcachedNode placeIn = null;
05     MemcachedNode primary = locator.getPrimary(key);
06     if (primary.isActive() || failureMode == FailureMode.Retry) {
07         placeIn = primary;
08     else if (failureMode == FailureMode.Cancel) {
09         o.cancel();
10     else {
11         for (Iterator<MemcachedNode> i = locator.getSequence(key); placeIn == null
12         && i.hasNext();) {
13             MemcachedNode n = i.next();
14             if (n.isActive()) {
15                 placeIn = n;
16             }
17         }
18         if (placeIn == null) {
19             placeIn = primary;
20         }
21     }
22     if (placeIn != null) {
23         addOperation(placeIn, o);
24     else {
25     }
26 }
1 // 最重要的方法
2 protected void addOperation(final MemcachedNode node, final Operation o) {
3     o.setHandlingNode(node);
4     o.initialize();  // 操作初始化,生成要發送的字節流數據,放到緩衝區中
5     node.addOp(o);   // 添加到節點的inputQueue中
6     addedQueue.offer(node);   // 有操作的節點放入 addedQueue中
7     Selector s = selector.wakeup(); // 喚醒IO線程
8 }
工作線程和IO線程之間傳遞的Future對象,結構如下 
GetFuture ---> OperationFuture ---> latch 
---> 表示依賴關係


01 // 最終工作線程在 OperationFuture的get方法上等待latch
02 public T get(long duration, TimeUnit units) {
03     if (!latch.await(duration, units)) { // 等待2.5秒
04         MemcachedConnection.opTimedOut(op);
05         if (op != null) {
06             op.timeOut(); //2.5秒後,操作沒有執行完,設置超時(IO線程會判斷,如果超時,就remove)
07         }
08         // throw exception
09     }
10     return objRef.get(); // objRef是一個原子引用,來保證對象的安全發佈(線程安全)
11 }
12 // objRef引用的是一個TranscodeService.Task(本身是個FutureTask)對象,如果沒有壓縮和序列化的話,最終工作線程會調用tc.decode方法,得到返回值。


2、IO線程

IO線程的操作循環
處理輸入隊列,註冊寫事件;執行寫操作,註冊讀事件;處理讀操作,解析結果。

1 public void run() {
2   while (running) {
3   handleIO();
4   }
5 }
01 public void handleIO() throws IOException {
02     handleInputQueue();
03     int selected = selector.select(delay);
04     Set<SelectionKey> selectedKeys = selector.selectedKeys();
05  
06     if (selectedKeys.isEmpty() && !shutDown) {
07         // some code
08     else {
09         for (SelectionKey sk : selectedKeys) {
10             handleIO(sk);
11         }
12         selectedKeys.clear();
13     }
14 }
01 handleInputQueue
02 處理addedQueue中的所有節點,對每一個節點複製inputQueue中的操作到writeQ中。註冊讀寫事件。
03 private void handleInputQueue() {
04     if (!addedQueue.isEmpty()) {
05         Collection<MemcachedNode> toAdd = new HashSet<MemcachedNode>();
06         Collection<MemcachedNode> todo = new HashSet<MemcachedNode>();
07         MemcachedNode qaNode = null;
08         while ((qaNode = addedQueue.poll()) != null) {
09             todo.add(qaNode);
10         }
11         for (MemcachedNode qa : todo) {
12             boolean readyForIO = false;
13             if (qa.isActive()) {
14                 if (qa.getCurrentWriteOp() != null) {
15                     readyForIO = true;
16                 }
17             else {
18                 toAdd.add(qa);
19             }
20             qa.copyInputQueue();
21             if (readyForIO) {
22                 try {
23                     if (qa.getWbuf().hasRemaining()) {
24                         handleWrites(qa.getSk(), qa);
25                     }
26                 catch (IOException e) {
27                     lostConnection(qa);
28                 }
29             }
30             qa.fixupOps();
31         }
32         addedQueue.addAll(toAdd);
33     }
34 }
01 spy中註冊讀寫事件的函數
02 readQ不爲空註冊讀事件;writeQ不爲空註冊寫事件;網絡沒有連接上註冊連接事件。
03 public final void fixupOps() {
04     SelectionKey s = sk;
05     if (s != null && s.isValid()) {
06         int iops = getSelectionOps();
07         s.interestOps(iops);
08     else {
09     }
10 }
01 public final int getSelectionOps() {
02     int rv = 0;
03     if (getChannel().isConnected()) {
04         if (hasReadOp()) {
05             rv |= SelectionKey.OP_READ;
06         }
07         if (toWrite > 0 || hasWriteOp()) {
08             rv |= SelectionKey.OP_WRITE;
09         }
10     else {
11         rv = SelectionKey.OP_CONNECT;
12     }
13     return rv;
14 }
1 public final boolean hasReadOp() {
2     return !readQ.isEmpty();
3 }
4  
5 public final boolean hasWriteOp() {
6     return !(optimizedOp == null && writeQ.isEmpty());
7 }

3、handleWrites(SelectionKey sk, MemcachedNode qa) 
我能夠想到的一些場景,這個狀態機代碼必須處理的
⑴ 當前隊列中有1個操作,操作要發送的字節數目小於16K
⑵ 當前隊列中有1個操作,操作要發送的字節數目大於16K(很大的set操作)
⑶ 當前隊列中有多個操作,操作要發送的字節數目小於16K
⑷ 當前隊列中有多個操作,操作要發送的字節數目大於16K
⑸ 任意一次寫操作wrote爲0

summary:處理節點中writeQ和inputQueue中的所有操作。每次循環會盡量填滿發送緩衝區,然後將發送緩衝區的內容全部發送到網絡上,循環往復,沒有異常的情況下,直至發送完數據。操作中發送的內容只要放入到發送緩衝區後,就把操作加入到readQ(spy中根據writeQ和readQ中有沒有數據,來註冊讀寫事件)。

執行時機:IO線程在select上休眠,被工作線程喚醒後,處理輸入隊列,把操作複製到writeQ 中,註冊寫事件;再次調用select,返回後,就會調用handleWrites(),數據全部發送後,會註冊讀事件。處理輸入隊列時,如果wbuf還有東西沒有發送,那麼會在select調用前,調用handleWrites函數。

01 private void handleWrites(SelectionKey sk, MemcachedNode qa) throws IOException {
02     qa.fillWriteBuffer(shouldOptimize); --->
03     boolean canWriteMore = qa.getBytesRemainingToWrite() > 0;
04     while (canWriteMore) {
05         int wrote = qa.writeSome(); --->
06         qa.fillWriteBuffer(shouldOptimize);
07         canWriteMore = wrote > 0 && qa.getBytesRemainingToWrite() > 0;
08     }
09 }
10  
11  -- 發送數據;執行一次後,wbuf可能還有數據未寫完
12 public final int writeSome() throws IOException {
13     int wrote = channel.write(wbuf);
14     toWrite -= wrote;
15     return wrote;
16 }
01 -- 填充緩衝區
02 toWrite=0 表明 寫緩衝區以前的內容已經全部寫入到網絡中,這樣纔會進行下一次的填充寫緩衝區
03 操作會盡量填滿16K的緩衝區(單一操作數據量很大比如500K;或多個操作數據量500K)
04 當一個操作中的數據完全寫入緩衝區後,操作的狀態變成READING,從writeQ中移除當前操作。
05 public final void fillWriteBuffer(boolean shouldOptimize) {
06     if (toWrite == 0 && readQ.remainingCapacity() > 0) {
07         getWbuf().clear();
08         Operation o=getNextWritableOp(); --->
09  
10         while(o != null && toWrite < getWbuf().capacity()) {
11             synchronized(o) {
12                 ByteBuffer obuf = o.getBuffer();
13                 int bytesToCopy = Math.min(getWbuf().remaining(), obuf.remaining());
14                 byte[] b = new byte[bytesToCopy];
15                 obuf.get(b);
16                 getWbuf().put(b);
17                 if (!o.getBuffer().hasRemaining()) {
18                     o.writeComplete();
19                     transitionWriteItem();
20                     preparePending(); -- copyInputQueue()
21                     if (shouldOptimize) {
22                         optimize();
23                     }
24                     o=getNextWritableOp();
25                 }
26                 toWrite += bytesToCopy;
27             }
28         }
29         getWbuf().flip();
30     else {
31     }
32 }
01 -- 獲取節點寫隊列中下一個可寫的操作
02 如果操作已經取消(前端線程等待超時,取消操作),或超時(IO線程沒有來得及執行操作,操作超時),那麼把操作從隊列中移除,繼續查找下一個操作。把可寫的操作的狀態從WRITE_QUEUED變成WRITING,同時把操作放入讀隊列中。
03 private Operation getNextWritableOp() {
04     Operation o = getCurrentWriteOp(); --->④
05     while (o != null && o.getState() == OperationState.WRITE_QUEUED) {
06         synchronized(o) {
07             if (o.isCancelled()) {
08                 Operation cancelledOp = removeCurrentWriteOp();--->⑤
09             else if (o.isTimedOut(defaultOpTimeout)) {
10                 Operation timedOutOp = removeCurrentWriteOp();
11             else {
12                 o.writing();
13                 if (!(o instanceof TapAckOperationImpl)) {
14                     readQ.add(o);
15                 }
16                 return o;
17             }
18             o = getCurrentWriteOp();
19         }
20     }
21     return o;
22 }
01 ④ -- 拿到當前寫操作(並不從隊列中移除)
02 public final Operation getCurrentWriteOp() {
03     return optimizedOp == null ? writeQ.peek() : optimizedOp;
04 }
05  
06 ⑤ -- remove當前寫操作
07 public final Operation removeCurrentWriteOp() {
08     Operation rv = optimizedOp;
09     if (rv == null) {
10         rv = writeQ.remove();
11     else {
12         optimizedOp = null;
13     }
14     return rv;
15 }
01 4、handleReads
02 handleReads(SelectionKey sk, MemcachedNode qa)
03 從網絡中讀取數據,放入rbuf。解析rbuf,得到結果;
04 private void handleReads(SelectionKey sk, MemcachedNode qa) throws IOException {
05     Operation currentOp = qa.getCurrentReadOp();
06     if (currentOp instanceof TapAckOperationImpl) { // no response
07         qa.removeCurrentReadOp();
08         return;
09     }
10     ByteBuffer rbuf = qa.getRbuf();
11     final SocketChannel channel = qa.getChannel();
12     int read = channel.read(rbuf);
13     if (read < 0) {
14         // some code
15     }
16     while (read > 0) { // 從網絡中讀數據一直讀到0爲止
17         rbuf.flip();
18         while (rbuf.remaining() > 0) { // 只要緩衝區有數據 就去解析操作
19             synchronized(currentOp) {
20                 currentOp.readFromBuffer(rbuf); // 從rbuf中解析響應
21                 if (currentOp.getState() == OperationState.COMPLETE) {
22                     Operation op = qa.removeCurrentReadOp(); // 操作解析成功,移除
23                 else if (currentOp.getState() == OperationState.RETRY) {
24                     ((VBucketAware) currentOp).addNotMyVbucketNode(currentOp.getHandlingNode());
25                     Operation op = qa.removeCurrentReadOp();
26                     retryOps.add(currentOp);
27                 }
28             }
29             currentOp=qa.getCurrentReadOp();
30         }
31         rbuf.clear();
32         read = channel.read(rbuf);
33     }
34 }
01 // 解析rbuf;readType有兩種取值,LINE 和 DATA,用來區分正在操作的數據是命令行還是數據塊。解析過程中,分別調用工作線程傳入到操作中的回調對象的三個方法,分別是:receivedStatus,gotData,complete。
02 public void readFromBuffer(ByteBuffer data) throws IOException {
03     while (getState() != OperationState.COMPLETE && data.remaining() > 0) {
04         if (readType == OperationReadType.DATA) {
05             handleRead(data);
06         else {
07             int offset = -1;
08             for (int i = 0; data.remaining() > 0; i++) {
09                 byte b = data.get();
10                 if (b == '\r') {
11                     foundCr = true;
12                 else if (b == '\n') {
13                     assert foundCr : "got a \\n without a \\r";
14                     offset = i;
15                     foundCr = false;
16                     break;
17                 else {
18                     assert !foundCr : "got a \\r without a \\n";
19                     byteBuffer.write(b);
20                 }
21             }
22             if (offset >= 0) {
23                 String line = new String(byteBuffer.toByteArray(), CHARSET);
24                 byteBuffer.reset();
25                 OperationErrorType eType = classifyError(line);
26                 if (eType != null) {
27                     handleError(eType, line);
28                 else {
29                     handleLine(line); // 取到完整的一行後 調用這個函數 解析行數據
30                 }
31             }
32         }
33     }
34 }
01 // 處理命令行和END行
02 // 解析命令行時,分析各種參數 data爲數據塊的字節數
03 public final void handleLine(String line) {
04     if (line.equals("END")) {
05         if (hasValue) {
06             getCallback().receivedStatus(END);
07         else {
08             getCallback().receivedStatus(NOT_FOUND);
09         }
10         transitionState(OperationState.COMPLETE);
11         data = null;
12     else if (line.startsWith("VALUE ")) {
13         String[] stuff = line.split(" ");
14         currentKey = stuff[1];
15         currentFlags = Integer.parseInt(stuff[2]);
16         data = new byte[Integer.parseInt(stuff[3])];
17         if (stuff.length > 4) {
18             casValue = Long.parseLong(stuff[4]);
19         }
20         readOffset = 0;
21         hasValue = true;
22         setReadType(OperationReadType.DATA);
23     else if (line.equals("LOCK_ERROR")) {
24         getCallback().receivedStatus(LOCK_ERROR);
25         transitionState(OperationState.COMPLETE);
26     else {
27         assert false "Unknown line type: " + line;
28     }
29 }

5、那個著名的bug

JAVA NIO bug 會導致 CPU 100%

http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6403933 

int selected = selector.select(delay);

    Set<SelectionKey> selectedKeys = selector.selectedKeys();

    if (selectedKeys.isEmpty() && !shutDown) {

      if (++emptySelects > DOUBLE_CHECK_EMPTY) {

        for (SelectionKey sk : selector.keys()) {

          if (sk.readyOps() != 0) {

            handleIO(sk);

          } else {

            lostConnection((MemcachedNode) sk.attachment());

          }

DOUBLE_CHECK_EMPTY = 256,當連續的select返回爲空時,++emptySelects,超過256,連接到當前mc節點的socket channel關閉,放入重連隊列。

七、調試 spymemcached

調試 spymemcached IO線程的過程中,工作線程放入到節點隊列的操作很容易超時,因此需要繼承DefaultConnectionFactory 複寫相關方法。

01 public class AstuteConnectionFactory extends DefaultConnectionFactory {
02 @Override
03 public boolean shouldOptimize() {
04 return false;
05 }
06 @Override
07 public long getOperationTimeout() {
08 return 3000000L; // 3000S
09 }
10 }

八、參考資料

NIOhttp://www.ibm.com/developerworks/cn/education/java/j-nio/index.html 

memcachedhttp://memcached.org/ 

protocolhttps://github.com/memcached/memcached/blob/master/doc/protocol.txt 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章