前言:這幾天在寫一個工具指令碼分析線上的大量的日誌檔案,本來應該是索然無味的一個工作,但是本著做到極致的原則,激發了我不斷思考如何優化。本文將從開發過程中的最開始版本,一點點講解優化的過程,最終用golang實現了一個類似java的worker執行緒池,收穫滿滿。
一,無腦開goroutine階段
1,任務背景 這個工具的作用簡單介紹如下:首先是線上的日誌量是非常龐大的,然後要去讀取日誌檔案的內容,然後一條條日誌項分析,匹配出想要的日誌項寫入檔案,再提取該檔案中的資料計算。日誌項格式模擬資料如下:
{
"id":xx,"time":"2017-11-19","key1":"value1","key2":"value2".......
}
複製程式碼
2,無腦開gorountine 先給下程式碼(只放核心程式碼,省略檔案操作和異常錯誤處理),再來說說這個階段的思路
var wirteChan = make(chan []byte) //用於寫入檔案
var waitgroup sync.WaitGroup //用於控制同步
func main(){
//省略寫入檔案的開啟操作,畢竟我們主要講併發這塊
//初始化寫入的channel
InitWriter(outLogFileWriter)
//接下來去遍歷每個日誌檔案,每讀出一個日誌項就開一個gorountine去處理
for _,file := range logDir {
if file.IsDir() {
continue
} else {
HandlerFile(arg.dir + "/" + file.Name()) //處理每個檔案
}
}
}
/**
* 初始化Writer的channel
*/
func InitWriter(outLogFileWriter *bufio.Writer) {
go func() {
for data := range wirteChan {
nn, err := outLogFileWriter.Write(data)
}
}()
}
//處理每個檔案,然後開G去處理每個日誌項
func HandlerFile(fileName string) {
file, err := os.Open(fileName)
defer file.Close()
br := bufio.NewReader(file)
for {
data, err := br.ReadBytes('\n')
if err == io.EOF {
break
} else {
go Handler(data) //每次開一個G去處理,處理完寫入writeChannel
}
}
}
複製程式碼
解析:寫部落格很不喜歡放大篇幅程式碼,所以上面給的只是重要的程式碼,上面程式碼註釋有說到的也不重複說了。好了,我們來想想上面的程式碼有什麼問題?我們現在就假設我們就只有一個非常大的檔案,檔案中每個記錄項都無腦開一個G去執行。那麼,執行一下,我們會發現,好慢呀~。問題出在哪裡呢?首先我們無法控制G的數量,其次日誌檔案非常大,這樣執行下來,G的數量是非常龐大的,多個G要往一個channel中寫資料,那麼也會發生嚴重的阻塞。種種原因,導致了這個方法是不適用的
二,加入帶緩衝的任務佇列
1,任務佇列 在上面我們說到,我們無法控制任務的數量,那麼,我在這裡就加入了一個任務佇列,來對任務進行排隊,同時可以控制任務的數量。上程式碼:
/**
* Job結構體,包含要處理的資料和處理函式(這個可根據需要修改)
*/
type Job struct {
Data []byte
Proc func([]byte)
}
//Job佇列,儲存要做的Job,將每個任務打包成Job傳送到這裡
var JobQueue chan Job = make(chan Job, arg.maxqueue)
//啟動處理函式處理
func Handler(Data []byte) {
for range job := <-Queue {
job.Proc(Data)
}
}
複製程式碼
解析:在這個時候,抽象出來了任務模型Job,由於函式呼叫其實就是函式地址加函式引數,所以我們可以將處理函式也放進Job中。然後讓處理函式去處理就行了。想到這裡,稍微有點佩服自己了,接著興致勃勃的執行一下。嗯,好像沒快多少(其實這個取決了你的處理函式,就是Job中的Proc)。What?冷靜下來分析一下,真覺得自己真可愛。我僅僅是對任務進行了包裝,然後用了一個帶緩衝的任務佇列,由於建立的Job遠遠大於單個M的處理能力,帶緩衝只是稍微把問題拖後了一點。
三,Job/Worker模型
其實寫到這裡,心裡對如何優化已經有點B數了。我想起了java中的執行緒池的概念,我可以建立一個執行緒池,然後池中包含多個worker(數量可以指定),每個worker去佇列中取任務處理,處理完則繼續取任務。同時為了提高通用性,引數型別都改為了interface{}。好了,接下來看看程式碼,這裡的程式碼都很關鍵,所以就全部放上來了
type Job struct {
Data interface{}
Proc func(interface{})
}
//Job佇列,儲存要做的Job
var JobQueue chan Job = make(chan Job, arg.maxqueue)
//Woker,用來從Job佇列中取出Job執行
type Worker struct {
WokerPool chan chan Job //表示屬於哪個Worker池,同時接收JobChannel註冊
JobChannel chan Job //任務管道,通過這個管道獲取任務執行
Quit chan bool //用來停止Worker
}
//新建一個Worker,需要傳入Worker池引數
func NewWorker(wokerPool chan chan Job) Worker {
return Worker{
WokerPool: wokerPool,
JobChannel: make(chan Job),
Quit: make(chan bool),
}
}
//Worker的啟動:包含:(1) 把該worker的JobChannel註冊到WorkerPool中去 (2) 監聽JobChannel上有沒有新的任務到來 (3) 監聽是否受到關閉的請求
func (worker Worker) Start() {
go func() {
for {
worker.WokerPool <- worker.JobChannel //每次做完任務後就重新註冊上去通知本worker又處於可用狀態了
select {
case job := <-worker.JobChannel:
job.Proc(job.Data)
case quit := <-worker.Quit: //接收到關閉資訊,直接退出即可
if quit {
return
}
}
}
}()
}
//Worker的關閉:只要傳送一個關閉訊號即可
func (worker Worker) Stop() {
go func() {
worker.Quit <- true
}()
}
//管理Worker的排程器,包含最大worker數量和workerpool
type Dispatcher struct {
MaxWorker int
WorkerPool chan chan Job
}
//啟動一個排程器
func (dispatcher *Dispatcher) Run() {
//啟動maxworker個worker
for i := 0; i < dispatcher.MaxWorker; i++ {
worker := NewWorker(dispatcher.WorkerPool)
worker.Start()
}
//接下來啟動排程服務
go dispatcher.dispatch()
}
func (dispatcher *Dispatcher) dispatch() {
for {
select {
case job := <-JobQueue:
go func(job Job) {
jobChannel := <-dispatcher.WorkerPool //獲取一個可用的worker
jobChannel <- job //將該job傳送給該worker
}(job)
}
}
}
//新建一個排程器
func NewDispatcher(maxWorker int) *Dispatcher {
workerPool := make(chan chan Job, maxWorker)
return &Dispatcher{
WorkerPool: workerPool,
MaxWorker: maxWorker,
}
}
複製程式碼
解析:程式碼中每句都註釋得非常清楚了,就不重複了。我們可以通過這樣來開啟這個模型:dispatcher := NewDispatcher(MaxWorker) dispatcher.Run()
。有一點需要強調的是,處理函式這塊需要根據自己的業務去寫,然後和資料打包成Job再發給JobQueue就行了。接著我執行了我的指令碼,幾十G的檔案經過三輪的處理函式(就是說我需要三輪處理,每輪處理都根據上輪的結果)耗時在三分鐘到四分鐘之間,而且CPU佔用率等也不高。對於耗時高的,可以使用pprof工具分析一下到底慢在了哪裡
四,總結
因為之前剛學了golang的併發原理,然後剛好有這個任務,於是自己就開始了從零一點點的摸索和優化,整個工具寫完,自己對golang的併發的理解又更加的深入了,而且對鎖,檔案操作等也熟悉了起來。收穫很多東西,所以我鼓勵學習一個新東西,不能只懂原理,還要自己多動手一下,這樣才牢固。其實這個模型還是存在一些不足之處,後續會繼續優化。 期間也參考了一些很不錯的部落格,在這裡也表示感謝。