不分享什么知识,聊一下最近的一些思考和看到的一些内容。这两个内容看起来没什么关系,其实也有关系。
sdk 大家都不陌生,比如我们经常用到的 npm 包。当我们以 sdk 的方式提供一种能力的时候,我们的实现不仅决定了业务的使用方式和成本,还决定用户是否乐意使用它。所以我们不能只考虑到功能,还需要考虑到使用方式以及 sdk 本身对业务的影响,不管是稳定性还是性能。当我们的 sdk 对业务来说是刚需时,如果 sdk 有问题,业务可能会联系我们处理,因为它需要这个 sdk。但是如果对业务来说这个 sdk 不是刚需时,业务可能直接 uninstall 我们的 sdk 并删除对应的代码。这对于提供 sdk 的我们来说显然不是个好事情。但是不管是否刚需,作为提供方,我们都需要努力去做好所提供的服务。
1 内嵌形式
## 1.1 内嵌于业务代码的形式
我们使用的 sdk 大多数都是引入业务代码中,然后使用它提供的功能,这种情况下,有两种模式,第一种是业务要感知 sdk 提供的 API。我们需要知道什么时候使用什么 API。第二种是业务不需要感知 sdk 提供的 API,或者说这时候 sdk 不提供 API,它本身就像一个黑盒子,业务引入后就内置了某些功能,比如我们提供一个定时上报业务内存使用情况的 sdk,那么业务就不需要关注 sdk 的具体实现。下面以统计请求耗时为例看看如何实现这个 sdk。
1.第一种
- {
- start(...) {}
- end(...) {}
- }
第一种方式是比较朴素的实现,sdk 提供了一个 start 和 end 的 API,业务在开始请求和结束请求时分别执行这两个 API,这样 sdk 就可以计算出这个请求的耗时。但是这种方式看起来并不是那么友好,首先会侵入业务的代码逻辑,其次业务还需要感知这个 sdk,需要考虑什么时候调 start,什么时候调 end,而且 sdk 还依赖业务传入请求和响应的上下文,才能计算出某一个请求的耗时,总的来说,这种方式比较麻烦。
2 第二种
我们希望对业务的侵入性和感知少一点,所以决定直接劫持 Node.js 里的 API。Node.js 里以下面的形式可以创建一个服务器。
- http.createServer((req, res) => {})
那么我们直接劫持这个 createServer。
- const createServer = http.createServer;
- http.createServer = (cb) => {
- return createServer((req, res) => {
- const start = Date.now();
- res.on('finish', () => {
- const cost = end - start;
- });
- cb();
- });
- }
通常,sdk 是提供 API,由业务主动调用,或者说触发 sdk 的代码,因为 sdk 无法捕获业务代码什么时候需要使用 sdk 的某个功能。但是当我们可以捕获到业务什么时候需要我们时,就可以以更好的方式去提供这个 sdk。这种方式可以使得业务不需要过多感知 sdk,比如上面的例子中,业务只需要保证在调用http.createServer 之前执行我们的 sdk 就可以。sdk 内嵌业务代码中是非常常见的形式,但是我们希望尽量减少对业务的侵入,或者说减少业务的心智负担,大家可能都有过这种经历,当看到一个 sdk 提供密密麻麻的参数时,第一反应就不想用了。
2 脱离业务代码的形式
那么是否能以一种脱离于业务代码的方式提供一个 sdk,这样不仅不会影响业务代码,对于升级 sdk 来说也更容易。但是这种方式往往不容易,主要取决于场景,比如业务需要通过一个 sdk 上传文件,那么这个 sdk 以内嵌的方式会比较合适。但是,某些场景下,脱离业务代码的 sdk 是可以做到的,比如排查问题类的工具。在 Node.js 里,我们调试或诊断进程的方式通常是在业务代码里内嵌相关的代码,然后在必要的时候执行对应的代码,比如获取堆快照。因为我们的代码只有置身于进程中,才能获取到这个信息。但是不是所有的信息都需要置身于进程中才能获取,比如系统级的数据。我之前碰到一个问题,就是在某个场景下,WebSocket 连接会很快底被断开,通过再客户端 wireshark 捕获的流量中,发现服务器会发送一个 fin 包给客户端,这样就知道是服务器的问题了,但是又因为从客户端到真正的服务器中间还隔了很多层,无法知道是哪一层服务器主动断开了连接,最后通过服务器提供的工具找到了主动发送 fin 包的服务器从而解决了问题。但是我发现服务器的那些工具用起来都非常复杂,如果不经常用,很快就忘了各种命令和参数,像这种场景,就可以封装 sdk 给业务使用,这种形式不仅可以帮助业务排查问题,还不需要侵入业务代码。
3 问题排查
我们排查问题通常借助日志,但是日志很多时候也解决不了问题,日志是静态埋点,打多了不仅浪费存储,而且消耗性能,打少了可能缺少排查问题的上下文。但是无论如何,重点是日志是静态埋点,如果我们要加埋点,就得重启服务,有些问题稍纵即逝,重启后可能就很难复现了。所以除了静态追踪技术外,动态追踪技术就非常必要,也非常 cool 了,之前看了一下 ebpf,但是后来没看了,最近重新研究了一下 ebpf 和所衍生的一些排查问题的工具,也看了一下 openresty 作者的文章《动态追踪技术漫谈》,可谓是精彩。当一个进程或者系统有问题时,我们希望保留现场,然后再慢慢分析。但是我们在进程之外怎么能获得进程的数据呢?除了系统本身提供的一些命令外,这里想说的是一种更复杂但更强大的技术。操作系统和我们写的业务代码一样,都是一些代码的逻辑,我们在写代码时,经常会用到钩子或者劫持的技术。同样,操作系统也不例子,但是操作系统为了提供这种技术,实现上复杂得多。这种技术就是 ebpf,ebpf 是把用户写的代码注入到内核中,内核有一个虚拟机,满足条件的时候就会执行我们的代码。
操作系统提供了钩子机制,比如我们可以注册一个钩子到系统,当系统收到网络包时,就会回调我们。另外一种就是劫持,比如 kprobe 到实现,当我们写一段代码指示操作系统当有人调用 x 的时候回调我们,操作系统就会把这个地址对应的指令改成 int3(x86 架构),然后执行到 x 这个函数的时候,就会触发 int3 中断,对应的处理函数就会执行我们注册的回调,然后再执行真正的函数。很多技术都依赖 ebpf,比如 tcpdump。ebpf 厉害之处在于内核编程可编程的了,真正情况下,我们可以通过基于 ebpf 的工具,从内核中查到非常多的信息,以帮助我们排查问题。ebpf 非常流行,也非常复杂,就不讨论太多,大家可以自行查阅相关信息。