乐趣区

关于iOS开发:设计一套完整的日志系统

需要

日志对于线上排查问题是十分重要的,很多问题其实是很偶现的,同样的零碎版本,同样的设施,可能就是用户的复现,而开发通过雷同的操作和设施就是不复现。然而这个问题也不能始终不解决,所以能够通过日志的形式排查问题。可能是后盾导致的问题,也可能是客户端逻辑问题,在关键点记录日志能够疾速定位问题。

假如咱们的用户量是一百万日活,其中有 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

淘汰策略

不光是要往数据库里写,还须要思考淘汰策略。淘汰策略须要均衡记录的日志数量,以及时效性的问题,日志数量尽量够排查问题,并且还不会占用过多的磁盘空间。所以,在日志上传之后会将已上传日志删除掉,除此之外日志淘汰策略有以下两种。

  1. 日志最多只保留三天,三天以前的日志都会被删掉。在利用启动后进行查看,并后盾线程执行这个过程。
  2. 日志减少一个最大阈值,超过阈值的日志局部,以工夫为序,从前往后删除。咱们定义的阈值大小为200MB,个别不会超过这个大小。

记录根底信息

在排查问题时一些要害信息也很重要,例如用户过后的网络环境,以及一些配置项,这些因素对代码的执行都会有一些影响。对于这个问题,咱们也会记录一些用户的配置信息及网络环境,不便排查问题,但不会波及用户经纬度等隐衷信息。

数据库

旧计划

之前的日志计划是通过 DDLog 实现的,这种计划有很重大的性能问题。其写入日志的形式,是通过 NSData 来实现的,在沙盒创立一个 txt 文件,通过一个句柄来向本地写文件,每次写完之后把句柄 seek 到文件开端,下次间接在文件开端持续写入日志。日志是以 NSData 的形式进行解决的,相当于始终在频繁的进行本地文件写入操作,还要在内存中维持一个或者多个句柄对象。

这种形式还有个问题在于,因为是间接进行二进制写入,在本地存储的是 txt 文件。这种形式是没有方法做筛选之类的操作的,扩展性很差,所以新的日志计划咱们打算采纳数据库来实现。

计划抉择

我比照了一下 iOS 平台支流的数据库,发现 WCDB 是综合性能最好的,某些方面比 FMDB 都要好,而且因为是 C++ 实现的代码,所以从代码执行的层面来讲,也不会有 OC 的音讯发送和转发的额定耗费。

依据 WCDB 官网的统计数据,WCDBFMDB 进行比照,FMDB是对 SQLite 进行简略封装的框架,和间接用 SQLite 差异不是很大。而 WCDB 则在 sqlcipher 的根底上进行的深度优化,综合性能比 FMDB 要高,以下是性能比照,数据来自 WCDB 官网文档。

单次读操作 WCDB 要比 FMDB5%左右,在 for 循环内始终读。

单次写操作 WCDB 要比 FMDB28%,一个 for 循环始终写。

批量写操作比拟显著,WCDB要比 FMDB180%,一个批量工作写入一批数据。

从数据能够看出,WCDB在写操作这块性能要比 FMDB 要快很多,而本地日志最频繁的就是写操作,所以这正好合乎咱们的需要,所以抉择 WCDB 作为新的数据库计划是最合适的。而且我的项目中曝光模块曾经用过WCDB,证实这个计划是可行并且性能很好的。

表设计

咱们数据库的表设计很简略,就上面四个字段,不同类型的日志用 type 做辨别。如果想减少新的日志类型,也能够在我的项目中扩大。因为应用的是数据库,所以扩展性很好。

  • index:主键,用来做索引。
  • content:日志内容,记录日志内容。
  • createTime:创立工夫,日志入库的工夫。
  • type:日志类型,用来辨别三种类型。

数据库优化

咱们是视频类利用,会波及播放、下载、上传等次要性能,这些性能都会大量记录日志,来不便排查线上问题。所以,防止数据库太大就成了我在设计日志零碎时,比拟看重的一点。

依据日志规模,我对播放、下载、上传三个模块进行了大量测试,播放一天两夜、下载 40 集电视剧、上传多个高清视频,累计记录的日志数量大略五万多条。我发现数据库文件夹曾经到 200MB+ 的大小,这个大小曾经是比拟大的,所以须要对数据库进行优化。

我察看了一下数据库文件夹,有三个文件,dbshmwal,次要是数据库的日志文件太大,db文件反而并不大。所以须要调用 sqlite3_wal_checkpointwal内容写入到数据库中,这样能够缩小 walshm文件的大小。但 WCDB 并没有提供间接 checkpoint 的办法,所以通过调研发现,执行 database 的敞开操作时,能够触发checkpoint

我在应用程序退出时,监听了 terminal 告诉,并且把解决理论尽量靠后。这样能够保障日志不被脱漏,而且还能够在程序退出时敞开数据库。通过验证,优化后的数据库磁盘占用很小。143,987条数据库,数据库文件大小为34.8MB,压缩后的日志大小为1.4MB,解压后的日志大小为13.6MB

wal 模式

这里顺带讲一下 wal 模式,以不便对数据库有更深刻的理解。SQLite3.7 版本退出了 wal 模式,但默认是不开启的,iOS版的 WCDBwal模式主动开启,并且做了一些优化。

wal文件负责优化多线程下的并发操作,如果没有 wal 文件,在传统的 delete 模式下,数据库的读写操作是互斥的,为了避免写到一半的数据被读到,会等到写操作执行实现后,再执行读操作。而 wal 文件就是为了解决并发读写的状况,shm文件是对 wal 文件进行索引的。

SQLite比拟罕用的 deletewal两种模式,这两种模式各有劣势。delete是间接读写 db-page,读写操作的都是同一份文件,所以读写是互斥的,不反对并发操作。而walappend新的 db-page,这样写入速度比拟快,而且能够反对并发操作,在写入的同时不读取正在操作的db-page 即可。

因为 delete 模式操作的 db-page 是离散的,所以在执行批量写操作时,delete模式的性能会差很多,这也就是为什么 WCDB 的批量写入性能比拟好的起因。而 wal 模式读操作会读取 dbwal两个文件,这样会肯定水平影响读数据的性能,所以 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 进行查问。如上图,下发指令时能够抉择上面的日志类型和工夫区间,客户端收到指令后会依据这些参数做筛选,如果没抉择则是默认参数。搜寻时也能够应用这三个参数。

日志平台对应一个服务,点击按钮下发上传指令时,服务会给长连贯通道下发一个 jsonjson 中蕴含下面的参数,当前也能够用来扩大其余字段。上传日志是以天为单位的,所以在这里能够依据天为单位进行搜寻,点击下载能够间接预览日志内容。

长连贯通道

指令下发这块咱们利用了现有的长连贯,当用户反馈问题后,咱们会记录下用户的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 阶段测试和开发同学,手动导出日志来排查问题,线上是不须要用户手动操作的。

退出移动版