2.1.2 物理隔离
物理隔离是将所有压测数据写入独立的数据区域的方法,这一数据区域与真实的数据库(或表)在物理上完全隔离,从而最大限度规避了数据污染风险,清理数据也比较方便。
要做到物理隔离,软件系统需要具备两项基础能力:压测流量打标与透传,以及影子库/表的支持。
压测流量打标与透传不同于逻辑隔离中的数据实体内打标,我们现在需要在流量中打标,以区分真实请求和压测请求。压测流量打标与透传具体的做法如图2.2所示,在用户访问请求的HTTP头中置入一个特殊的压测标识,当流量进入内网并在各服务和中间件之间传递时,我们需要确保这一标识始终能够完整透传,最终抵达数据层,数据层根据流量类型将数据写入隔离区域(如影子表)或正常区域(如真实表)。在图2.2中,我们可以看到压测流量的传递路径(红色路径),因此压测流量打标与透传又称为“流量染色”。
图2.2 压测流量打标与透传
在这一过程中有两点要格外注意。首先,在请求跨线程传递和请求协议转换的时候我们也要保证压测标识能够透传,即使压测标识的存放位置发生了改变,例如,HTTP流量进入内网后转换成RPC(Remote Procedure Call)流量,此时需要将HTTP头中的压测标识转移至RPC请求的上下文中,避免丢失。其次,当请求经过异步化中间件(如消息队列)时,压测标识也应当传递无误,例如,针对消息队列,当带有压测标识的生产者推送消息时,我们需要将压测标识转存至数据中,以免丢失,当异步服务消费消息时,再将该标识恢复至消费者的请求体或上下文中继续传递。
上述工作涉及不少技术“改造”点,其中涉及中间件的改造工作将在2.2节中讲解,这里我们先来了解一下跨线程压测标识透传的技术实现方案,我们以阿里巴巴开源的TransmittableThreadLocal为例介绍基于Java语言的实现过程。
我们知道,Java语言提供的ThreadLocal对象可以存储当前线程的共享变量,但它无法应对跨线程的情况,此时TransmittableThreadLocal就有了用武之地。在TransmittableThreadLocal中设有一个Holder来保存每个线程所持有的所有ThreadLocal对象,并在线程提交的时候进行上下文的复制,这样就能够保证变量在各线程间正确地传递。
我们通过以下代码进一步展示TransmittableThreadLocal的能力。这段代码很简单,首先创建一个TransmittableThreadLocal对象,然后在父线程中设置一个名为perftest的变量作为压测标识,在子线程中尝试获取这个变量。
public class ThreadTest {
private static ThreadLocal<String> TTL = new TransmittableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
// 在父线程中设置标识
TTL.set(“perftest”);
new Thread(() -> {
// 在子线程中输出标识
System.out.println(TTL.get());
}).start();
}).start();
}
}
如图2.3所示,我们成功在子线程中得到了这一变量,实现了压测标识透传的效果。
图2.3 压测标识由父线程透传至子线程
具备了压测流量打标与透传的能力后,我们再来探讨影子库/表的支持,这部分内容比较多而且相对独立,我们专门用2.1.3节进行讲述。