共计 6579 个字符,预计需要花费 17 分钟才能阅读完成。
需要
日志对于线上排查问题是十分重要的,很多问题其实是很偶现的,同样的零碎版本,同样的设施,可能就是用户的复现,而开发通过雷同的操作和设施就是不复现。然而这个问题也不能始终不解决,所以能够通过日志的形式排查问题。可能是后盾导致的问题,也可能是客户端逻辑问题,在关键点记录日志能够疾速定位问题。
假如咱们的用户量是一百万日活,其中有 1%
的用户应用呈现问题,即便这个问题并不是解体,就是业务上或播放呈现问题。那这部分用户就是一万的用户,一万的用户数量是很宏大的。而且大多数用户在遇到问题后,并不会被动去分割客服,而是转到其余平台上。
尽管咱们当初有 Kibana
网络监控,然而只能排查网络申请是否有问题,用户是否在某个工夫申请了服务器,服务器下发的数据是否正确,然而如果定位业务逻辑的问题,还是要客户端记录日志。
现状
咱们我的项目中之前有日志零碎,然而从业务和技术的角度来说,存在两个问题。现有的日志零碎从业务层角度,须要用户手动导出并发给客服,对用户有不必要的打搅。而且大多数用户并不会许可客服的申请,不会导出日志给客服。从技术的角度,现有的日志零碎代码很乱,而且性能很差,导致线上不敢继续记录日志,会导致播放器卡顿。
而且现有的日志零碎仅限于 debug
环境开启被动记录,线上是不开启的,线上出问题后须要用户手动关上,并且记录时长只有三分钟。正是因为当初存在的诸多问题,所以大家对日志的应用并不是很踊跃,线上排查问题就比拟艰难。
方案设计
思路
正是针对当初存在的问题,我筹备做一套新的日志零碎,来代替现有的日志零碎。新的日志零碎定位很简略,就是纯正的记录业务日志。Crash
、埋点这些,咱们都不记录在外面,这些能够当做当前的扩大。日志零碎就记录三种日志,业务日志、网络日志、播放器日志。
日志收集咱们采纳的被动回捞策略,在日志平台上填写用户的 uid
,通过uid
对指定设施下发回捞指令,回捞指令通过长连贯的形式下发。客户端收到回捞指令后,依据筛选条件对日志进行筛选,随后以天为单位写入到不同的文件中,压缩后上传到后端。
在日志平台能够依据指定的条件进行搜寻,并下载文件查看日志。为了便于开发者查看日志,从数据库取出的日志都会写成 .txt
模式,并上传此文件。
API 设计
对于调用的 API
设计,应该足够简略,业务层应用时就像调用 NSLog
一样。所以对于 API
的设计方案,我采纳的是宏定义的形式,调用办法和 NSLog
一样,调用很简略。
#if DEBUG
#define SVLogDebug(frmt, ...) [[SVLogManager sharedInstance] mobileLogContent:(frmt), ##__VA_ARGS__]
#else
#define SVLogDebug(frmt, ...) NSLog(frmt, ...)
#endif
日志总共分为三种类型,业务日志、播放器日志、网络日志,对于三种日志别离对应着不同的宏定义。不同的宏定义,写入数据库的类型也不一样,能够用户日志筛选。
- 业务日志:
SVLogDebug
。 - 播放器日志:
SVLogDebugPlayer
。 - 网络日子:
SVLogDebugQUIC
。
淘汰策略
不光是要往数据库里写,还须要思考淘汰策略。淘汰策略须要均衡记录的日志数量,以及时效性的问题,日志数量尽量够排查问题,并且还不会占用过多的磁盘空间。所以,在日志上传之后会将已上传日志删除掉,除此之外日志淘汰策略有以下两种。
- 日志最多只保留三天,三天以前的日志都会被删掉。在利用启动后进行查看,并后盾线程执行这个过程。
- 日志减少一个最大阈值,超过阈值的日志局部,以工夫为序,从前往后删除。咱们定义的阈值大小为
200MB
,个别不会超过这个大小。
记录根底信息
在排查问题时一些要害信息也很重要,例如用户过后的网络环境,以及一些配置项,这些因素对代码的执行都会有一些影响。对于这个问题,咱们也会记录一些用户的配置信息及网络环境,不便排查问题,但不会波及用户经纬度等隐衷信息。
数据库
旧计划
之前的日志计划是通过 DDLog
实现的,这种计划有很重大的性能问题。其写入日志的形式,是通过 NSData
来实现的,在沙盒创立一个 txt
文件,通过一个句柄来向本地写文件,每次写完之后把句柄 seek
到文件开端,下次间接在文件开端持续写入日志。日志是以 NSData
的形式进行解决的,相当于始终在频繁的进行本地文件写入操作,还要在内存中维持一个或者多个句柄对象。
这种形式还有个问题在于,因为是间接进行二进制写入,在本地存储的是 txt
文件。这种形式是没有方法做筛选之类的操作的,扩展性很差,所以新的日志计划咱们打算采纳数据库来实现。
计划抉择
我比照了一下 iOS
平台支流的数据库,发现 WCDB
是综合性能最好的,某些方面比 FMDB
都要好,而且因为是 C++
实现的代码,所以从代码执行的层面来讲,也不会有 OC
的音讯发送和转发的额定耗费。
依据 WCDB
官网的统计数据,WCDB
和 FMDB
进行比照,FMDB
是对 SQLite
进行简略封装的框架,和间接用 SQLite
差异不是很大。而 WCDB
则在 sqlcipher
的根底上进行的深度优化,综合性能比 FMDB
要高,以下是性能比照,数据来自 WCDB
官网文档。
单次读操作 WCDB
要比 FMDB
慢5%
左右,在 for
循环内始终读。
单次写操作 WCDB
要比 FMDB
快28%
,一个 for
循环始终写。
批量写操作比拟显著,WCDB
要比 FMDB
快180%
,一个批量工作写入一批数据。
从数据能够看出,WCDB
在写操作这块性能要比 FMDB
要快很多,而本地日志最频繁的就是写操作,所以这正好合乎咱们的需要,所以抉择 WCDB
作为新的数据库计划是最合适的。而且我的项目中曝光模块曾经用过WCDB
,证实这个计划是可行并且性能很好的。
表设计
咱们数据库的表设计很简略,就上面四个字段,不同类型的日志用 type
做辨别。如果想减少新的日志类型,也能够在我的项目中扩大。因为应用的是数据库,所以扩展性很好。
- index:主键,用来做索引。
- content:日志内容,记录日志内容。
- createTime:创立工夫,日志入库的工夫。
- type:日志类型,用来辨别三种类型。
数据库优化
咱们是视频类利用,会波及播放、下载、上传等次要性能,这些性能都会大量记录日志,来不便排查线上问题。所以,防止数据库太大就成了我在设计日志零碎时,比拟看重的一点。
依据日志规模,我对播放、下载、上传三个模块进行了大量测试,播放一天两夜、下载 40 集电视剧、上传多个高清视频,累计记录的日志数量大略五万多条。我发现数据库文件夹曾经到 200MB+
的大小,这个大小曾经是比拟大的,所以须要对数据库进行优化。
我察看了一下数据库文件夹,有三个文件,db
、shm
、wal
,次要是数据库的日志文件太大,db
文件反而并不大。所以须要调用 sqlite3_wal_checkpoint
将wal
内容写入到数据库中,这样能够缩小 wal
和shm
文件的大小。但 WCDB
并没有提供间接 checkpoint
的办法,所以通过调研发现,执行 database
的敞开操作时,能够触发checkpoint
。
我在应用程序退出时,监听了 terminal
告诉,并且把解决理论尽量靠后。这样能够保障日志不被脱漏,而且还能够在程序退出时敞开数据库。通过验证,优化后的数据库磁盘占用很小。143,987
条数据库,数据库文件大小为34.8MB
,压缩后的日志大小为1.4MB
,解压后的日志大小为13.6MB
。
wal 模式
这里顺带讲一下 wal
模式,以不便对数据库有更深刻的理解。SQLite
在 3.7
版本退出了 wal
模式,但默认是不开启的,iOS
版的 WCDB
将wal
模式主动开启,并且做了一些优化。
wal
文件负责优化多线程下的并发操作,如果没有 wal
文件,在传统的 delete
模式下,数据库的读写操作是互斥的,为了避免写到一半的数据被读到,会等到写操作执行实现后,再执行读操作。而 wal
文件就是为了解决并发读写的状况,shm
文件是对 wal
文件进行索引的。
SQLite
比拟罕用的 delete
和wal
两种模式,这两种模式各有劣势。delete
是间接读写 db-page
,读写操作的都是同一份文件,所以读写是互斥的,不反对并发操作。而wal
是append
新的 db-page
,这样写入速度比拟快,而且能够反对并发操作,在写入的同时不读取正在操作的db-page
即可。
因为 delete
模式操作的 db-page
是离散的,所以在执行批量写操作时,delete
模式的性能会差很多,这也就是为什么 WCDB
的批量写入性能比拟好的起因。而 wal
模式读操作会读取 db
和wal
两个文件,这样会肯定水平影响读数据的性能,所以 wal
的查问性能绝对 delete
模式要差。
应用 wal
模式须要管制 wal
文件的 db-page
数量,如果 page
数量太大,会导致文件大小不受管制。wal
文件并不是始终减少的,依据 SQLite
的设计,通过 checkpoint
操作能够将 wal
文件合并到 db
文件中。但同步的时机会导致查问操作被阻塞,所以不能频繁执行 checkpoint
。在WCDB
中设置了一个 1000
的阈值,当 page
达到 1000
后才会执行一次checkpoint
。
这个 1000
是微信团队的一个经验值,太大会影响读写性能,而且占用过多的磁盘空间。太小会频繁执行checkpoint
,导致读写碰壁。
# define SQLITE_DEFAULT_WAL_AUTOCHECKPOINT 1000
sqlite3_wal_autocheckpoint(db, SQLITE_DEFAULT_WAL_AUTOCHECKPOINT);
int sqlite3_wal_autocheckpoint(sqlite3 *db, int nFrame){
#ifdef SQLITE_OMIT_WAL
UNUSED_PARAMETER(db);
UNUSED_PARAMETER(nFrame);
#else
#ifdef SQLITE_ENABLE_API_ARMOR
if(!sqlite3SafetyCheckOk(db) ) return SQLITE_MISUSE_BKPT;
#endif
if(nFrame>0){sqlite3_wal_hook(db, sqlite3WalDefaultHook, SQLITE_INT_TO_PTR(nFrame));
}else{sqlite3_wal_hook(db, 0, 0);
}
#endif
return SQLITE_OK;
}
也能够设置日志文件的大小限度,默认是 -1
,也就是没限度,journalSizeLimit
的意思是,超出的局部会被覆写。尽量不要批改这个文件,可能会导致 wal
文件损坏。
i64 sqlite3PagerJournalSizeLimit(Pager *pPager, i64 iLimit){if( iLimit>=-1){
pPager->journalSizeLimit = iLimit;
sqlite3WalLimit(pPager->pWal, iLimit);
}
return pPager->journalSizeLimit;
}
下发指令
日志平台
日志上报应该做到用户无感知,不须要用户被动配合即可进行日志的主动上传。而且并不是所有的用户日志都须要上报,只有出问题的用户日志才是咱们须要的,这样也能够防止服务端的存储资源节约。对于这些问题,咱们开发了日志平台,通过下发上传指令的形式告知客户端上传日志。
咱们的日志平台做的比较简单,输出 uid
对指定的用户下发上传指令,客户端上传日志之后,也能够通过 uid
进行查问。如上图,下发指令时能够抉择上面的日志类型和工夫区间,客户端收到指令后会依据这些参数做筛选,如果没抉择则是默认参数。搜寻时也能够应用这三个参数。
日志平台对应一个服务,点击按钮下发上传指令时,服务会给长连贯通道下发一个 json
,json
中蕴含下面的参数,当前也能够用来扩大其余字段。上传日志是以天为单位的,所以在这里能够依据天为单位进行搜寻,点击下载能够间接预览日志内容。
长连贯通道
指令下发这块咱们利用了现有的长连贯,当用户反馈问题后,咱们会记录下用户的uid
,如果技术须要日志进行排查问题时,咱们会通过日志平台下发指令。
指令会发送到公共的长连贯服务后盾,服务会通过长连贯通道下发指令,如果指令下发到客户端之后,客户端会回复一个 ack
音讯回复,告知通道曾经收到指令,通道会将这条指令从队列中移除。如果此时用户未关上App
,则这条指令会在下次用户关上App
,和长连贯通道建设连贯时从新下发。
未实现的上传指令会在队列中,但最多不超过三天,因为超过三天的指令就曾经失去其时效性,问题过后可能曾经通过其余路径解决。
静默 push
用户如果关上 App
时,日志指令的下发能够通过长连贯通道下发。还有一种场景,也是最多的一种场景,用户未关上 App
怎么解决日志上报的问题,这块咱们还在摸索中。
过后也调研了美团的日志回捞,美团的计划中蕴含了静默 push
的策略,然而通过咱们调研之后,发现静默 push
根本意义不大,只能笼罩一些很小的场景。例如用户 App
被零碎 kill
掉,或者在后盾被挂起等,这种场景对于咱们来说并不多见。另外和 push
组的人也沟通了一下,push
组反馈说静默 push
的达到率有些问题,所以就没采纳静默 push
的策略。
日志上传
分片上传
进行方案设计的时候,因为后端不反对间接展现日志,只能以文件的形式下载下来。所以过后和后端约定的是以天为单位上传日志文件,例如回捞的工夫点是,开始工夫 4 月 21 日 19:00,完结工夫 4 月 23 日 21:00。对于这种状况会被宰割为三个文件,即每天为一个文件,第一天的文件只蕴含 19:00 开始的日志,最初一天的文件只蕴含到 21:00 的日志。
这种计划也是分片上传的一种策略,上传时以每个日志文件压缩一个 zip
文件后上传。这样一方面是保障上传成功率,文件太大会导致成功率降落,另一方面是为了做文件宰割。通过察看,每个文件压缩成 zip
后,文件大小能够管制在 500kb
以内,500kb
这个是咱们之前做视频切片上传时的一个经验值,这个经验值是上传成功率和分片数量的一个平衡点。
日志命名应用工夫戳为组合,工夫单位应该准确到分钟,以不便服务端做工夫筛选操作。上传以表单的形式进行提交,上传实现后会删除对应的本地日志。如果上传失败则应用重试策略,每个分片最多上传三次,如果三次都上传失败,则这次上传失败,在其余机会再从新上传。
安全性
为了保障日志的数据安全性,日志上传的申请咱们通过 https
进行传输,但这还是不够的,https
仍然能够通过其余形式获取到 SSL
管道的明文信息。所以对于传输的内容,也须要进行加密,抉择加密策略时,思考到性能问题,加密形式采纳的对称加密策略。
但对称加密的秘钥是通过非对称加密的形式下发的,并且每个上传指令对应一个惟一的秘钥。客户端先对文件进行压缩,对压缩后的文件进行加密,加密后分片再上传。服务端收到加密文件后,通过秘钥解密失去 zip
并解压缩。
被动上报
新的日志零碎上线后,咱们发现回捞成功率只有 40%
,因为有的用户反馈问题后就失去分割,或者反馈问题后始终没有关上App
。对于这个问题,我剖析用户反馈问题的路径次要有两大类,一种是用户从零碎设置里进去反馈问题,并且和客服沟通后,技术染指排查问题。另一种是用户产生问题后,通过反馈群、App Store
评论区、经营等渠道反馈的问题。
这两种形式都实用于日志回捞,但第一种因为有特定的触发条件,也就是用户点击进入反馈界面。所以,对于这种场景反馈问题的用户,咱们减少了被动上报的形式。即用户点击反馈时,被动上报以以后工夫为完结点,三天内的日志。这样能够把日志上报的成功率晋升到 90%
左右,成功率上来后也会推动更多人接入日志模块,不便排查线上问题。
手动导出
日志上传的形式还蕴含手动导出,手动导出就是通过某种形式进入调试页面,在调试页面抉择对应的日志分享,并且调起零碎分享面板,通过对应的渠道分享进来。在新的日志零碎之前,就是通过这种形式让用户手动导出日志给客服的,可想而知对用户的打搅有多重大吧。
当初手动导出的形式仍然存在,但只用于 debug
阶段测试和开发同学,手动导出日志来排查问题,线上是不须要用户手动操作的。