zap-Logger의 디자인 사고방식

전언


이전의 일련의 글에서 주로 추적한 것은 코드의 집행 논리와 처리 사고방식을 추적하여 코드를 익히고 이해와 사용을 깊이 있게 하는 것이다.근데 zap이 왜 빨라요?그것은 어떻게 설계되었습니까?코드를 익힌 후에zap의 디자인 사고방식을 돌아보고 이전의 의혹을 풀 수 있는지 살펴보자.

디자인 사고방식


zap README의 Performance 섹션에서는 다음을 설명합니다.
For applications that log in the hot path, reflection-based serialization and 
string formatting are prohibitively expensive — they're CPU-intensive and 
make many small allocations. Put differently, using encoding/json and 
fmt.Fprintf to log tons of interface{}s makes your application slow.

Zap takes a different approach. It includes a reflection-free, zero-allocation 
JSON encoder, and the base Logger strives to avoid serialization overhead 
and allocations wherever possible. By building the high-level SugaredLogger 
on that foundation, zap lets users choose when they need to count every 
allocation and when they'd prefer a more familiar, loosely typed API.

As measured by its own benchmarking suite, not only is zap more performant 
than comparable structured logging packages — it's also faster than the 
standard library. Like all benchmarks, take these with a grain of salt.1

핵심을 짚어 말하자면 다음과 같다.
반사에 기반한 서열화와 문자열 포맷은 대가가 높고, 이런 조작은 대량의 CPU를 차지하며, 많은 작은 메모리를 분배한다.그래서 엔코딩/json과 fmt.Fprintf에서 대량의 인터페이스 로그를 가져오면 프로그램이 느려집니다. (둘 다 반사 처리와 관련이 있습니다.)zap의 사고방식은 가능한 한 이런 서열화 비용과 분배를 피하고 반사에 기반한 인코더를 구성하는 것이다(ReflectType의Filed를 제외하고 ReflectType는 확장용으로만 사용하는 것을 권장한다).

코더


zap의 핵심은 encoder의 실현이다. encoder는log의 서열화/포맷을 책임지고 그 성능이 전체적인 성능을 결정한다.
인코더가 실현하는 주된 사고방식은interface에 대한 직접적인 지원을 포기하고 정확한 유형을 직접 처리하여 표준 라이브러리에 관련된 반사 처리를 피하고 성능을 향상시키는 것이다.

JSON encoder

func (enc *jsonEncoder) EncodeEntry(ent Entry, fields []Field) (*buffer.Buffer, error) {
    final := enc.clone()
    final.buf.AppendByte('{')

    if final.LevelKey != "" {
        final.addKey(final.LevelKey)
        cur := final.buf.Len()
        final.EncodeLevel(ent.Level, final)
        if cur == final.buf.Len() {
            // User-supplied EncodeLevel was a no-op. Fall back to strings to keep
            // output JSON valid.
            final.AppendString(ent.Level.String())
        }
    }
    if final.TimeKey != "" {
        final.AddTime(final.TimeKey, ent.Time)
    }
    if ent.LoggerName != "" && final.NameKey != "" {
        final.addKey(final.NameKey)
        cur := final.buf.Len()
        nameEncoder := final.EncodeName

        // if no name encoder provided, fall back to FullNameEncoder for backwards
        // compatibility
        if nameEncoder == nil {
            nameEncoder = FullNameEncoder
        }

        nameEncoder(ent.LoggerName, final)
        if cur == final.buf.Len() {
            // User-supplied EncodeName was a no-op. Fall back to strings to
            // keep output JSON valid.
            final.AppendString(ent.LoggerName)
        }
    }
    if ent.Caller.Defined && final.CallerKey != "" {
        final.addKey(final.CallerKey)
        cur := final.buf.Len()
        final.EncodeCaller(ent.Caller, final)
        if cur == final.buf.Len() {
            // User-supplied EncodeCaller was a no-op. Fall back to strings to
            // keep output JSON valid.
            final.AppendString(ent.Caller.String())
        }
    }
    if final.MessageKey != "" {
        final.addKey(enc.MessageKey)
        final.AppendString(ent.Message)
    }
    if enc.buf.Len() > 0 {
        final.addElementSeparator()
        final.buf.Write(enc.buf.Bytes())
    }
    addFields(final, fields)
    final.closeOpenNamespaces()
    if ent.Stack != "" && final.StacktraceKey != "" {
        final.AddString(final.StacktraceKey, ent.Stack)
    }
    final.buf.AppendByte('}')
    if final.LineEnding != "" {
        final.buf.AppendString(final.LineEnding)
    } else {
        final.buf.AppendString(DefaultLineEnding)
    }

    ret := final.buf
    putJSONEncoder(final)
    return ret, nil
}

