目录
- 前言
dlog
的用途- 对
dlog
的一些非功能性需求 - 碰到问题及解决方案
- 何时使用
panic
,何时使用return error
- 如何实现一个
logger
只能接收对应类型的data log
- 如何实现批量发送
data log
- 如何实现对
Logger.Log
方法的调用超时机制 - 如何在
logger
没有收到新msg
情况下,保证buf
中的数据依然会定期发送给AWS Kinesis - 如何向程序外部暴露运行指标
- 如何在单元测试中实现
Setup
和TearDown
- 如何实现
kinesisMock
- 如何模拟AWS Kinesis响应慢或者不可用
- 提交到代码库中的测试代码是否可以保留
log.Print
- 何时使用
- 踩过的一些坑
- 未来可以优化的地方
- 参考
前言
本文记录了前段时间我和王益使用Go语言合作开发一个log组件dlog的过程中学到的一些知识。在整个合作开发的过程中,王益严谨认真的态度,对开发质量的严格要求,给我留下了极其深刻的印象。能够和王益这样的顶级工程师切磋技艺,对我学习Go语言帮助非常大。也谨以此文表达对王益的感谢。
注:本文假设读者已经对Go语法已经有基本了解。
dlog
的用途
首先引用项目readme文档的第一段文字介绍一下dlog
的用途:
dlog is a Go package for distributed structure logging using Amazon AWS Kinesis/Firehose.
更多介绍和设计请阅读readme文档
dlog
主要是用来记录程序的data log
的这样一个Golang package,那什么是data log
?这里先简要解释一下。一般程序运行过程中主要产生两类日志:
status log
:主要用于帮助调试、定位程序Bug、或者找到性能瓶颈,比如方法调用日志、错误日志、方法执行时间日志等data log
:主要用于记录用户行为,收集的data log
用于后期的个性化搜索、智能推荐等,比如搜索行为、点击行为等
对dlog
的一些非功能性需求
- 每一种类型的
data log
对应一种logger
,一个logger
只能记录对应类型的data log
dlog
内部发生的错误,不能影响调用的程序代码的执行- 应考虑到AWS Kinesis服务响应慢或者不可用的场景(暂未实现)
- 程序代码中通过调用
dlog
的方法记录data log
,dlog
的方法不能阻塞调用的程序代码的执行(这一点dlog
暂时未满足要求,需要后期改进) - AWS Kinesis提供两个API接收数据,一个是PutRecord, 另一个是PutRecords,为了减少对Kinesis的调用次数,采用后者批量发送
data log
PutRecords
对一次调用的record
数量限制是500
,每个record
大小必须小于等于1MB,整个request
的大小必须小于等于5MB- 每一个Kinesis Stream能够承受的最大TPS和写数据量,与这个stream拥有的shard的数量有关。一个shard支持最大TPS是
1000 records per second
, 写数据量是1MB per second
- 通过单元测试保证功能正确性
碰到问题及解决方案
何时使用panic
,何时使用return error
先看看panic
和return error
的执行机制。
panic
的执行机制
panic
会中断当前goroutine
的执行,如果不对panic
的错误进行recover
,那么整个进程都会崩溃。
1 | package main |
执行上面代码请点击这里
可以通过recover
捕捉当前goroutine
中panic
的错误并进行错误处理,整个进程的正常运行不受影响。
1 | package main |
执行上面代码请点击这里
我们可以发现Go语言中的panic
、recover
机制,和Java、.NET中的throw
、try...catch
机制非常类似。
return error
的执行机制
return error
是利用Go语言函数的多值返回的特性,通过函数的其中一个返回值(一般是第一个或者最后一个),向caller
返回函数执行过程中产生的异常,其它值返回执行结果。
这种方式的问题,主要在于:如果函数调用层次比较多,每一层函数都通过return error
方式返回错误,都需要处理被调用函数的return error
,增加代码复杂度。对于无法恢复的错误也没有必要一层一层往上抛,直接panic/recover
更加简洁。
1 | package main |
执行上面代码请点击这里
dlog
错误处理原则
使用panic
还是return error
的方式处理错误,要区分不同的场景。重要是不论使用panic
还是return error
,都需要符合架构上更高层面错误处理需求。
dlog
是一个日志记录package
,暴露给其它程序调用的方法如下:
func NewLogger(example interface{}, opts *Options) (*Logger, error)
func (l *Logger) Log(msg interface{}) error
这两个方法的使用场景并不一样,错误处理原则也不完全一致:
NewLogger
方法一般是在程序初始化的时候调用,用于创建记录程序运行过程中产生的data log的记录器。通过NewLogger
创建一个logger
的时候,如果传入参数不正确,使用panic
方式,在上层调用程序不处理错误情况下会导致程序崩溃,所以使用return error
方式向caller
报告错误。大多数Golang package也是按此原则处理。- 上层程序调用
logger.Log
时,如果Log
方法内部发生的错误,不能影响调用的代码的执行,所以这里绝对不能用panic
方式抛出错误。日志记录是辅助功能,如果日志记录行为失败,导致业务逻辑代码执行不下去,估计负责业务逻辑开发的工程师会和你拼命。logger.Log
可以使用return error
方式返回msg
校验类的错误logger.Log
发送日志采用的是异步批量方式向AWS Kinesis发送数据,向AWS Kinesis发送数据相关的错误无法通过panic
或者return error
方式直接报告给调用程序。最好的方式是允许调用程序向logger
注册发送失败处理的handler
,出现发送失败错误时执行handler
逻辑。(暂未实现)
如何实现一个logger
只能接收对应类型的data log
要实现一个logger
只能接收对应类型的data log
,主要思路如下:
Logger
的定义中通过属性msgType reflect.Type
记住能够接受的消息类型- 通过
NewLogger
方法创建logger
的时候,指定logger
可以接受的消息类型 Log
方法中首先校验msg
的类型是否是创建logger
时指定的类型
以下是相关代码:
1 | // msgType保存Logger能够接受的消息类型 |
Log
方法中为什么要用AssignableTo
,而不是直接判断两个类型相等。其实都可以,在msg
是struct
情况下,AssignableTo
返回True
意味着两个类型相等。参考下面的例子:
1 | package main |
执行上面代码请点击这里
如何实现批量发送data log
要实现批量发送,首先我们可以想到应该要有个buffer
用来收集一定数量的的message
,等待buffer
中的数据积累到一定程度后,一次性发送给AWS Kinesis。设计buffer
结构不难,难点在于如何解决多线程(goroutine)并发读写buffer
的问题,主要的解决方案有两种:
- 基于锁机制实现对
buffer
访问控制 - 基于
channel
实现对buffer
的访问控制
前者对于有Java、.NET等语言的并发编程经验的工程师来说,非常熟悉。而后者则体现了CSP(Communicating Sequential Processes)并发编程模型的优势。
dlog
的Log
方法把收到的msg
写到名字叫buffer
的channel
中,另外一个单独的goroutine
在channel
的另一头收集编码后的日志信息,然后保存到buf := make([][]byte, 0)
中。当buf
中的数据量要达到一次向AWS Kinesis发送的最大量时,调用flush
方法向AWS Kinesis发送数据。由于只有一个goroutine
对buf
进行访问,所以不需要通过锁机制控制对buf
的读写。
具体代码实现:
1 | func NewLogger(example interface{}, opts *Options) (*Logger, error) { |
如何实现对Logger.Log
方法的调用超时机制
如果一个IO操作耗时较长,并且调用比较频繁的情况下,不仅会阻塞caller
的执行,还会消耗大量系统资源。我们通常会使用超时机制,避免程序长时间等待或者对系统资源大量占用。
Logger.Log
方法利用Go语言channel
非常简洁的实现了超时机制:
1 | func (l *Logger) Log(msg interface{}) error { |
对比Java、.NET语言中超时机制的实现方法,Go语言的实现简洁的令人发指:
- C#
- Java
如何在logger
没有收到新msg
情况下,保证buf
中的数据依然会定期发送给AWS Kinesis
dlog
在Logger.sync()
方法中通过一个定时器,定期将buf
中数据发送给AWS Kinesis。
1 | func (l *Logger) sync() { |
通过ticker
,dlog
保证了即使没有收到新的msg
的时候,保存在buf
中的数据最长l.SyncPeriod
时间后也会发送给AWS Kinesis。
互联网产品的生产环境的上线,通常的做法是,将现有服务分组,然后交替切流量、升级。如果没有类似的机制,那么在服务程序断掉流量,没有收到新的访问时候,保存在内存中的数据就不会发送出去,升级时就可能导致数据丢失。
如何向程序外部暴露运行指标
Go语言的官方Package expvar
提供一种标准化的接口,允许程序暴露公开访问的变量。expvar
通过HTTP地址/debug/vars
提供访问入口,并以JSON格式展示这些变量。下面是关于expvar
常见用法的一个例子:
1 | package main |
按照如下步骤测试运行效果:
go run expvarexample.go
运行例子代码- 在浏览器中访问
http://localhost:8080/ethan
- 在浏览器中访问
http://localhost:8080/err
- 在浏览器中访问
http://localhost:8080/debug/vars
,得到如下结果:1
2
3
4
5
6
7
8
9
10
11
12
13{
"cmdline": ["/var/folders/jf/65ft181j33j_d75ktgv67bsc0000gn/T/go-build467453980/command-line-arguments/_obj/exe/expvarsample"],
"hits": {
"ethan": 1,
"favicon.ico": 2
},
"memstats": { ... },
"now": "\"2016-04-19 20:17:40\"",
"stats": {
"TotalHit":3,
"ErrorNums":1
}
}
expvarmon是一个帮助查看expvar
暴露运行指标的工具,用法如下:
- 安装:
go get github.com/divan/expvarmon
- 运行:
expvarmon -ports="8080" -vars="hits.ethan,stats.TotalHit,stats.ErrorNums,now"
- 效果如下:
dlog
使用expvar
向程序外部(比如监控程序)暴露运行指标,目前dlog
中定义的运行指标包括:
writtenRecords
: 成功写到AWS Kinesis的msg
数量writtenBatches
: 成功调用AWS Kinesis批量写数据API的次数failedRecords
: 写到AWS Kinesis失败的msg
数量tooBigMesssages
: 编码后体积过大(加上partitionKeySize大于1MB)的msg
数量
未来还需要根据运维的需求对运行指标进行调整,当前的用法也有一些问题,后期需要重构。
如何在单元测试中实现Setup
和TearDown
Go语言提供一种轻量级的单元测试框架(无需第三方工具或者程序包)。通过使用go test
命令和testing
package,可以非常快速的实现单元测试。先借用官方文档中的例子回顾一下Go单元测试框架的用法:
1 | //$GOPATH/src/github.com/user/stringutil/reverse_test.go |
运行测试只需要简单的输入命令:
1 | $ go test github.com/user/stringutil |
很多情况下,要执行单元测试,我们需要依赖一些外部资源,比如已完成初始化数据的数据库、公有云上的一些IaaS服务等。这些依赖资源,我们希望在单元测试执行前,能够自动的被初始化;单元测试完成后,能够自动的被清理。testify/suite package就提供这样的支持。通过testify/suite,你可以构建一个测试集struct
,建立测试集的setup
(初始化)/teardown
(清理)方法,和最终实现测试用例逻辑的方法。而运行测试,仍然只需要一句简单的go test
。
以下是使用testify/suite实现测试集的常见模式:
1 | package suite |
dlog
中为了测试Logger.Log
方法能否正常工作,按照上面的模式编写了相应的测试代码:
1 | package dlog |
注:
- 很多场景下,测试程序自动创建依赖的资源需要运维部门的授权,所以实现前有必要先和运维部门沟通。
- 云环境下,出于安全上的考虑,需要对创建、删除测试资源的账户管理严格管理
- 账户信息不能写在可以公开访问的测试代码、配置文件中
- 只给账户分配必要资源的最小权限
- 为账户能够创建的资源设定配额
如何实现kinesisMock
上一节我们提到在测试执行前初始化依赖资源,现实场景中,并不是任何情况下都能够获得依赖的测试资源,或者测试资源也会出现不可用的情况。通过Mock技术,可以减少测试代码对其它资源(或模块)的依赖。
dlog
的测试代码中,首先定义了一个KinesisInterface
:
1 | type KinesisInterface interface { |
KinesisInterface
包含了dlog
用到的github.com/AdRoll/goamz/kinesis/kinesis的所有方法。因为Go语言interface
实现非侵入式的特点,github.com/AdRoll/goamz/kinesis/kinesis自动实现了KinesisInterface
,我们再定义一个kinesisMock
实现KinesisInterface
:
1 | type kinesisMock struct { |
然后,把业务代码中所有类型kinesis
的变量,替换成KinesisInterface
类型。
1 | type Logger struct { |
测试代码中,在构造Logger
时传入kinesisMock
,而不是真实的kinesis
,这样就做到了“狸猫换太子”。
1 | func TestLoggingToMockKinesis(t *testing.T) { |
如何模拟AWS Kinesis响应慢或者不可用
kinesisMock
完全是我们“虚构”出来的一个kinesis
,在它的基础上,我们完全可以模拟响应慢或者不可用的情况。
上一节中,不知道大家注意到没有,kinesisMock
有个属性叫putRecordLatency
,用来模拟调用PutRecords
方法的延迟时间。
1 | type kinesisMock struct { |
模拟不可用的kinesis
则重新定义了一个brokenKinesisMock
:
1 | type brokenKinesisMock struct { |
kinesisMock
是brokenKinesisMock
的嵌入struct
,brokenKinesisMock
会自动拥有kinesisMock
的所有公开方法,这样也就实现了KinesisInterface
。
提交到代码库中的测试代码是否可以保留log.Print
结论是“不可以”,原因总结如下:
- 测试代码中的
log.Print
,一般用于调试代码,或者在stdout
打印出一些信息帮助判断测试失败原因。不论哪种目的,这样的代码目的都仅仅是为了辅助开发,而不应该出现在最终交付的产品代码中。 go test
命令会在控制台输出失败的测试方法,如果加上-v
标志会打印出所有测试方法的执行结果,log.Print
会影响执行结果的展示效果。团队合作开发,如果每个人都在测试代码中加上自己的log.Print
,那么控制台打印出来的测试结果就没法看了。
踩过的一些坑
- AWS Kinesis API - CreateStream是异步创建Stream,而且耗时10+秒,才能完成一个Stream的创建。开始以为是同步创建,结果执行测试逻辑的时候总是出错。
- github.com/AdRoll/goamz/aws/regions.go中缺少中国区AWS Kinesis的URL地址,调用中国区AWS Kinesis会出错。
- Travis CI会Kill掉执行时间超过1分钟的CI过程,而不是如它文档中介绍的“10分钟”
未来可以优化的地方
- 发送失败的错误事件机制
- 实现Kinesis服务不可用或者响应慢的场景下
dlog
的容错处理
参考
招聘消息
我所在的奥阁门科技有限公司正在招聘后端、运维工程师,想加入的朋友、或者有朋友可以推荐的都可以联系我(ethancai@qq.com)。
后端工程师 / Backend Engineer
职责
- 研讨和设计产品功能特性;
- 设计研发系统后端的一个或多个独立服务(micro-service)模块;
- 设计研发业务运营管理系统;
- Code Review。
要求
- 有良好的编程习惯和代码风格;
- 精通至少一种后台开发语言,包括但不限于Go、Node.js、C++、Python;
- 对RESTful、RPC等架构有深刻理解和运用经验;
- 有丰富的web service、web app开发经验;使用过著名的开源应用框架,并完整阅读过源代码;
- 对Mysql、Redis、MongoDB或同类数据存储技术有丰富的使用经验;
- 有提交代码到著名开源库或创建过开源项目者优先;
- 能熟练查阅英文技术文档;
- 有开放、坦诚的沟通心态,乐于分享;
- 5年以上工作经验,3年以上后台系统开发经验。
高级系统运维工程师 / Senior Ops Engineer
职责
- 负责日常业务系统基础实施(AWS)、网络及各子系统的管理维护。
- 负责设计并部署相关应用平台,并提出平台的实施、运行报告。
- 负责配合开发搭建测试平台,协助开发设计、推行、实施和持续改进。
- 负责相关故障、疑难问题排查处理,编制汇总故障、问题,定期提交汇总报告。
- 负责网络监控和应急反应,以确保网络系统有7*24小时的持续运作能力。
- 负责日常系统维护,及监控,提供IT方面的服务和支持,保证系统的稳定。
要求
- 深入理解Linux/Unix操作系统并能熟练使用,了解Linux系统内核,有相关操作系统调优经验优先;
- 熟悉计算机网络基础知识,了解TCP/IP、HTTP等网络协议;
- 熟悉系统服务的管理和维护,例如:Nginx、DNS服务器、NTP服务等;
- 熟悉一种或者多种脚本语言,例如:Shell、Python、Perl 、Ruby等;
- 熟练掌握Linux管理相关命令行工具,例如:grep、awk、sed、tmux、vim等;
- 对数据库系统(MySQL)运维管理有一定的了解;
- 熟悉常见分布式系统系统架构部署管理,熟悉基础设施管理、并具有较强的故障排查和解决问题的能力;
- 具有 2 年以上中大型互联网系统或亚马逊AWS管理经验者优先;
- 有DevOps经验者优先;
- 学习能力和沟通能力较强,具有良好的团队协作精神;
- 工作中需要胆大心细,具备探索创新精神;
- 具有良好的文档编写能力;
- 具有一定的英文技术文档阅读能力。