記錄一個拷貝檔案到GlusterFS卡住的解決過程
來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。# 問題簡介我們有一個分布式服務,儲存為Gluster FS,需要大量的讀寫檔案。在公司開發環境、測試環境都正常的情況下,線上上環境、高仿環境卻屢屢出現拷貝檔案到Gluster FS卡住的問題(檔案若是200M~5G大小,機率大概在3~4%左右,檔案已拷貝完成,源檔案和目標檔案md5一致,卡在目標檔案控制代碼close處。)。```func CopyFile(src, dest string) (copiedSize int64, err error) {copiedSize = 0srcFile, err := os.Open(src)if err != nil {return copiedSize, err}defer srcFile.Close()destFile, err := os.Create(dest)if err != nil {return copiedSize, err}defer destFile.Close() // 卡在這return io.Copy(destFile, srcFile)}```卡住的goroutine資訊樣本:```goroutine 109667 [syscall, 711 minutes]:syscall.Syscall(0x3, 0xf, 0x0, 0x0, 0xafb1a0, 0xc42000c150, 0x0)/usr/local/go/src/syscall/asm_linux_amd64.s:18 +0x5syscall.Close(0xf, 0x0, 0x0)/usr/local/go/src/syscall/zsyscall_linux_amd64.go:296 +0x4aos.(*file).close(0xc420344f00, 0x455550, 0xc4203696d0)/usr/local/go/src/os/file_unix.go:140 +0x86os.(*File).Close(0xc4200289f0, 0x1b6, 0xc4200289f0)/usr/local/go/src/os/file_unix.go:132 +0x33Common/utils.CopyFile(0xc42031eea0, 0x5d, 0xc420314840, 0x36, 0x10ce9d94, 0x0, 0x0)......```最後的/usr/local/go/src/syscall/asm_linux_amd64.s第18行前後代碼如下```TEXT·Syscall(SB),NOSPLIT,$0-56CALLruntime·entersyscall(SB) // 卡在系統調用開始處MOVQa1+8(FP), DIMOVQa2+16(FP), SIMOVQa3+24(FP), DXMOVQ$0, R10MOVQ$0, R8MOVQ$0, R9MOVQtrap+0(FP), AX// syscall entrySYSCALLCMPQAX, $0xfffffffffffff001JLSokMOVQ$-1, r1+32(FP)MOVQ$0, r2+40(FP)NEGQAXMOVQAX, err+48(FP)CALLruntime·exitsyscall(SB)RETok:MOVQAX, r1+32(FP)MOVQDX, r2+40(FP)MOVQ$0, err+48(FP)CALLruntime·exitsyscall(SB)RET```# 解決過程由於開發環境、測試環境用的Gluster FS是3.3.2,線上環境、高仿環境的Gluster FS版本是3.7.6,最開始是猜測可能是版本不一致導致的問題。因此最開始是從Gluster FS版本是否有問題,部署GlusterFS的軟硬體是否有問題開始入手,但始終找不出真正的原因。這時候公司流程就成了阻礙,因為原因基本靠經驗猜測,即便改一點點代碼,都要提測,找多個領導簽字,最壞的一次情況是一天都沒走完一個流程。最後,實在無奈,向領導申請操作部分高仿環境的許可權。好了,終於可以施展拳腳了。## 第一次,採用逾時處理機制我們想到的是,參考tensorflow的源碼,在Golang中用reflect實現一個類似 ./tensorflow/core/platform/cloud/retrying_utils.cc的代碼。基本原理就是,close等一些可能會卡住的函數是新啟一個goroutine來做,如果在close階段卡住,超過一定時間繼續往下走,反本文件都已經拷貝完了。主要代碼如下:```type RetryingUtils struct {Timeout time.DurationMaxRetries int}type CallReturn struct {Error errorReturnValues []reflect.Value}func NewRetryingUtils(timeout time.Duration, maxRetries int) *RetryingUtils {return &RetryingUtils{Timeout: timeout, MaxRetries: maxRetries}}func (r *RetryingUtils) CallWithRetries(any interface{}, args ...interface{}) CallReturn {var callReturn CallReturnvar retries intfor {callReturn.Error = nildone := make(chan int, 1)go func() {function := reflect.ValueOf(any)inputs := make([]reflect.Value, len(args))for i, _ := range args {inputs[i] = reflect.ValueOf(args[i])}callReturn.ReturnValues = function.Call(inputs)done <- 1}()select {case <-done:return callReturncase <-time.After(r.Timeout):callReturn.Error = errTimeout}retries++if retries >= r.MaxRetries {break}}return callReturn}```調用方式樣本:```NewRetryingUtils(time.Second*10, 1).CallWithRetries(fd.Close)```壓測兩天,此方法不可行。因為卡住之後,任務goroutine繼續往下走,會有機率導致進程為defunct。(註:無數的文章告訴我們,defunct殭屍進程的產生是沒有處理子進程退出資訊之類的,這個只是殭屍進程的一部分;比如只要goroutine卡住,若kill該進程,進程就會成為defunct)## 第二次,採用系統命令拷貝檔案我們採用linux的cp命令拷貝檔案,壓測兩天,通過。(它為什麼成功的原因,還沒完全整明白。因為每次壓測需要佔用兩位測試美女的大量時間,反反覆複地壓測,也不太好。如果是開發環境或測試環境,那好辦,我們開發可以自己壓測)## 第三次,測試是否由於多申請了Read許可權引起我們開始通過閱讀linux cp命令的源碼來尋找原因。發現coreutils/src/copy.c的[copy_reg](https://github.com/coreutils/coreutils/blob/master/src/copy.c)函數,這個函數的功能是拷貝普通檔案。golang的os.Create函數預設比該函數多申請了一個read許可權。copy_reg中的建立檔案:```int open_flags = O_WRONLY | O_BINARY | (x->data_copy_required ? O_TRUNC : 0);dest_desc = open (dst_name, open_flags);```golang中的建立檔案:```func Create(name string) (*File, error) {return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)}```由於以前我做遠程進程注入的時候,知道申請不必要的許可權,會導致失敗率增加。因此,猜測可能是此原因。測試結果,很遺憾,不是。## 第四次,在目標檔案控制代碼關閉之前,顯式調用Sync函數如下所示:```func CopyFile(src, dest string) (copiedSize int64, err error) {copiedSize = 0srcFile, err := os.Open(src)if err != nil {return copiedSize, err}defer srcFile.Close()destFile, err := os.Create(dest)if err != nil {return copiedSize, err}defer func() {destFile.Sync() // 卡在這destFile.Close()}return io.Copy(destFile, srcFile)}```卡住的goroutine樣本:```goroutine 51634 [syscall, 523 minutes]:syscall.Syscall(0x4a, 0xd, 0x0, 0x0, 0xafb1a0, 0xc42000c160, 0x0)/usr/local/go/src/syscall/asm_linux_amd64.s:18 +0x5syscall.Fsync(0xd, 0x0, 0x0)/usr/local/go/src/syscall/zsyscall_linux_amd64.go:492 +0x4aos.(*File).Sync(0xc420168a00, 0xc4201689f8, 0xc420240000)/usr/local/go/src/os/file_posix.go:121 +0x3eCommon/utils.CopyFile.func3()```越來越接近真相了,激動。想必原因是:write函數只是將資料寫到緩衝,並沒有實際寫到磁碟(這裡是GlusterFS)。由於網路或其它原因,導致最後Sync()卡住。## 真相我們找到了這篇文章https://lists.gnu.org/archive/html/gluster-devel/2011-09/msg00005.html這時營運查了下各個環境的GlusterFS配置,跟我們說,線上環境和高仿環境的GlusterFS的配置項performance.flush-behind為off,開發環境和測試環境是on。智者千慮,必有一失。嚴謹的營運們,偶爾疏忽實屬正常。當然最開心的是終於解決了問題。```gluster> volume infoVolume Name: pre-volumeType: Striped-ReplicateVolume ID: 3b018268-6b4b-4659-a5b0-38e1f949f10fStatus: StartedNumber of Bricks: 1 x 2 x 2 = 4Transport-type: tcpBricks:Brick1: 10.10.20.201:/data/preBrick2: 10.10.20.202:/data/preBrick3: 10.10.20.203:/data/preBrick4: 10.10.20.204:/data/preOptions Reconfigured:performance.flush-behind: OFF // 此處若為on,就Okdiagnostics.count-fop-hits: ondiagnostics.latency-measurement: onperformance.readdir-ahead: on```相關issue:https://github.com/gluster/glusterfs/issues/341346 次點擊