摘要:概要本文將從源碼的角度分析怎么將容器的日志收集出來并通過配置的發(fā)送出去,并結合示例介紹了好雨云幫中實現(xiàn)的一個。首先根據容器配置的類別調用返回一個方法類型實質就是從工廠類注冊的插件去查找,具體源碼下文分析。直接接收容器的日志。
概要
本文將從docker(1.12.6)源碼的角度分析docker daemon怎么將容器的日志收集出來并通過配置的log-driver發(fā)送出去,并結合示例介紹了好雨云幫中實現(xiàn)的一個zmq-loger。閱讀本文,你也可以實現(xiàn)適合自己業(yè)務場景的log-driver。
閱讀準備本文適合能夠閱讀和編寫golang代碼的同學。
(1)首先你需要認知以下幾個關鍵詞:
stdout:
標準輸出,進程寫數(shù)據的流。
stderr:
錯誤輸出,進程寫錯誤數(shù)據的流。
子進程:
由一個進程(父進程)創(chuàng)建的進程,集成父進程大部分屬性,同時可以被父進程守護和管理。
(2)你需要知道關于進程產生日志的形式:
進程產生日志有兩類輸出方式,一類是寫入到文件中。另一類是直接寫到stdout或者stderr,例如php的echo python的print golang的fmt.Println("")等等。
(3)是否知道docker-daemon與運行中container的關系?
一個container就是一個特殊的進程,它是由docker daemon創(chuàng)建并啟動,因此container是docker daemon的子進程。由docker daemon守護和管理。因此container的stdout能夠被docker daemon獲取到。基于此理論,我們來分析docker daemon相關代碼。
# /container/container.go:62 type CommonContainer struct{ StreamConfig *stream.Config ... } # /container/stream/streams.go:26 type Config struct { sync.WaitGroup stdout *broadcaster.Unbuffered stderr *broadcaster.Unbuffered stdin io.ReadCloser stdinPipe io.WriteCloser }
找到如上所示對應的代碼,顯示了每一個container實例都有幾個屬性stdout,stderr,stdin,以及管道stdinPipe。這里說下stdinPipe,當容器使用-i參數(shù)啟動時標準輸入將被運行,daemon將能夠使用此管道向容器內寫入標準輸入。
我們試想以上圖例,如果是你,你怎么實現(xiàn)日志收集轉發(fā)?
# /container/container.go:312 func (container *Container) StartLogger(cfg containertypes.LogConfig) (logger.Logger, error) { c, err := logger.GetLogDriver(cfg.Type) if err != nil { return nil, fmt.Errorf("Failed to get logging factory: %v", err) } ctx := logger.Context{ Config: cfg.Config, ContainerID: container.ID, ContainerName: container.Name, ContainerEntrypoint: container.Path, ContainerArgs: container.Args, ContainerImageID: container.ImageID.String(), ContainerImageName: container.Config.Image, ContainerCreated: container.Created, ContainerEnv: container.Config.Env, ContainerLabels: container.Config.Labels, DaemonName: "docker", } // Set logging file for "json-logger" if cfg.Type == jsonfilelog.Name { ctx.LogPath, err = container.GetRootResourcePath(fmt.Sprintf("%s-json.log", container.ID)) if err != nil { return nil, err } } return c(ctx) } #/container/container.go:978 func (container *Container) startLogging() error { if container.HostConfig.LogConfig.Type == "none" { return nil // do not start logging routines } l, err := container.StartLogger(container.HostConfig.LogConfig) if err != nil { return fmt.Errorf("Failed to initialize logging driver: %v", err) } copier := logger.NewCopier(map[string]io.Reader{"stdout": container.StdoutPipe(), "stderr": container.StderrPipe()}, l) container.LogCopier = copier copier.Run() container.LogDriver = l // set LogPath field only for json-file logdriver if jl, ok := l.(*jsonfilelog.JSONFileLogger); ok { container.LogPath = jl.LogPath() } return nil }
第一個方法是為container查找log-driver。首先根據容器配置的log-driver類別調用:logger.GetLogDriver(cfg.Type)返回一個方法類型:
/daemon/logger/factory.go:9 type Creator func(Context) (Logger, error)
實質就是從工廠類注冊的logdriver插件去查找,具體源碼下文分析。獲取到c方法后構建調用參數(shù)具體就是容器的一些信息。然后使用調用c方法返回driver。driver是個接口類型,我們看看有哪些方法:
# /daemon/logger/logger.go:61 type Logger interface { Log(*Message) error Name() string Close() error }
很簡單的三個方法,也很容易理解,Log()發(fā)送日志消息到driver,Close()進行關閉操作(根據不同實現(xiàn))。
也就是說我們自己實現(xiàn)一個logdriver,只需要實現(xiàn)如上三個方法,然后注冊到logger工廠類中即可。下面我們來看/daemon/logger/factory.go
第二個方法就是處理日志了,獲取到日志driver,在創(chuàng)建一個Copier,顧名思義就是復制日志,分別從stdout 和stderr復制到logger driver。下面看看具體關鍵實現(xiàn):
#/daemon/logger/copir.go:41 func (c *Copier) copySrc(name string, src io.Reader) { defer c.copyJobs.Done() reader := bufio.NewReader(src) for { select { case <-c.closed: return default: line, err := reader.ReadBytes(" ") line = bytes.TrimSuffix(line, []byte{" "}) // ReadBytes can return full or partial output even when it failed. // e.g. it can return a full entry and EOF. if err == nil || len(line) > 0 { if logErr := c.dst.Log(&Message{Line: line, Source: name, Timestamp: time.Now().UTC()}); logErr != nil { logrus.Errorf("Failed to log msg %q for logger %s: %s", line, c.dst.Name(), logErr) } } if err != nil { if err != io.EOF { logrus.Errorf("Error scanning log stream: %s", err) } return } } } }
每讀取一行數(shù)據,構建一個消息,調用logdriver的log方法發(fā)送到driver處理。
日志driver注冊器位于/daemon/logger/factory.go的源碼實現(xiàn)即時日志driver的注冊器,其中幾個重要的方法(上文已經提到一個):
# /daemon/logger/factory.go:21 func (lf *logdriverFactory) register(name string, c Creator) error { if lf.driverRegistered(name) { return fmt.Errorf("logger: log driver named "%s" is already registered", name) } lf.m.Lock() lf.registry[name] = c lf.m.Unlock() return nil } # /daemon/logger/factory.go:39 func (lf *logdriverFactory) registerLogOptValidator(name string, l LogOptValidator) error { lf.m.Lock() defer lf.m.Unlock() if _, ok := lf.optValidator[name]; ok { return fmt.Errorf("logger: log validator named "%s" is already registered", name) } lf.optValidator[name] = l return nil }
看起來很簡單,就是將一個Creator方法類型添加到一個map結構中,將LogOptValidator添加到另一個map這里注意加鎖的操作。
#/daemon/logger/factory.go:13 type LogOptValidator func(cfg map[string]string) error
這個主要是驗證driver的參數(shù) ,dockerd和docker啟動參數(shù)中有:--log-opt
好雨云幫自己實現(xiàn)一個基于zmq的log-driver上文已經完整分析了docker daemon管理logdriver和處理日志的整個流程。相信你已經比較明白了。下面我們以zmq-driver為例講講我們怎么實現(xiàn)自己的driver。直接接收容器的日志。
上文我們已經談了一個log-driver需要實現(xiàn)的幾個方法。
我們可以看看位于/daemon/logger目錄下的已有的driver的實現(xiàn),例如fluentd,awslogs等。
下面我們來分析zmq-driver具體的代碼:
//定義一個struct,這里包含一個zmq套接字 type ZmqLogger struct { writer *zmq.Socket containerId string tenantId string serviceId string felock sync.Mutex } //定義init方法調用logger注冊器的方法注冊當前driver //和參數(shù)驗證方法。 func init() { if err := logger.RegisterLogDriver(name, New); err != nil { logrus.Fatal(err) } if err := logger.RegisterLogOptValidator(name, ValidateLogOpt); err != nil { logrus.Fatal(err) } } //實現(xiàn)一個上文提到的Creator方法注冊logdriver. //這里新建一個zmq套接字構建一個實例 func New(ctx logger.Context) (logger.Logger, error) { zmqaddress := ctx.Config[zmqAddress] puber, err := zmq.NewSocket(zmq.PUB) if err != nil { return nil, err } var ( env = make(map[string]string) tenantId string serviceId string ) for _, pair := range ctx.ContainerEnv { p := strings.SplitN(pair, "=", 2) //logrus.Errorf("ContainerEnv pair: %s", pair) if len(p) == 2 { key := p[0] value := p[1] env[key] = value } } tenantId = env["TENANT_ID"] serviceId = env["SERVICE_ID"] if tenantId == "" { tenantId = "default" } if serviceId == "" { serviceId = "default" } puber.Connect(zmqaddress) return &ZmqLogger{ writer: puber, containerId: ctx.ID(), tenantId: tenantId, serviceId: serviceId, felock: sync.Mutex{}, }, nil } //實現(xiàn)Log方法,這里使用zmq socket發(fā)送日志消息 //這里必須注意,zmq socket是線程不安全的,我們知道 //本方法可能被兩個線程(復制stdout和膚質stderr)調用//必須使用鎖保證線程安全。否則會發(fā)生錯誤。 func (s *ZmqLogger) Log(msg *logger.Message) error { s.felock.Lock() defer s.felock.Unlock() s.writer.Send(s.tenantId, zmq.SNDMORE) s.writer.Send(s.serviceId, zmq.SNDMORE) if msg.Source == "stderr" { s.writer.Send(s.containerId+": "+string(msg.Line), zmq.DONTWAIT) } else { s.writer.Send(s.containerId+": "+string(msg.Line), zmq.DONTWAIT) } return nil } //實現(xiàn)Close方法,這里用來關閉zmq socket。 //同樣注意線程安全,調用此方法的是容器關閉協(xié)程。 func (s *ZmqLogger) Close() error { s.felock.Lock() defer s.felock.Unlock() if s.writer != nil { return s.writer.Close() } return nil } func (s *ZmqLogger) Name() string { return name } //驗證參數(shù)的方法,我們使用參數(shù)傳入zmq pub的地址。 func ValidateLogOpt(cfg map[string]string) error { for key := range cfg { switch key { case zmqAddress: default: return fmt.Errorf("unknown log opt "%s" for %s log driver", key, name) } } if cfg[zmqAddress] == "" { return fmt.Errorf("must specify a value for log opt "%s"", zmqAddress) } return nil }總結
多研究源碼可以方便我們理解docker的工作原理。今天我們分析了日志部分。希望讀者對這部分功能能夠理解得更清晰。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://systransis.cn/yun/28006.html
摘要:我推薦你使用進行日志收集,將作為的出口。集群目前暫時沒有提供日志查看機制。以如下的形式啟動容器,容器日志將發(fā)往配置的。 【作者barnett】本文介紹了k8s官方提供的日志收集方法,并介紹了Fluentd日志收集器并與其他產品做了比較。最后介紹了好雨云幫如何對k8s進行改造并使用ZeroMQ以消息的形式將日志傳輸?shù)浇y(tǒng)一的日志處理中心。 容器日志存在形式 目前容器日志有兩種輸出形式: ...
摘要:我推薦你使用進行日志收集,將作為的出口。集群目前暫時沒有提供日志查看機制。以如下的形式啟動容器,容器日志將發(fā)往配置的。 【作者barnett】本文介紹了k8s官方提供的日志收集方法,并介紹了Fluentd日志收集器并與其他產品做了比較。最后介紹了好雨云幫如何對k8s進行改造并使用ZeroMQ以消息的形式將日志傳輸?shù)浇y(tǒng)一的日志處理中心。 容器日志存在形式 目前容器日志有兩種輸出形式: ...
摘要:實現(xiàn)方式是每個掛載目錄使用的插件掃描每個容器日志文件,直接發(fā)送給。首先根據容器配置的類別調用返回一個方法類型實質就是從工廠類注冊的插件去查找,具體源碼下文分析。在處理中心統(tǒng)一完成下一步處理。直接接收容器的日志。 容器日志 輸出形式: 目前容器日志有兩種輸出形式: stdout,stderr 標準輸出 這種形式的日志輸出我們可以直接使用docker logs查看日志, k8s 集群中同樣...
摘要:在中默認開啟端口用于偵聽發(fā)送過來的日志報文。至此,在中如何部署一套完整系統(tǒng)已經介紹完了,下面我將介紹如何將容器日志發(fā)送到中。下面我將介紹如何收集容器日志。目前日志顯的雜亂無序。,現(xiàn)在我們再去查看發(fā)現(xiàn)我們的容器日志已經展示在這里了。 相信大家對于容器和 docker 這個概念并不陌生,很高興的是 docker 為我們提供了多種log-driver。 showImg(https://se...
摘要:概述應用一旦容器化以后,需要考慮的就是如何采集位于容器中的應用程序的打印日志供運維分析。 showImg(https://segmentfault.com/img/remote/1460000014146680); 概述 應用一旦容器化以后,需要考慮的就是如何采集位于Docker容器中的應用程序的打印日志供運維分析。典型的比如 SpringBoot應用的日志 收集。本文即將闡述如何利...
閱讀 579·2023-04-25 16:00
閱讀 1624·2019-08-26 13:54
閱讀 2504·2019-08-26 13:47
閱讀 3435·2019-08-26 13:39
閱讀 1053·2019-08-26 13:37
閱讀 2748·2019-08-26 10:21
閱讀 3544·2019-08-23 18:19
閱讀 1609·2019-08-23 18:02