RSS

MOSN 源码解析 - log 系统

对 MOSN Log系统的源码解析。

本文的目的是分析 MOSN 源码中的Log系统

本文的内容基于 MOSN v0.10.0。

概述

MOSN 日志系统分为日志Metric两大部分,其中日志主要包括errorlogaccesslogMetrics主要包括console数据prometheus数据

日志

errorlog

errorlog 主要是用来记录MOSN运行时候的日志信息,配置结构:

type ServerConfig struct {
......
	DefaultLogPath  string `json:"default_log_path,omitempty"`
	DefaultLogLevel string `json:"default_log_level,omitempty"`
	GlobalLogRoller string `json:"global_log_roller,omitempty"`
......
}

初始化 errorlog 包括两个对象StartLoggerDefaultLogger

  • StartLogger 主要用来记录 mosn 启动的日志信息,日志级别是 INFO
  • DefaultLogger 主要是用来记录MOSN启动之后的运行日志信息,默认和 StartLogger 一样,可以通过配置文件覆盖

代码如下:

func init() {
	......
	// use console as start logger
	StartLogger, _ = GetOrCreateDefaultErrorLogger("", log.INFO) // 默认INFO
	// default as start before Init
	log.DefaultLogger = StartLogger
	DefaultLogger = log.DefaultLogger
	// default proxy logger for test, override after config parsed
	log.DefaultContextLogger, _ = CreateDefaultContextLogger("", log.INFO)
	......
}

......

func InitDefaultLogger(output string, level log.Level) (err error) {
	// 使用配置文件来覆盖默认配置
	DefaultLogger, err = GetOrCreateDefaultErrorLogger(output, level)
	......
}

accesslog

accesslog 主要用来记录上下游请求的数据信息,配置结构:

type AccessLog struct {
	Path   string `json:"log_path,omitempty"`
	Format string `json:"log_format,omitempty"`
}

每个配置文件下面 servers->listener->access_logs,具体配置示例如下:

{
	"servers": [
		{
			"mosn_server_name": "mosn_server_1",
			......
			"listeners": [
				{
					"name": "ingress_sofa",
					......
					"log_path": "./logs/ingress.log",
					"log_level": "DEBUG",
					"access_logs": [
						{
							"log_path": "./logs/access_ingress.log",
							"log_format": "%start_time% %request_received_duration% %response_received_duration% %bytes_sent% %bytes_received% %protocol% %response_code% %duration% %response_flag% %response_code% %upstream_local_address% %downstream_local_address% %downstream_remote_address% %upstream_host%"
						}
					]
				}
			]
		}
	]
}

accesslog 实现如下接口:

AccessLog interface {
    // Log write the access info.
    Log(ctx context.Context, reqHeaders HeaderMap, respHeaders HeaderMap, requestInfo RequestInfo)
}

调用 Log 记录日志的时候,通过使用 变量机制 来填充log_format里面的变量,相关信息保存在 ctx 里面。用于保存变量信息的 entries 通过 NewAccessLog 初始化的时候,调用 parseFormat 方法来初始化的,参考相关代码

log 的具体实现

log 的具体实现已经分离到了 mosn/pkg/log 下面,errorlogaccesslog 的具体实现都是通过 log.GetOrCreateLogger 来初始化的。当 roller 为空的时候使用默认的 defaultRoller,默认每天轮转。

defaultRoller = Roller{MaxTime: defaultRotateTime}
......
defaultRotateTime = 24 * 60 * 60

start

根据不同的输出方式,初始化不同的 io.Writer 对象, 详情

type io.Writer
”“, “stderr”, “/dev/stderr” os.Stderr
“stdout”, “/dev/stdout” os.Stdout
“syslog” gsyslog本地对象
其他 gsyslog远程对象

创建好 log 对象之后,通过 loggers 保存起来,避免创建多个对象,loggers 是一个 sync.Map对象,是 golang1.9 之后加入的一个新的线程安全的 map

start 启动之后会 创建一个一直循环读取的协程 handler

handler

相关代码

在初始化的时候,创建了一个 500 大小的 chan writeBufferChan,并且在 handler 里面处理需要记录的日志、重命名的事件、关闭的事件。

