時(shí)序數(shù)據(jù)庫(kù)超表架構(gòu)落地的一次實(shí)戰(zhàn)復(fù)盤)
一、金倉(cāng)時(shí)序數(shù)據(jù)庫(kù)超表架構(gòu)難在哪先撂個(gè)結(jié)論手工分表這套辦法毛病不在“分”上在于分完之后所有的活兒都得你自己干。做過(guò)時(shí)序數(shù)據(jù)的人應(yīng)該都懂。按月建表嘛xxx_202501、xxx_202502一路往下排。寫入靠應(yīng)用層拼表名做路由查跨月的數(shù)據(jù)就 UNION ALL 一串。量小的時(shí)候是真沒(méi)毛病甚至可以說(shuō)還挺優(yōu)雅。可數(shù)據(jù)一上來(lái)麻煩就開(kāi)始一個(gè)接一個(gè)往外蹦。最先繃不住的往往是分區(qū)規(guī)則。它散落在代碼里啊建表靠定時(shí)任務(wù)路由邏輯埋在應(yīng)用里頭。新同事不知道有這套約定改查詢忘了帶月份路由一條 SQL 直接掃當(dāng)月大表。這種事幾乎每個(gè)團(tuán)隊(duì)都出過(guò)出一次記一輩子。然后是跨月查詢。UNION ALL 完再聚合你自己琢磨琢磨各表上的索引不就白建了嘛。月份跨得越多越慢沒(méi)有例外。冷熱數(shù)據(jù)還混著放。三個(gè)月前的明細(xì)除了出報(bào)表那天誰(shuí)碰它啊可它偏偏和今天的熱數(shù)據(jù)躺同一塊盤上磁盤告警三天兩頭來(lái)報(bào)到。刪又不敢刪鬼知道哪張報(bào)表哪天要用。到期刪舊數(shù)據(jù)這事兒靠人惦記忘了就堆著堆著堆著就成了誰(shuí)也不敢動(dòng)的歷史包袱。想擴(kuò)容就剩換更貴的服務(wù)器一條道賬單蹭蹭漲。這些麻煩攤開(kāi)來(lái)看根子其實(shí)就一個(gè)問(wèn)題分區(qū)這件事到底歸誰(shuí)管手工分表時(shí)代它歸應(yīng)用管所以樁樁件件都得人扛。金倉(cāng)的超表Hypertable治的就是這個(gè)。它把分區(qū)的活兒整個(gè)下沉到數(shù)據(jù)庫(kù)里頭去了。數(shù)據(jù)進(jìn)來(lái)按時(shí)間自動(dòng)切成一個(gè)個(gè)數(shù)據(jù)塊Chunk每塊只管一段時(shí)間的再疊個(gè)空間維度比如按點(diǎn)位 ID 哈希把高并發(fā)寫入攤開(kāi)。應(yīng)用這邊看到啥從頭到尾就一張monitor_point_dataINSERT 該咋寫咋寫。查半年的數(shù)據(jù)也是一條普普通通的 SQL跟你當(dāng)前時(shí)間區(qū)間沒(méi)關(guān)系的塊數(shù)據(jù)庫(kù)自己就不碰了。兩種模式擺一塊兒對(duì)比下對(duì)比項(xiàng)手工分表金倉(cāng)超表分區(qū)誰(shuí)管應(yīng)用建表定時(shí)任務(wù)規(guī)則散在代碼里數(shù)據(jù)庫(kù)自己按時(shí)間空間切 Chunk跨時(shí)段查詢多表 UNION ALL索引廢掉一條普通 SQL沒(méi)關(guān)的塊直接不碰寫入路由應(yīng)用按時(shí)間拼表名不存在這個(gè)環(huán)節(jié)直接插老數(shù)據(jù)治理手工 DROP忘了就堆著到期自動(dòng)按塊刪以后擴(kuò)容堆硬件燒錢可以往分布式超表走SQL 兼容-標(biāo)準(zhǔn) SQLBI 工具直連靠 SQL 吃飯的團(tuán)隊(duì)最后一行搞不好才是最香的。報(bào)表工具、BI 看板、備份腳本、監(jiān)控探針、權(quán)限體系全能接著用不用為時(shí)序能力單獨(dú)搭一條工具鏈。人就那么幾個(gè)的小團(tuán)隊(duì)這點(diǎn)比啥跑分都實(shí)在。不過(guò)丑話說(shuō)前頭超表只是把復(fù)雜度從應(yīng)用層挪到了數(shù)據(jù)庫(kù)層它可沒(méi)消失。塊間隔、壓縮、保留策略、連續(xù)聚合這套新機(jī)制各有各的坑坑跟坑之間還會(huì)互相影響。下面按落地的順序一個(gè)一個(gè)說(shuō)。目錄一、金倉(cāng)時(shí)序數(shù)據(jù)庫(kù)超表架構(gòu)難在哪二、建表三、第一個(gè)坑塊間隔四、壓縮和保留五、第二個(gè)坑刷新窗口和保留策略會(huì)互相咬六、幾句經(jīng)驗(yàn)二、建表先建張普通表就你平時(shí)寫的那種一點(diǎn)特殊語(yǔ)法都沒(méi)有CREATETABLEmonitor_point_data(timeTIMESTAMPTZNOTNULL,point_idINTEGERNOTNULL,metricTEXTNOTNULL,valueDOUBLEPRECISION,qualitySMALLINT);-- 轉(zhuǎn)為超表時(shí)間列做主分區(qū)point_id 哈希做空間分區(qū)SELECTcreate_hypertable(monitor_point_data,time,partitioning_columnpoint_id,number_partitions8,chunk_time_intervalINTERVAL1 day);一個(gè)函數(shù)調(diào)用完事兒。時(shí)間軸按chunk_time_interval自動(dòng)滾新塊空間軸按點(diǎn)位 ID 哈希成 8 個(gè)分區(qū)。應(yīng)用側(cè)要做的改造就是把原來(lái)拼表名那段代碼刪掉。有個(gè)約束得提前講省得對(duì)著報(bào)錯(cuò)發(fā)半天呆唯一索引必須帶上分區(qū)列。想拿point_id這種單列做唯一約束數(shù)據(jù)庫(kù)直接給你彈回來(lái)報(bào)錯(cuò)還老長(zhǎng)一段。道理不復(fù)雜每個(gè) Chunk 各自建索引數(shù)據(jù)庫(kù)沒(méi)法跨塊替你保證唯一性所以唯一索引里必須把時(shí)間列捎上。三、第一個(gè)坑塊間隔塊間隔這個(gè)參數(shù)直覺(jué)上特別容易犯一個(gè)錯(cuò)覺(jué)得塊切得越小查詢?cè)娇焐蟻?lái)就設(shè)個(gè) 1 小時(shí)。直說(shuō)了吧會(huì)翻車。塊切太細(xì)后臺(tái)建新塊的頻率就飛起寫入高峰還會(huì)莫名其妙冒出鎖等待。為啥建新塊要拿的鎖比往已有塊里插數(shù)據(jù)拿的鎖時(shí)間長(zhǎng)。一堆事務(wù)擠在同一時(shí)刻搶著開(kāi)新塊可不就互相頂死了嘛。那到底設(shè)多大手冊(cè)里有參考值按日寫入量給的每天寫 2GB、內(nèi)存 64GB 的機(jī)器7 天一塊正合適一天寫到 10GB縮到 1 天一塊。所以原則就一句話先抄手冊(cè)的作業(yè)再按自己的量微調(diào)。直覺(jué)在這兒不值錢。運(yùn)行中想調(diào)也有接口但藏著個(gè)語(yǔ)義坑-- 注意只對(duì)之后新建的塊生效已經(jīng)建好的塊不動(dòng)SELECTset_chunk_time_interval(monitor_point_data,INTERVAL24 hours);-- 看看當(dāng)前分區(qū)配置長(zhǎng)啥樣SELECTcolumn_name,num_partitions,time_intervalFROMtimescaledb_information.dimensionsWHEREhypertable_namemonitor_point_data;改間隔只管新塊舊塊一個(gè)不碰。也就是說(shuō)塊間隔設(shè)大了想改小舊塊是救不回來(lái)的。要么干等它自己滾出保留期要么老老實(shí)實(shí)遷數(shù)據(jù)。這參數(shù)務(wù)必上線前定死。四、壓縮和保留先問(wèn)一句一個(gè)月之前的明細(xì)除了出報(bào)表那天還有誰(shuí)碰它沒(méi)人碰。時(shí)序數(shù)據(jù)的訪問(wèn)模式就是這么有規(guī)律最近的數(shù)據(jù)天天查老數(shù)據(jù)出了報(bào)表就沒(méi)人搭理了。壓縮策略照著這個(gè)規(guī)律配就行近幾天原樣擱著更老的塊自動(dòng)壓成列存。ALTERTABLEmonitor_point_dataSET(timescaledb.compress,timescaledb.compress_segmentbypoint_id,timescaledb.compress_orderbytime DESC);-- 超過(guò) 7 天的塊自動(dòng)壓縮SELECTadd_compression_policy(monitor_point_data,compress_afterINTERVAL7 days);-- 原始明細(xì)保留 180 天到期自動(dòng)刪塊SELECTadd_retention_policy(monitor_point_data,drop_afterINTERVAL180 days);配置里兩個(gè)參數(shù)說(shuō)下作用。compress_segmentby是按哪列分組壓縮時(shí)序場(chǎng)景一般挑設(shè)備或者點(diǎn)位 ID。compress_orderby是組內(nèi)按啥排通常就填時(shí)間列。這倆選對(duì)了壓縮比和查詢效率都跟著受益實(shí)測(cè)壓縮比 4:1 上下跟官方口徑基本對(duì)得上。順帶提個(gè)不大但挺陰的細(xì)節(jié)壓縮塊上沒(méi)法直接加帶默認(rèn)值的列要加得先解壓。嫌解壓麻煩也有變通加個(gè)可空列再 UPDATE 把值補(bǔ)上就這么繞過(guò)去。五、第二個(gè)坑刷新窗口和保留策略會(huì)互相咬報(bào)表聚合慢靠連續(xù)聚合治。思路不玄乎把小時(shí)級(jí)的聚合結(jié)果提前物化成一張?zhí)厥獾某砗笈_(tái)按策略增量刷新查詢直接拿現(xiàn)成的不碰明細(xì)。CREATEMATERIALIZEDVIEWpoint_data_hourlyWITH(timescaledb.continuous)ASSELECTpoint_id,time_bucket(INTERVAL1 hour,time)ASbucket,avg(value)ASavg_val,max(value)ASmax_val,min(value)ASmin_valFROMmonitor_point_dataGROUPBYpoint_id,bucketWITHNODATA;SELECTadd_continuous_aggregate_policy(point_data_hourly,start_offsetINTERVAL3 days,end_offsetINTERVAL1 hour,schedule_intervalINTERVAL30 minutes);下面這個(gè)坑我個(gè)人認(rèn)為是整套方案里最容易翻車的必須單獨(dú)拎出來(lái)說(shuō)。很多人配這倆策略的時(shí)候是分開(kāi)想的保留策略拍一個(gè)數(shù)刷新窗口拍另一個(gè)數(shù)各管各的看著多合理啊。但它們實(shí)際上會(huì)互相咬。你保留 30 天刷新窗口也伸到 30 天前會(huì)咋樣聚合刷新跑到那個(gè)時(shí)間段一看源數(shù)據(jù)讓保留策略給刪了那它就把物化好的結(jié)果也順手刪了。注意這整個(gè)過(guò)程沒(méi)有報(bào)錯(cuò)。沒(méi)告警沒(méi)異常日志就是報(bào)表上的數(shù)字悄無(wú)聲息地沒(méi)了。直到哪天有人盯著一條空曲線問(wèn)數(shù)據(jù)咋回事你才知道壞了。這種靜默失敗排查起來(lái)最磨人。正確姿勢(shì)就一條保留周期要遠(yuǎn)大于刷新窗口。照上面例子那樣保留 180 天、刷新窗口只伸到 3 天前讓刷新永遠(yuǎn)落在還有原始數(shù)據(jù)的時(shí)段里這坑就踩不著。手冊(cè)里對(duì)這個(gè)組合是有明確警告的別問(wèn)我為啥知道得這么清楚。日常查趨勢(shì)還是普通 SQL配個(gè)時(shí)間桶函數(shù)要多細(xì)有多細(xì)SELECTtime_bucket(5 minutes,time)ASfive_min,avg(value)ASavg_valFROMmonitor_point_dataWHEREpoint_id1024ANDtimenow()-INTERVAL2 hoursGROUPBYfive_minORDERBYfive_min;六、幾句經(jīng)驗(yàn)回頭看超表落地這事技術(shù)上真不難難的是心態(tài)。把原來(lái)攥在自己手里那點(diǎn)“分表智慧”整個(gè)交還給數(shù)據(jù)庫(kù)交出去那幾天是真沒(méi)底老琢磨它真能替我管好幾條經(jīng)驗(yàn)擱這兒。塊間隔先抄作業(yè)再微調(diào)按日寫入量對(duì)照手冊(cè)來(lái)。這參數(shù)改小只對(duì)新塊生效設(shè)大了想縮沒(méi)有回頭路。唯一索引必須帶分區(qū)列單列唯一約束直接報(bào)錯(cuò)把時(shí)間列加上就好。保留策略和刷新窗口必須一起設(shè)計(jì)。這條得再說(shuō)一遍因?yàn)檫@倆配岔了連報(bào)錯(cuò)都沒(méi)有數(shù)據(jù)就這么沒(méi)了。壓縮塊上別直接加帶默認(rèn)值的列先解壓或者可空列加 UPDATE 繞過(guò)去。還有個(gè)容易忽略的超表的價(jià)值不止“快”那一下。應(yīng)用代碼干凈了團(tuán)隊(duì)里沒(méi)人再需要搞懂那套分表路由的約定。這種工程上的松快時(shí)間拉長(zhǎng)了看比查詢快幾秒值錢。往后單機(jī)寫入真到了天花板還有分布式超表這條路寫入再橫向攤一層架構(gòu)不用推倒重來(lái)。先寫到這等真跑起來(lái)了再補(bǔ)后話。