JSON encoder의 입구 소스입니다.이 소스에서 알 수 있는 내용은 다음과 같습니다.

1. 매개 변수 및 데이터 직접 제한 유형, 반사 확인 후 처리 없이 성능 향상

func (enc *jsonEncoder) addKey(key string) {
    enc.addElementSeparator()
    enc.buf.AppendByte('"')
    enc.safeAddString(key)
    enc.buf.AppendByte('"')
    enc.buf.AppendByte(':')
    if enc.spaced {
        enc.buf.AppendByte(' ')
    }
}

func (enc *jsonEncoder) AppendString(val string) {
    enc.addElementSeparator()
    enc.buf.AppendByte('"')
    enc.safeAddString(val)
    enc.buf.AppendByte('"')
}

대응하는 매개 변수와 유형에 따라 직접 처리하여 반사 등 과정이 필요 없고 성능이 높습니다.

2. 서열화는 일정한 순서에 따라 진행된다


소스 코드에서 알 수 있는 순서는 다음과 같습니다.
LevelKey->TimeKey->NameKey->CallerKey->MessageKey->[]Field->StacktraceKey->LineEnding
주의: 대응하는 키가 비어 있을 때 서열화에 참여하지 않습니다.
MessageKey에 대응하는 내용과 []Field를 제외한 다른 매개 변수는 Entry에 한정되어 있으며 logger를 만들 때 지정됩니다.

3. 필드는 맨 윗부분의 json 서열화에 참여하고 Field는 맞춤형 빠른 형식 처리가 있지만 반사 형식인 ReflectType을 사용하지 마십시오.

func (f Field) AddTo(enc ObjectEncoder) {
    var err error
    switch f.Type {
    case ArrayMarshalerType:
        err = enc.AddArray(f.Key, f.Interface.(ArrayMarshaler))
    case ObjectMarshalerType:
        err = enc.AddObject(f.Key, f.Interface.(ObjectMarshaler))
    case BinaryType:
        enc.AddBinary(f.Key, f.Interface.([]byte))
    case BoolType:
        enc.AddBool(f.Key, f.Integer == 1)
    case ByteStringType:
        enc.AddByteString(f.Key, f.Interface.([]byte))
    case Complex128Type:
        enc.AddComplex128(f.Key, f.Interface.(complex128))
    case Complex64Type:
        enc.AddComplex64(f.Key, f.Interface.(complex64))
    case DurationType:
        enc.AddDuration(f.Key, time.Duration(f.Integer))
    case Float64Type:
        enc.AddFloat64(f.Key, math.Float64frombits(uint64(f.Integer)))
    case Float32Type:
        enc.AddFloat32(f.Key, math.Float32frombits(uint32(f.Integer)))
    case Int64Type:
        enc.AddInt64(f.Key, f.Integer)
    case Int32Type:
        enc.AddInt32(f.Key, int32(f.Integer))
    case Int16Type:
        enc.AddInt16(f.Key, int16(f.Integer))
    case Int8Type:
        enc.AddInt8(f.Key, int8(f.Integer))
    case StringType:
        enc.AddString(f.Key, f.String)
    case TimeType:
        if f.Interface != nil {
            enc.AddTime(f.Key, time.Unix(0, f.Integer).In(f.Interface.(*time.Location)))
        } else {
            // Fall back to UTC if location is nil.
            enc.AddTime(f.Key, time.Unix(0, f.Integer))
        }
    case Uint64Type:
        enc.AddUint64(f.Key, uint64(f.Integer))
    case Uint32Type:
        enc.AddUint32(f.Key, uint32(f.Integer))
    case Uint16Type:
        enc.AddUint16(f.Key, uint16(f.Integer))
    case Uint8Type:
        enc.AddUint8(f.Key, uint8(f.Integer))
    case UintptrType:
        enc.AddUintptr(f.Key, uintptr(f.Integer))
    case ReflectType:
        err = enc.AddReflected(f.Key, f.Interface)
    case NamespaceType:
        enc.OpenNamespace(f.Key)
    case StringerType:
        err = encodeStringer(f.Key, f.Interface, enc)
    case ErrorType:
        encodeError(f.Key, f.Interface.(error), enc)
    case SkipType:
        break
    default:
        panic(fmt.Sprintf("unknown field type: %v", f))
    }

    if err != nil {
        enc.AddString(fmt.Sprintf("%sError", f.Key), err.Error())
    }
}

