Docker 원본 분석의 용기 로그 처리와log-driver 구현

17590 단어 Docker
개요
본고는 docker(1.12.6) 원본의 측면에서 docker daemon이 용기의 로그를 어떻게 수집하고 설정된log-driver를 통해 보내는지 분석하고 예시와 결합하여 좋은 비구름 방에서 이루어진 zmq-loger를 소개한다.본문을 읽으면 자신의 업무 상황에 적합한log-driver를 실현할 수 있습니다.
읽기 준비
이 글은 Golang 코드를 읽고 작성할 수 있는 학우들에게 적합하다.(1) 우선 다음과 같은 몇 가지 키워드를 인지해야 한다. *stdout: 표준 출력, 프로세스 쓰기 데이터의 흐름.*stderr: 오류 출력, 프로세스가 오류 데이터를 쓰는 흐름. *하위 프로세스: 한 프로세스(부 프로세스)에서 만든 프로세스로 부 프로세스의 대부분 속성을 통합하고 부 프로세스에 의해 수호하고 관리할 수 있습니다.
(2) 프로세스 생성 로그의 형식을 알아야 한다. 프로세스 생성 로그는 두 가지 출력 방식이 있는데 하나는 파일에 쓰는 것이다.또 다른 유형은 stdout이나 stderr에 직접 쓰는 것이다. 예를 들어 php의 echopython의 printgolang의 fmt.Println("") 등이다.(3) docker-daemon과 실행 중container의 관계를 아십니까?하나의container는 특수한 프로세스입니다. 이것은 docker daemon이 만들고 시작하는 것입니다. 따라서container는docker daemon의 하위 프로세스입니다.docker daemon이 지키고 관리합니다.따라서 컨테이너의 stdout는 docker daemon에서 얻을 수 있습니다.이 이론을 바탕으로 docker daemon 관련 코드를 분석합니다.
docker-daemon 로그 소스 분석
container 실례 원본 코드
# /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 파라미터를 사용하여 시작할 때 표준 입력이 실행되고, 데몬은 이 파이프를 사용하여 용기에 표준 입력을 쓸 수 있습니다.
![2017011930658image2017-1-18 17-18-38.png](http://7xqmjb.com1.z0.glb.clouddn.com/2017011930658image2017-1-18 17-18-38.png)
우리는 상기 도례를 생각해 보았는데, 만약 당신이라면, 당신은 어떻게 로그 수집과 전송을 실현합니까?
# /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 방법을 얻은 후 호출 파라미터를 구축하는 것은 용기의 일부 정보입니다.그리고 c 방법을 사용해서driver를 되돌려줍니다.driver는 인터페이스 유형입니다. 어떤 방법이 있는지 살펴보겠습니다.
# /daemon/logger/logger.go:61
type Logger interface {
    Log(*Message) error
    Name() string
    Close() error
}

간단한 세 가지 방법도 이해하기 쉽다. Log()은 로그 메시지를driver에 보내고 Close()은 닫기 조작을 한다(서로 다른 실현에 따라).즉, 우리 스스로logdriver를 실현하려면 상기 세 가지 방법을 실현한 다음에logger공장류에 등록하면 된다./daemon/logger/factory.go을 살펴보겠습니다.
두 번째 방법은 로그를 처리하는 것이다. 로그driver를 가져와 Copier을 만드는 것이다. 말 그대로 로그를 복제하는 것이다. 각각 stdout과 stderr에서logger driver로 복제한다.다음은 구체적인 관건적인 실현을 살펴보자.
#/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 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 } } } }

한 줄의 데이터를 읽을 때마다 메시지를 구축하고logdriver의log 방법을 호출하여driver 처리에 보냅니다.
로그driver 등록기/daemon/logger/factory.go에 위치한 원본 코드는 실시간 로그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 방법 유형을 하나의 맵 구조에 추가하고, LogOptValidator을 다른 맵에 추가하면 자물쇠를 채우는 작업에 주의해야 한다.
#/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의 구체적인 코드를 분석합니다.
//    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의 작업 원리를 이해하는 데 편리할 수 있다.오늘 우리는 일지 부분을 분석했다.독자들이 이 부분의 기능에 대해 더욱 분명하게 이해할 수 있기를 바란다.

좋은 웹페이지 즐겨찾기