對於linux go1.5版本的一種tcp監聽關閉處理方式

cdh0805010118發表於2018-07-23

tchannel-go專案作者prashantv對golang1.5 linux版本socket Accept方法的封裝

問題描述

作者在linux上測試tchannel-go專案時發現,當關閉服務端的listener後,有時候還是有一些client connection能夠進來。後來作者對當時的go1.5版本進行大量測試,確實發現存在,當服務端主動關閉後,client還能夠進來。但是在osx其他平臺不會發現這個問題。

當listener做關閉操作後,然後client再發起請求建立連線。實際上它只是標記socket為Closed狀態,但是不會影響epoll接收新連線。

詳細解釋:

如果epoll所在的監聽佇列上有新來的連線,這時socket accept正在從該佇列上獲取新來的連線。這時,如果server主動關閉listener,則因為server端存在連線引用,所以暫時不會關閉,需要等待accept當前新來的連線處理完成後,再關閉並destory fd。

所以出現該問題的主要原因是,當accept正在獲取新來的連線時,因為引用計數不為0,所以導致監聽無法真正關閉。只有當前accept獲取到新來的連線後,才會使得引用計數降為0,則時才會真正關閉監聽。

問題解決

所以針對這個go1.5版本linux等存在的缺陷,需要在外層引入引用計數和條件變數,當accept正在阻塞或獲取新來的連線時,如果server直接關閉監聽,則正在阻塞狀態的goroutine,直接收到server關閉error;如果正在獲取新來的連線,則外層加一個引用計數,當獲取完成後,在減去這個引用計數;在server的close方法包裝一層,如果這個引用計數不為0,則阻塞當前這個goroutine,直到引用計數等於0,再退出。這樣保證了accept不會再接收到新的連線。

程式碼示例

// 對net.Listener的封裝,引入引用計數和條件變數
type SaneListener struct {
    l net.Listener
    c *sync.Cond
    refCount int
}

// 當進入Accept之前,引用計數做加一操作,防止server主動Close listener操作時,因為底層的引用計數不為0,導致暫時不會發生真正的close fd操作。
// SaleListener的Close操作,因為refCount引用計數不為0,則Close暫時不會退出。底層的監聽已關閉,但是會等待accept獲取新連線操作處理完成,這樣close操作就相當於滯後了一個連線處理。
func (s *SaneListener) incRef() {
    s.c.L.Lock()
    s.refCount++
    s.c.L.Unlock()
}

// 當accept獲取到新來的連線或者獲取到一個server監聽關閉error,引用計數減一
// 這樣SaneListener的Close操作,因為引用計數關閉,則真正關閉。
// 
// 由於SaneListener的Close操作,如果server正在獲取新連線,則該goroutine會發生條件阻塞;等待accept操作完成後,通過Broadcast操作喚醒睡眠的goroutine,繼續Close操作。
func (s *SaneListener) decRef() {
    s.c.L.Lock()
    s.refCount---
    s.c.Broadcast()
    s.c.L.Unlock()
}

// accept操作
func (s *SaneListener) Accept() (net.Conn, error){
    s.incRef()
    defer s.decRef()
    return s.l.Accept()
}

// Close操作:底層的監聽是提前關閉了,但是epoll佇列中正在被accept的新連線還尚在處理中,所以底層的引用計數不等於0,則需要該操作完成後,再退出Close呼叫。這樣,在Close操作後,server不會接收新的連線了
func (s *SaneListener) Close() error {
    err := s.l.Close()
    if err == nil {
        s.c.L.Lock()
        for s.refCount > 0 {
            s.c.Wait()
        }
        s.c.L.Unlock()
    }
    return err
}

func (s *SaneListener) Addr() net.Addr {
    return s.l.Addr()
}

條件變數cond,使得refCount大於0時,主動阻塞該goroutine;等待accept完成獲取新連線或者獲取到error操作後,在通過decRef的Broadcast廣播喚醒阻塞的goutines。

小結

我們可以看到server對監聽關閉操作,當accept正在獲取新來的連線時,因引用計數不為0,則不會真正的destroy掉net fd。通過引入上層引用計數,來達到當關閉監聽後,確保server不會再接收新連線了。這個引用計數和條件變數大家可以認真學習,同時學習下golang的網路庫。

參考資料

tchannel-go net.listener

net: Listener sometimes accepts connections after Close

Golang網路:核心API實現剖析(一)

關於TCP 半連線佇列和全連線佇列

後記

// go1.10.2/src/internal/poll/fd_unix.go 第93行
// 我們可以看到當底層accept獲取新連線的引用計數為0時,才會真正destory掉net fd。

 77 // Close closes the FD. The underlying file descriptor is closed by the
 78 // destroy method when there are no remaining references.
 79 func (fd *FD) Close() error {
 80     if !fd.fdmu.increfAndClose() {
 81         return errClosing(fd.isFile)
 82     }
 83
 84     // Unblock any I/O.  Once it all unblocks and returns,
 85     // so that it cannot be referring to fd.sysfd anymore,
 86     // the final decref will close fd.sysfd. This should happen
 87     // fairly quickly, since all the I/O is non-blocking, and any
 88     // attempts to block in the pollDesc will return errClosing(fd.isFile).
 89     fd.pd.evict()
 90
 91     // The call to decref will call destroy if there are no other
 92     // references.
 93     err := fd.decref()
 94
 95     // Wait until the descriptor is closed. If this was the only
 96     // reference, it is already closed. Only wait if the file has
 97     // not been set to blocking mode, as otherwise any current I/O
 98     // may be blocking, and that would block the Close.
 99     if !fd.isBlocking {
100         runtime_Semacquire(&fd.csema)
101     }
102
103     return err
104 }

// go1.10.2/src/internal/poll/fd_mutex.go
209 func (fd *FD) decref() error {
210     if fd.fdmu.decref() {
211         return fd.destroy()
212     }
213     return nil
214 }

相關文章