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