卡皮巴拉设计思路和难点
拖了一周多以后今天开始相对实质性地开展了对卡皮巴拉的工作……的准备工作。
卡皮巴拉是一个做协议层连接迁移的工作。它的主要特性有
- (几)微秒级超低延迟
- 无需感知迁移;对于应用来说连接就像没有迁移过
- 迁移策略设计,这个不太影响我参与的工作
卡皮巴拉这个工作本来是专门做TCP连接迁移的,在这一轮迭代中决定转向更加通用的,(基于TCP的)多层协议栈连接迁移。核心案例是对TCP+TLS连接的支持。
这就引出了我的工作内容:设计一套通用的协议栈迁移接口并将迁移功能基于这套接口进行翻新,并且为TCP+TLS编写这套接口的实现,最后把TCP+TLS+上层应用(大概是Redis)跑起来。
功能定义
上周的项目会作为上周的工作量讲了一下。(实际上是开会时一边想一边说的。)
当迁移的对象从「exactly TCP」变成了「一类被用来维持连接的状态」时,卡皮巴拉到底对什么做迁移而什么不做迁移就变得值得探究。
首先不会因为卡皮巴拉有迁移状态的能力就把所有状态都迁移了。那就做成了重型应用迁移工作了,效果也不会好动机也不清楚。
所以肯定是有东西不迁移的,比如TCP+TLS+Redis中的Redis。没有迁移的部分就直接复位成初始状态。这对于无状态和弱状态(像Redis这种丢了也不会影响正确性的状态)来说是合适的。
所以上面特性中说的是「对于应用来说连接就像没有迁移过」,而不是「对于应用来说就像没有迁移过」。来到目标机器上的应用被全新启动了一遍,然后接收一些迁移过来的连接。应用需要对「迁移」这件事有感知,知道这些连接不是新的所以不要再重复握手把客户端搞晕,而是直接开始该读读该写写;但是连接本身是没有感知的,换句话说,它不会断,也不需要经过一个显式的「重新建连」过程。
我们将运行时状态分成两部分:连接状态和应用状态。连接状态每个(进行中的)连接实例都有一份,会在迁移过程中发往目标机器;应用状态我们不管,迁移会导致其丢失。
一个运行时状态属于连接状态还是应用状态并不取决于它由谁提供和管理。在Redis实现中,TLS和Redis是一个整体,与卡皮巴拉运行时以一种相当生疏地方式协同(LD_PRELOAD)。即便如此,TLS状态属于连接状态,在迁移时它要被送进卡皮巴拉,连从卡皮巴拉侧的TCP状态一起被发走,而Redis状态则是应用状态被丢掉。这种不匹配将会带来一定的工程复杂度。
接口设计
虽然对于应用状态我们无需做设计,但是对于应用还是要做一点设计的:上面提到的用于接受迁移来的连接的接口accept_migrated(void *user_data) -> fd
(暂定)。应用要负责调用它,不然的话接收不到迁移来的连接(而把网络栈缓存撑爆)就是应用自己的责任。
连接状态需要被发走,所以序列化/反序列化是必须的。连接层应当支持以下一组接口
- 指明其数据类型。对于TCP来说,因为实际的数据留存在卡皮巴拉运行时内部,所以只是一个描述符;对于TLS则应该是某种上下文对象。
- 序列化和反序列化方法。需要对它们额外添加一个引用卡皮巴拉相关数据的参数,方便TCP实现从里面拿/往里面塞东西。
连接协议栈应当以一层一层包洋葱的方式拼装起来。运行时关于迁入/迁出连接的接口都只需要接收最顶层的连接类型,并通过它对连接层接口的实现与整个连接层(包括最底下自己实现的TCP层)进行交互。每层连接状态都应该嵌套地调用其下一层的序列化/反序列化。这种设计应该是相对自然的,比如TLS确实会想要持有一个TCP套接字的标识符,那么反序列化的时候就可以先嵌套地完成TCP层的反序列化,然后就可以拿到标识符从而完成自己的反序列化。
开发测试方案
之前的迁移决策判断是深埋在网络栈内部的,无法做相对可控地手动触发迁移。今天找一作老哥添加了一点样例测试代码,可以在运行时之外进行微操了。
目前的出发点是一个可以迁移的TCP连接和跑在其之上的无状态HTTP。接下来要
- 设计出连接层接口,更新运行时
- 在TCP之上跑一个玩具伪连接,只在迁移期间存在,传点东西过去就行
- 把TLS跑起来
这周应该也就这样了。
原本的计划是一路向上跑到Redis。但是Redis自带的TLS方案是基于OpenSSL的。OpenSSL没有提供序列化接口。所以需要把Redis重新跑在别的TLS实现之上,比如TLSe。考虑到这个的话稍微延期一点应该也还好吧。