前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Docker源码分析之容器日志处理与log-driver实现

Docker源码分析之容器日志处理与log-driver实现

作者头像
Rainbond开源
发布于 2018-05-31 05:14:52
发布于 2018-05-31 05:14:52
1.4K00
代码可运行
举报
运行总次数:0
代码可运行

概要

本文将从docker(1.12.6)源码的角度分析docker daemon怎么将容器的日志收集出来并通过配置的log-driver发送出去,并结合示例介绍了好雨云帮中实现的一个zmq-loger。阅读本文,你也可以实现适合自己业务场景的log-driver。

阅读准备

本文适合能够阅读和编写golang代码的同学。 (1)首先你需要认知以下几个关键词:

  • stdout: 标准输出,进程写数据的流。
  • stderr: 错误输出,进程写错误数据的流。
  • 子进程: 由一个进程(父进程)创建的进程,集成父进程大部分属性,同时可以被父进程守护和管理。

(2)你需要知道关于进程产生日志的形式: 进程产生日志有两类输出方式,一类是写入到文件中。另一类是直接写到stdout或者stderr,例如php的echo python的print golang的fmt.Println("")等等。 (3)是否知道docker-daemon与运行中container的关系? 一个container就是一个特殊的进程,它是由docker daemon创建并启动,因此container是docker daemon的子进程。由docker daemon守护和管理。因此container的stdout能够被docker daemon获取到。基于此理论,我们来分析docker daemon相关代码。

docker-daemon关于日志源码分析

container实例源码

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# /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参数启动时标准输入将被运行,daemon将能够使用此管道向容器内写入标准输入。

我们试想以上图例,如果是你,你怎么实现日志收集转发?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# /container/container.go:312func (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:978func (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)返回一个方法类型:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/daemon/logger/factory.go:9
type Creator func(Context) (Logger, error)

实质就是从工厂类注册的logdriver插件去查找,具体源码下文分析。获取到c方法后构建调用参数具体就是容器的一些信息。然后使用调用c方法返回driver。driver是个接口类型,我们看看有哪些方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# /daemon/logger/logger.go:61type Logger interface {
    Log(*Message) error
    Name() string
    Close() error
}

很简单的三个方法,也很容易理解,Log()发送日志消息到driver,Close()进行关闭操作(根据不同实现)。 也就是说我们自己实现一个logdriver,只需要实现如上三个方法,然后注册到logger工厂类中即可。下面我们来看/daemon/logger/factory.go

第二个方法就是处理日志了,获取到日志driver,在创建一个Copier,顾名思义就是复制日志,分别从stdout 和stderr复制到logger driver。下面看看具体关键实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#/daemon/logger/copir.go:41func (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('\n')
            line = bytes.TrimSuffix(line, []byte{'\n'})            // 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
            }
        }
    }
}

每读取一行数据,构建一个消息,调用logdriver的log方法发送到driver处理。

日志driver注册器

位于/daemon/logger/factory.go的源码实现即时日志driver的注册器,其中几个重要的方法(上文已经提到一个):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# /daemon/logger/factory.go:21func (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:39func (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这里注意加锁的操作。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#/daemon/logger/factory.go:13
type LogOptValidator func(cfg map[string]string) error

这个主要是验证driver的参数 ,dockerd和docker启动参数中有:--log-opt

好雨云帮自己实现一个基于zmq的log-driver