func (enc *jsonEncoder) AppendString(val string) {
    enc.addElementSeparator()
    enc.buf.AppendByte('"')
    enc.safeAddString(val)
    enc.buf.AppendByte('"')
}

type jsonEncoder struct {
    *EncoderConfig
    buf            *buffer.Buffer
    spaced         bool // include spaces after colons and commas
    openNamespaces int

    // for encoding generic values by reflection
    reflectBuf *buffer.Buffer
    reflectEnc *json.Encoder
}

type ObjectEncoder interface {
    ...
    // AddReflected uses reflection to serialize arbitrary objects, so it's slow
    // and allocation-heavy.
    AddReflected(key string, value interface{}) error
    ...
}

func (enc *jsonEncoder) AddReflected(key string, obj interface{}) error {
    enc.resetReflectBuf()
    err := enc.reflectEnc.Encode(obj)
    if err != nil {
        return err
    }
    enc.reflectBuf.TrimNewline()
    enc.addKey(key)
    _, err = enc.buf.Write(enc.reflectBuf.Bytes())
    return err
}

zap은 Field에 대응하는 유형에 상응하는 봉인 처리를 하고 ReflectType을 제외하고 정보를 캐시에 직접 연결할 수 있다.ReflectType은 표준 라이브러리의 서열화 과정을 호출하여 반사 사용과 관련이 있기 때문에 zap을 사용하는 것은 불필요한 상황에서 ReflectType 유형의 Field를 사용하지 말라고 강력하게 권장한다. 이것은 zap의 장점을 발휘할 수 없고 오히려 처리 과정이 많아서 성능을 더욱 향상시킬 수 있다.

4. 사용자 정의 하위 Encoder에 작업이 없을 경우 JSON이 유효하도록 기본 작업이 사용됩니다.

if final.LevelKey != "" {
        final.addKey(final.LevelKey)
        cur := final.buf.Len()
        final.EncodeLevel(ent.Level, final)
        if cur == final.buf.Len() {
            // User-supplied EncodeLevel was a no-op. Fall back to strings to keep
            // output JSON valid.
            final.AppendString(ent.Level.String())
        }
    }

CONSOLE encoder

func (c consoleEncoder) EncodeEntry(ent Entry, fields []Field) (*buffer.Buffer, error) {
    line := bufferpool.Get()

    // We don't want the entry's metadata to be quoted and escaped (if it's
    // encoded as strings), which means that we can't use the JSON encoder. The
    // simplest option is to use the memory encoder and fmt.Fprint.
    //
    // If this ever becomes a performance bottleneck, we can implement
    // ArrayEncoder for our plain-text format.
    arr := getSliceEncoder()
    if c.TimeKey != "" && c.EncodeTime != nil {
        c.EncodeTime(ent.Time, arr)
    }
    if c.LevelKey != "" && c.EncodeLevel != nil {
        c.EncodeLevel(ent.Level, arr)
    }
    if ent.LoggerName != "" && c.NameKey != "" {
        nameEncoder := c.EncodeName

        if nameEncoder == nil {
            // Fall back to FullNameEncoder for backward compatibility.
            nameEncoder = FullNameEncoder
        }

        nameEncoder(ent.LoggerName, arr)
    }
    if ent.Caller.Defined && c.CallerKey != "" && c.EncodeCaller != nil {
        c.EncodeCaller(ent.Caller, arr)
    }
    for i := range arr.elems {
        if i > 0 {
            line.AppendByte('\t')
        }
        fmt.Fprint(line, arr.elems[i])
    }
    putSliceEncoder(arr)

    // Add the message itself.
    if c.MessageKey != "" {
        c.addTabIfNecessary(line)
        line.AppendString(ent.Message)
    }

    // Add any structured context.
    c.writeContext(line, fields)

    // If there's no stacktrace key, honor that; this allows users to force
    // single-line output.
    if ent.Stack != "" && c.StacktraceKey != "" {
        line.AppendByte('
'
) line.AppendString(ent.Stack) } if c.LineEnding != "" { line.AppendString(c.LineEnding) } else { line.AppendString(DefaultLineEnding) } return line, nil } func (c consoleEncoder) writeContext(line *buffer.Buffer, extra []Field) { context := c.jsonEncoder.Clone().(*jsonEncoder) defer context.buf.Free() addFields(context, extra) context.closeOpenNamespaces() if context.buf.Len() == 0 { return } c.addTabIfNecessary(line) line.AppendByte('{') line.Write(context.buf.Bytes()) line.AppendByte('}') }