lg := &Logger{
	output:          output,
	roller:          roller,
	writeBufferChan: make(chan buffer.IoBuffer, 500),
	reopenChan:      make(chan struct{}),
	closeChan:       make(chan struct{}),
	// writer and create will be setted in start()
}

for {
	select {
	case <-l.reopenChan:
	......
	case <-l.closeChan:
	......
	case buf = <-l.writeBufferChan:
	......
	runtime.Gosched()
}

reopenChan

通过重命名文件之后,重新调用 start 方法创建新文件,主要使用在文件轮转的时候。os.Stdout os.Stderr 不支持操作,会报错。

closeChan

把当前 writeBufferChan 需要写入的数据写入到对象中,然后退出当前协程。

writeBufferChan

for i := 0; i < 20; i++ {
	select {
	case b := <-l.writeBufferChan:
		buf.Write(b.Bytes())
		buffer.PutIoBuffer(b)
	default:
		break
	}
}
buf.WriteTo(l)
buffer.PutIoBuffer(buf)

当收到第一次写数据的时候不是立刻写入数据到 log 对象,而是在等待 20 次读取信息,一起写入到对 log 象中,在大量写日志的时候不会导致调用太频繁。如频繁写入文件、频繁调用写日志接口,相反,这会增加内存分配,最好的其实是使用 writev,但是 go runtimeio 库没有这个实现。可以采用 plugin 机制 来接管日志的打印,减少 io 卡顿对 go runtime 的调度影响

*当一次循环处理完之后,会调用 runtime.Gosched() 主动让出当前协程的 cpu 资源

Metrics

Metrics 是一种规范的度量,分为如下类型,摘抄至 METRIC TYPES

  • Gauges: 代表可以任意上下波动的单个数值,通常用来表示测量值。比如内存,cpu,磁盘等信息。
  • Counters: 累计度量,代表单调递增的计数器,只有在重启或者重置的时候数量为 0,其他时候一般不使用减少。可以用来表示请求的数量。
  • Histograms: 直方图,对观察值(通常是请求持续时间或返回大小之类的数据)进行采样,并将其计数放到对应的配置桶中,也提供所有观测值总和信息。
  • Summary: 类似于直方图,摘要采样的观测结果,可以计算滑动时间窗口内的可配置分位数。

主要代码在 pkg/metrics 下面包括 pkg/metrics/sinkpkg/metrics/shm

sink

pkg/metrics/sink 包含 consoleprometheus,两者都实现了 types.MetricsSink 接口。prometheus 是通过工厂方法 注册 进去使用的;console 是通过直接调用 console.NewConsoleSink() 来使用的。

prometheus

主要是通过 prometheusmetrics 统计请求的信息,配置文件示例:

{
	"metrics": {
		......
		"sinks": [
			{
				"type": "prometheus",
				"config": {
					"port": 34903
				}
			}
		]
	}
}

其中 type 目前只支持 prometheus

通过 prometheus 库 提供的 http 能力,使用配置信息启动一个 http 服务,把 Metrics 信息通过 http://host:port/metrics 的方式供prometheus收集或展示。

console

主要用于 admin api/api/v1/stats 展示。所以必须配置 admin 相关信息,示例:

{
  "admin": {
    "address": {
      "socket_address": {
        "address": "0.0.0.0",
        "port_value": 34901
      }
    }
  }
}

如果不配置会打印 no admin config, no admin api served 告警信息,参考

admin api 中还包括如下接口

  • /api/v1/config_dump
  • /api/v1/stats
  • /api/v1/update_loglevel
  • /api/v1/enable_log
  • /api/v1/disbale_log
  • /api/v1/states
  • /api/v1/plugin
  • /

其中 update_loglevel 用于更新 errorlog 日志的输出级别,enable_logdisbale_log 用于启用/禁用 errorlog 的输出

shm

pkg/metrics/shm 主要是通过 mmap 将一个文件或者其它对象映射进内存,让多个进程共用,可以让 MOSN 在热升级的过程中 metrics 数据不会出现 "断崖",关于 shm 的分析内容可以参考 共享内存模型

  • 不鼓励在 Go 里面使用共享内存,除非你有明确的使用场景

总结

通过分析 MOSN 源码的 log系统 模块,不单单是了解了日志部分,从配置、启动流程,到上下游请求都有所涉及。学习了很多,希望 MOSN 越来越强大。