
來源:阿飛的博客
寫在前面:如果對(duì)分庫分表還不是很熟悉的,可以參考筆者之前的文章《分庫分表技術(shù)演進(jìn)&最佳實(shí)踐》。
在這篇文章中提到了一個(gè)場景,即電商的訂單。我們都知道訂單表有三大主要查詢:基于訂單ID查詢,基于商戶編號(hào)查詢,基于用戶ID查詢。且那篇文章給出的方案是基于訂單ID、商戶編號(hào)、用戶ID都有一份分庫分表的數(shù)據(jù)。那么為什么要這么做?能否只基于某一列例如用戶ID分庫分表,答案肯定是不能。
筆者基于sharding-sphere(GitHub地址:https://github.com/apache/incubator-shardingsphere)進(jìn)行了一個(gè)簡單的測試,測試環(huán)境如下:
128個(gè)分表:image_${0..127};
數(shù)據(jù)庫服務(wù)器:32C64G;
數(shù)據(jù)庫版本:MySQL-5.7.23;
操作系統(tǒng):CentOS 6.9 Final;
連接池:druid 1.1.6;
mysql-connector-java:6.0.5;
mybatis:3.4.5;
mybatis-spring:1.3.1;
springboot:1.5.9.RELEASE;
sharding-sphere-3.1.0;
JVM參數(shù):-Xmx2g -Xms2g -Xmn1g -Xss256k -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -XX: AlwaysPreTouch;
druid配置:默認(rèn)參數(shù);
表信息如下:
-- id是分片鍵。備注,DDL是偽SQL
CREATE TABLE `image_${0..127}` (
`id` varchar(32) NOT NULL,
`image_no` varchar(50) NOT NULL,
`file_name` varchar(200) NOT NULL COMMENT '影像文件名稱',
`source` varchar(32) DEFAULT NULL COMMENT '影像來源',
`create_date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '影像文件創(chuàng)建時(shí)間',
PRIMARY KEY (`id`),
KEY `idx_image_no` (`image_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
第1個(gè)測試場景如下:
測試結(jié)果如下:
分片鍵PK.跨分片鍵結(jié)論:由測試結(jié)果可知,跨分片查詢相比帶分片鍵查詢的性能衰減了很多。
第2個(gè)測試場景如下:
跨分片鍵查詢壓力測試結(jié)論:跨的分表數(shù)量越大,跨分表查詢的性能越差;
我們要弄明白跨分片查詢?yōu)槭裁催@么慢之前,首先要掌握跨分片查詢原理。以sharding-sphere為例,其跨分片查詢的原理是:通過線程池并發(fā)請(qǐng)求到所有符合路由規(guī)則的目標(biāo)分表,然后對(duì)所有結(jié)果進(jìn)行歸并。需要說明的是,當(dāng)路由結(jié)果只有1個(gè),即不跨分片操作時(shí)sharding-sphere不會(huì)通過線程池異步執(zhí)行,而是直接同步執(zhí)行,這么做的原因是為了減少線程開銷,核心源碼在ShardingExecuteEngine.java中)。
既然是這個(gè)執(zhí)行原理,為什么跨分片查詢,隨著跨分片數(shù)量越多,性能會(huì)越來越差?我們再看一下第2個(gè)測試場景,當(dāng)測試跨1個(gè)分表時(shí),1w次查詢只需要5889ms,即平均1次查詢不到1ms。所以性能瓶頸不應(yīng)該在SQL執(zhí)行階段,而應(yīng)該在結(jié)果歸并階段。為了驗(yàn)證這個(gè)猜想,筆者空跑sharding-sphere依賴的并發(fā)執(zhí)行組件google-guava的MoreExecutors。其結(jié)果如下:
Multi-Thread Executor Test結(jié)論:由這個(gè)測試結(jié)果可知,當(dāng)并發(fā)執(zhí)行越來越多,其結(jié)果歸并的代價(jià)越來越大。
附--空跑sharding-sphere依賴的并發(fā)執(zhí)行組件google-guava的MoreExecutors的部分源碼如下:
public class ConcurrentExecutorTest {
private static final ListeningExecutorService executorService;
public static final int CONCURRENT_COUNT = 64;
public static final int batchSize = CONCURRENT_COUNT;
public static final int EXECUTOR_SIZE = 8;
static {
executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(EXECUTOR_SIZE));
MoreExecutors.addDelayedShutdownHook(executorService, 60, TimeUnit.SECONDS);
}
private static <I, O> List<O> execute(final Collection<I> inputs) {
if (inputs.isEmpty()) {
return Collections.emptyList();
}
// 并發(fā)執(zhí)行
Collection<ListenableFuture<O>> allFutures = asyncExecute(inputs);
// 結(jié)果歸并
return getResults(allFutures);
}
private static <I, O> Collection<ListenableFuture<O>> asyncExecute(final Collection<I> inputs) {
Collection<ListenableFuture<O>> result = new ArrayList<>(inputs.size());
for (final I each : inputs) {
// 異步執(zhí)行時(shí)直接返回結(jié)果
result.add(executorService.submit(() -> (O) each));
}
return result;
}
private static <O> List<O> getResults(final Collection<ListenableFuture<O>> allFutures) {
List<O> result = new LinkedList<>();
for (ListenableFuture<O> each : allFutures) {
result.add(each.get());
}
return result;
}
}
總結(jié)
跨分片查詢的性能這么差,為什么sharding-sphere還要去做呢?筆者認(rèn)為首先sharding-sphere是一個(gè)通用的分庫分表中間件,而不是在某些特定條件才能使用的中間件,所以應(yīng)該要盡可能的兼容所有SQL。其次,即使跨分片查詢性能這么差,這個(gè)主要是在OLTP系統(tǒng)中使用時(shí)要小心,在一些OLAP或者后臺(tái)管理系統(tǒng)等一些低頻次操作的系統(tǒng)中,還是可以使用的。
比如,賬戶表已經(jīng)根據(jù)賬戶ID分表,但是在運(yùn)營操作的后臺(tái)管理系統(tǒng)中維護(hù)賬戶信息時(shí),肯定有一些操作的SQL是不會(huì)帶有分片鍵賬戶ID的,比如查詢賬戶余額最多的88個(gè)土豪用戶。這個(gè)時(shí)候,我們可以通過sharding-sphere中間件直接執(zhí)行這條低頻次SQL。而不需要為了這些操作引入es或者其他組件來解決這種低頻次的問題(當(dāng)然,隨著系統(tǒng)的演進(jìn),最后可能還是需要引入es等一些中間件來解決這些問題)。所以,分庫分表中間件的跨分片查詢在項(xiàng)目特定階段能夠大大減少開發(fā)成本,從而以最短的時(shí)間上線業(yè)務(wù)需求。