CONSOLE encoder와 JSON encoder는 키를 처리하는 순서가 일치하고 문자열을 직접 연결하여 실현하는 고성능이다.다음 사항에 유의하십시오.

1. LevelKey 등 Key는 포맷에 참여하지 않고 그 값이 포맷에 참여하는지 여부만 결정합니다


2. 사용자 정의 하위 Encoder는 작동하지 않을 수 있습니다. 물론 결과에도 표시되지 않습니다.


3. 개별 결과 간에\t 분리


4.fields에서 json 형식을 서열화한 후 전체 형식으로 포맷합니다


encoder의 처리 과정을 본 후 Config에서 EncoderConfig의 디자인을 돌아보십시오.

EncoderConfig

type EncoderConfig struct {
    // Set the keys used for each log entry. If any key is empty, that portion
    // of the entry is omitted.
    MessageKey    string `json:"messageKey" yaml:"messageKey"`
    LevelKey      string `json:"levelKey" yaml:"levelKey"`
    TimeKey       string `json:"timeKey" yaml:"timeKey"`
    NameKey       string `json:"nameKey" yaml:"nameKey"`
    CallerKey     string `json:"callerKey" yaml:"callerKey"`
    StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"`
    LineEnding    string `json:"lineEnding" yaml:"lineEnding"`
    // Configure the primitive representations of common complex types. For
    // example, some users may want all time.Times serialized as floating-point
    // seconds since epoch, while others may prefer ISO8601 strings.
    EncodeLevel    LevelEncoder    `json:"levelEncoder" yaml:"levelEncoder"`
    EncodeTime     TimeEncoder     `json:"timeEncoder" yaml:"timeEncoder"`
    EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
    EncodeCaller   CallerEncoder   `json:"callerEncoder" yaml:"callerEncoder"`
    // Unlike the other primitive type encoders, EncodeName is optional. The
    // zero value falls back to FullNameEncoder.
    EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
}

EncoderConfig는 로그에서 자주 사용하는 Level, Time, Msg, Caller 등의 정보를 키로 한정하고 대응하는 사용자 정의 이름과 encoder를 제공한다. 모두 로그의 키, 저장 데이터 유형 등 정보를 한정함으로써 인코딩할 때 데이터 유형에 따라 데이터를 직접 처리할 수 있고 반사 없이 빠르고 효율적이다.규범을 한정하고 파라미터 남용을 줄이는 경우도 있다.

총결산


Logger는 Config를 통해 log에서 자주 사용하는 매개 변수의 유형과 키를 한정하고 이 키를 통해 json의 서열화를 신속하게 할 수 있습니다.전체적으로 보면 Logger가 일정한 자유성을 희생하고 충분한 성능을 바꾸는 것처럼 보이지만 log의 포맷과 Filed의 확장을 고려하면 절대 다수의 상황에서 수요를 충족시킬 수 있고 성능의 향상은 큰 장점을 가져올 수 있다.물론 zap은 더 큰 유연성을 만족시키기 위해 Sugared Logger를 제공했지만 성능은 Logger보다 조금 떨어지기 때문에 뒷부분에서 다시 논의할 것이다.

좋은 웹페이지 즐겨찾기