MOSN 源码解析 - log 系统
本文的目的是分析 MOSN 源码中的Log系统
。
本文的内容基于 MOSN v0.10.0。
概述
MOSN 日志系统分为日志
和Metric
两大部分,其中日志
主要包括errorlog
和accesslog
,Metrics
主要包括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 包括两个对象StartLogger
和DefaultLogger
- 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 下面,errorlog
和 accesslog
的具体实现都是通过 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 runtime
的 io
库没有这个实现。可以采用 plugin 机制 来接管日志的打印,减少 io
卡顿对 go runtime
的调度影响
*当一次循环处理完之后,会调用 runtime.Gosched()
主动让出当前协程的 cpu
资源
Metrics
Metrics
是一种规范的度量,分为如下类型,摘抄至 METRIC TYPES
- Gauges: 代表可以任意上下波动的单个数值,通常用来表示测量值。比如内存,cpu,磁盘等信息。
- Counters: 累计度量,代表单调递增的计数器,只有在重启或者重置的时候数量为 0,其他时候一般不使用减少。可以用来表示请求的数量。
- Histograms: 直方图,对观察值(通常是请求持续时间或返回大小之类的数据)进行采样,并将其计数放到对应的配置桶中,也提供所有观测值总和信息。
- Summary: 类似于直方图,摘要采样的观测结果,可以计算滑动时间窗口内的可配置分位数。
主要代码在 pkg/metrics
下面包括 pkg/metrics/sink
和 pkg/metrics/shm
。
sink
pkg/metrics/sink
包含 console
和 prometheus
,两者都实现了 types.MetricsSink 接口。prometheus
是通过工厂方法 注册 进去使用的;console
是通过直接调用 console.NewConsoleSink()
来使用的。
prometheus
主要是通过 prometheus
的 metrics
统计请求的信息,配置文件示例:
{
"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_log
和 disbale_log
用于启用/禁用 errorlog
的输出
shm
pkg/metrics/shm
主要是通过 mmap
将一个文件或者其它对象映射进内存,让多个进程共用,可以让 MOSN
在热升级的过程中 metrics
数据不会出现 "断崖"
,关于 shm
的分析内容可以参考 共享内存模型。
- 不鼓励在 Go 里面使用共享内存,除非你有明确的使用场景
总结
通过分析 MOSN
源码的 log系统
模块,不单单是了解了日志部分,从配置、启动流程,到上下游请求都有所涉及。学习了很多,希望 MOSN
越来越强大。
- MOSN 源码 v0.10.0
- pkg 源码 commit 1e41847