上文已经完整分析了docker daemon管理logdriver和处理日志的整个流程。相信你已经比较明白了。下面我们以zmq-driver为例讲讲我们怎么实现自己的driver。直接接收容器的日志。 上文我们已经谈了一个log-driver需要实现的几个方法。 我们可以看看位于/daemon/logger目录下的已有的driver的实现,例如fluentd,awslogs等。 下面我们来分析zmq-driver具体的代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//定义一个struct,这里包含一个zmq套接字type ZmqLogger struct {
    writer      *zmq.Socket
    containerId string
    tenantId    string
    serviceId   string
    felock      sync.Mutex
}//定义init方法调用logger注册器的方法注册当前driver//和参数验证方法。func init() {    if err := logger.RegisterLogDriver(name, New); err != nil {
        logrus.Fatal(err)
    }    if err := logger.RegisterLogOptValidator(name, ValidateLogOpt); err != nil {
        logrus.Fatal(err)
    }
}//实现一个上文提到的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}//实现Log方法,这里使用zmq socket发送日志消息//这里必须注意,zmq socket是线程不安全的,我们知道//本方法可能被两个线程(复制stdout和肤质stderr)调用//必须使用锁保证线程安全。否则会发生错误。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}//实现Close方法,这里用来关闭zmq socket。//同样注意线程安全,调用此方法的是容器关闭协程。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
}//验证参数的方法,我们使用参数传入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的工作原理。今天我们分析了日志部分。希望读者对这部分功能能够理解得更清晰。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2017-01-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Rainbond 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
docker源码分析-Daemon创建及启动
上一篇分析了Docker Client的源码运行逻辑,本篇接着分析Docker Daemon的运行逻辑。Docker Daemon的运行逻辑很复杂,大家看着来要有耐心了。 Docker Daemon的执行 Docker Daemon的入口在cmd/dockerd/docker.go,先看main函数。 func main() { if reexec.Init() { return } // Set terminal emulation based on platform as required.
jeremyxu
2018/05/10
2.1K0
golang 源码分析(16):Docker CE 18.03源码
这里做的事情可就多了,加载配置,设置相关的变量和配置,监听端口,设置信号handler, 起了API server,等待接受请求。
golangLeetcode
2022/08/02
4360
敲黑板 | 云帮日志那点事儿
容器日志 输出形式: 目前容器日志有两种输出形式: stdout,stderr 标准输出 这种形式的日志输出我们可以直接使用docker logs查看日志, k8s 集群中同样集群可以使用kubectl logs类似的形式查看日志。 日志文件记录 这种日志输出我们无法从以上方法查看日志内容,只能tail日志文件查看。 收集方式: 不论你的业务容器日志如何输出,都是可以使用统一的日志收集器收集。常见的日志收集方式: k8s 集群 集群启动时会在每个机器启动一个Fluentd agent收集日志然后发送给 El
Rainbond开源
2018/05/31
6600
docker container DNS配置介绍和源码分析
本文主要介绍了docker容器的DNS配置及其注意点,重点对docker 1.10发布的embedded DNS server进行了源码分析,看看embedded DNS server到底是个啥,它是如何工作的。 Configure container DNS DNS in default bridge network OptionsDescription -h HOSTNAME or --hostname=HOSTNAME在该容器启动时,将HOSTNAME设置到容器内的/etc/hosts, /e
Walton
2018/04/13
7.1K1
docker container DNS配置介绍和源码分析
Golang中log日志包的使用
Golang中log日志包的使用
Java架构师必看
2021/09/14
8400
Go中日志库
Gin框架的请求日志默认在控制台输出,但更多的时候,尤其上线运行时,我们希望将用户的请求日志保存到日志文件中,以便更好的分析与备份。
码客说
2024/03/29
1460
Go中日志库
docker源码分析-Client创建与命令执行
一直在研究docker,最近被人问到docker到底是怎么工作的却不是太清楚,在网上偶然看到一本讲docker源码的电子书,花了整晚看了下,终于对docker的实现细节比较清楚了。但这本电子书讲的是1.2版本时的docker源码,跟最新的1.12版本相比差别还是挺大的,在这本书里讲到的源码与最新源码已经对应不上了。因此我计划写一份针对1.12版本的docker源码分析。 docker的总体架构 这部分基本没有太大的变化,我觉得可以直接参照1.2版本的总体架构,就不重复分析了。见这里。 Client创建与命令
jeremyxu
2018/05/10
1.3K0
docker v1.11 源码重构分析
基于docker v1.12的源代码,对docker engine v1.11中重构后的源码结构进行分析,涵盖dockerd, containerd, containerd-shim, runC。 ##docker1.11新特性 docker在v1.11版本进行了重大的重构,对docker engine和container进行了解耦,docker engine运行在containerd上,containerd运行在runC上,通过containerd-shim中间层进行了解耦。之前的docker engin
Walton
2018/04/13
1.6K0
docker v1.11 源码重构分析
golang 源码分析(17):cobra docker
然后创建DockerCli对象,DockerCli对象在cli/cli.go里声明。
golangLeetcode
2022/08/02
5100
容器日志知多少 (1) Docker logs & logging driver
本篇已加入《.NET Core on K8S学习实践系列文章索引》,可以点击查看更多容器化技术相关系列文章。监控和日志历来都是系统稳定运行和问题排查的关键,在微服务架构中,数量众多的容器以及快速变化的特性使得一套集中式的日志管理系统变成了生产环境中一个不可获取的部分。此次话题我们会集中在日志管理方面,本篇会介绍Docker自带的logs子命令以及其Logging driver。
Edison Zhou
2019/11/19
2K0
容器日志知多少 (1) Docker logs & logging driver
Docker EOF问题排查
某天接到客户反馈,pod的事件中出现大量的 warning event: Readiness probe failed: OCI runtime exec failed: exec failed: EOF: unknown。但不影响客户访问该服务。
没有故事的陈师傅
2021/06/24
5.1K0
Docker EOF问题排查
容器日志收集方案对比,以及对log-pilot组件原理分析
收集POD中container日志,日志还分为两种一种是容器标准输出日志和容器内日志。
silenceper
2019/11/19
4K0
容器日志收集方案对比,以及对log-pilot组件原理分析
Go微服务,第10部分:集中式日志记录
在Go微服务博客系列的这一部分中,我们将介绍基于Logrus,Docker Gelf日志驱动程序和“作为服务的日志记录” Loggly服务的Go微服务的日志记录策略。
Aaroncang
2018/07/05
2.7K0
Go微服务,第10部分:集中式日志记录
golang 源码分析(14)docker NewDaemon
在Docker架构中有很多重要的概念,如:graph,graphdriver,execdriver,networkdriver,volumes,Docker containers等。Docker在实现过程中,需要将以上实体进行统一化管理,而Docker Daemon中的daemon实例就是设计用来完成这一任务的实体。
golangLeetcode
2022/08/02
8180
Golang深入浅出之-Go语言中的日志记录:log与logrus库
日志记录是软件开发中不可或缺的一环,它帮助开发者监控应用状态、追踪错误及优化性能。在Go语言中,标准库提供了基本的日志功能,而logrus作为第三方库,则提供了更为丰富和灵活的解决方案。本文将对比介绍log和logrus,揭示常见问题、易错点及其避免策略,并辅以代码示例。
Jimaks
2024/04/30
4960
Docker容器的日志处理
Docker有很多的日志插件,默认使用 json-file,只有使用json-file时,sudo docker logs -f 才可以显示,输入以下命令查看docker日志插件:
张乘辉
2019/06/14
2.6K0
聊聊golang的zap的Sink
Sink接口内嵌了zapcore.WriteSyncer(Write、Sync)、io.Closer(Close)接口;zap.RegisterSink用于注册指定scheme的sink factory,而zap.Open则会解析url来找到对应的sink factory创建对应的sink,即writer。
code4it
2020/12/22
4030
聊聊golang的zap的Sink
containerd源码分析
本文是对containerd v0.2.4的源码分析。 ##Containerd源码流程图 源码接口调用详情 从ctr调用containerd-api ####checkpoint(用于快照,doc
Walton
2018/04/13
3K0
containerd源码分析
Containerd深度剖析-CRI篇
目前我司现网的K8s集群的运行时已经完成从docker到Containerd的切换,有小伙伴对K8s与Containerd调用链涉及的组件不了解,其中Containerd和RunC是什么关系,docker和containerd又有什么区别,以及K8s调用Containerd创建容器又是怎样的流程,最终RunC又是如何创建容器的,诸如此类的疑问。本文就针对K8s使用Containerd作为运行时的整个调用链进行介绍和源码级别的分析。
zouyee
2023/01/31
1.5K0
Containerd深度剖析-CRI篇
openshift源码简析之pod网络配置:Openshift源码分析系列第一篇
大魏:从今天开始,大魏分享将会转载Openshift源码分析系列。文章为红帽合作伙伴书写,大魏已经得到了转载授权。
魏新宇
2018/09/30
1.4K0
openshift源码简析之pod网络配置:Openshift源码分析系列第一篇
相关推荐
docker源码分析-Daemon创建及启动
更多 >
领券
社区富文本编辑器全新改版!诚邀体验~
全新交互,全新视觉,新增快捷键、悬浮工具栏、高亮块等功能并同时优化现有功能,全面提升创作效率和体验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文