一、背景
部門中一核心應(yīng)用,因為各種原因其依賴的MySQL數(shù)據(jù)庫一直處于高水位運行,無論是硬件資源,還是磁盤使用率或者QPS等都處于較高水位,急需在大促前完成對應(yīng)的治理,降低各項指標(biāo),以保障在大促期間平穩(wěn)運行,以期更好的支撐前端業(yè)務(wù)。
二、基本情況
2.1、數(shù)據(jù)庫
目前該數(shù)據(jù)庫是一主兩從,且都是零售的物理機(jī),運行多年已都是過保機(jī)器。同時因為CPU和磁盤較大,已無同規(guī)格的物理機(jī)可以增加一個從庫。同時其中一個從庫的內(nèi)存減半且磁盤還是機(jī)械盤,出故障風(fēng)險極高且IO性能低導(dǎo)致查詢偏慢,出現(xiàn)過多次因性能問題切到另一個從庫的情況。
以下是其3臺機(jī)器的硬件資源信息,MySQL版本、部署機(jī)房和硬件配置情況。其中135機(jī)器硬盤容量128T是統(tǒng)計顯示有誤,可以認(rèn)為也是16T。因為磁盤做了RAID0,因此實際容量在7T左右。
| IP | 域名 | 主/從 | CPU | 內(nèi)存 | 容量 | 機(jī)房 | DISK(/export)使用率(%) | Memory使用率(%) | 數(shù)據(jù)庫版本 |
| 1x.x.x.36 | xxx_m.mysql.jddb.com | 主 | 64 | 256G | 16T | 匯天云端機(jī)房 | 66.3% | 87.7% | 5.5.14 |
| 1x.x.x.73 | xxx_sb.mysql.jddb.com | 從 | 64 | 256G | 16T | 匯天云端機(jī)房 | 66.6% | 85.2% | 5.5.14 |
| 1x.x.x.135 | xxx_sa.mysql.jddb.com | 從 | 64 | 128G | 128T | 廊坊機(jī)房 | 76.5% | 57.2% | 5.5.14 |
2.2、磁盤空間
截止到2月底,各數(shù)據(jù)庫磁盤空間占用情況如下:
| IP | 主從 | 使用大小(G) | 已用比例(%) | 剩余空間(G) | 周增長量(G) | 預(yù)計報警(d) | 預(yù)計可用(d) | binlog(G) | 日志(G) |
| 1x.x.x.36 | M | 5017 | 69 | 2151 | 9 | 617.1 | 1735.8 | 159.45543 | 6 |
| 1x.x.x.73 | S | 5017 | 71 | 2151 | 14.8 | 333.2 | 1012.7 | 158.52228 | 1 |
| 1x.x.x.135 | S | 5017 | 4 | 129000 | 14.4 | 2986 | 8958 | 158.13548 | 0 |
從上表咱們可以看出,各數(shù)據(jù)庫的磁盤空間占用已處于較高水位,急需需要治理,通過結(jié)轉(zhuǎn)或刪除數(shù)據(jù)來降低磁盤占用比例。
2.3、表空間
數(shù)據(jù)庫存在大表其中一個原因是多條業(yè)務(wù)線共用一個應(yīng)用,同時代碼層面抽象的部分不夠抽象,擴(kuò)展部分又不容易擴(kuò)展,導(dǎo)致數(shù)據(jù)都糅合和一起。
以下是所有的表空間占用情況,可以明顯看到大部分的表數(shù)據(jù)量都在千萬行以上,特別是前7張表的表空間占用都在100個G以上,數(shù)據(jù)行數(shù)也都在億級以上,最多的是status表,30億行數(shù)據(jù),典型的大庫大表。

2.4、QPS情況
黃色的為主庫的QPS,可以看出主庫的查詢量遠(yuǎn)大于從庫,由于各種原因,應(yīng)用代碼里只有少部分的查詢是走的從庫,急需將部分流量大的查詢接口從主庫切到從庫去查詢;

2.5、慢SQL
不論是主庫還是從庫,都有偶發(fā)的慢SQL查詢,引發(fā)磁盤繁忙,影響系統(tǒng)穩(wěn)定性。

三、治理目標(biāo)
1.數(shù)據(jù)結(jié)轉(zhuǎn),降低磁盤使用率,處較低水位運行。
治理目標(biāo):將表空間占用大于100G的7張表(xxx_status、xxx_main、xxx_exception、xxx_product_code、xxx_item、freights_info、xxx_extend)先進(jìn)行集中結(jié)轉(zhuǎn),保留一年數(shù)據(jù)后進(jìn)行常態(tài)化結(jié)轉(zhuǎn),按天結(jié)轉(zhuǎn),將數(shù)據(jù)量保持在365天;
1.降低主庫QPS,保障主庫安全。
治理目標(biāo):將主庫的高頻查詢切換到從庫查詢,使主庫白天QPS降低30%,近一個月上午峰值平均在20k,下午峰值平均在25k;治理的目標(biāo)為:上午峰值15k,下午峰值18k;
1.慢SQL治理,避免導(dǎo)致磁盤繁忙而影響整體業(yè)務(wù)。
治理目標(biāo):10s以上的徹底消除;5s以上的,消除80%;1s以上的消除60%;底數(shù)是過去一個月(1s以上慢sql);
四、治理方案
4.1、大表數(shù)據(jù)結(jié)轉(zhuǎn)
根據(jù)這7張表的業(yè)務(wù)屬性不同,結(jié)轉(zhuǎn)的類型也不相同;比如對于歷史數(shù)據(jù)無意義的,可以將歷史數(shù)據(jù)直接刪除,比如xxx_exception;另外一類是純歷史數(shù)據(jù),比如流水?dāng)?shù)據(jù)xxx_status表,結(jié)轉(zhuǎn)方式是同步大數(shù)據(jù)平臺后就可以刪除;最后是業(yè)務(wù)主數(shù)據(jù),是需要同步大數(shù)據(jù)平臺和需要結(jié)轉(zhuǎn)至歷史庫的,比如main、item和extend表等;
| 表名 | 表空間GB | 索引空間GB | 大數(shù)據(jù) | 結(jié)轉(zhuǎn)類型 | 開始值 | 完成值 |
| xxx_status | 991.65 | 265.29 | 是 | 刪除 | 2020-04-30 01:00:00 | 2022-01-01 |
| xxx_main | 611.80 | 149.91 | 是 | 結(jié)轉(zhuǎn) | 2021-09-30 | 2022-01-01 |
| xxx_exception | 382.80 | 24.65 | 否 | 刪除 | 2018-05-16 20:30:04 | 2022-01-01 |
| xxx_product_code | 244.18 | 61.54 | 是 | 刪除 |
? |
23億 |
| xxx_item | 208.66 | 85.46 | 是 | 結(jié)轉(zhuǎn) | 2016-12-29 13:20:33 | 2022-01-01 |
| xxx_freights_info | 128.78 | 109.03 | 是 | 結(jié)轉(zhuǎn) | 2018-11-29 13:26:00 |
? |
| xxx_extend | 127.36 | 26.07 | 是 | 結(jié)轉(zhuǎn) | 2019-03-29 14:30:00 | 2022-01-01 |
以下的統(tǒng)計表格是在同步大數(shù)據(jù)平臺后集中刪除和結(jié)轉(zhuǎn)的空間釋放情況,在1個月內(nèi)對數(shù)據(jù)量在1億以上并且占用空間在100G以上的7張大表進(jìn)行了刪除和結(jié)轉(zhuǎn)后刪除,使數(shù)據(jù)在保留365天的業(yè)務(wù)承諾時間范圍內(nèi),降低了470G(10%)的磁盤空間占用;

PS:紅色數(shù)字部分為負(fù)值,也就是磁盤的釋放空間。
4.2、攔截?zé)o參數(shù)查詢
運單主檔查詢偶發(fā)會有無任何參數(shù)的查詢,引發(fā)嚴(yán)重慢SQL,造成數(shù)據(jù)庫磁盤繁忙度嚴(yán)重飚高,極大地影響了其他業(yè)務(wù)操作,而由于入口眾多和交叉調(diào)用,如果在入口做參數(shù)校驗工作量及風(fēng)險都比較大,所以采用MyBatis的插件機(jī)制在dao層做攔截,直接拒絕掉無參數(shù)的查詢,上線后就再沒有出現(xiàn)過因無參查詢而出現(xiàn)慢SQL而導(dǎo)致的磁盤繁忙情況;
mybatis-config.xml里的plugin配置:

ParameterInterceptor關(guān)鍵代碼如下:

源代碼如下:
import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.plugin.*; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; /** * MyBatis攔截器,用于攔截SQL查詢無入?yún)⒌膱鼍埃苊馊聿樵? * */ @Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})}) public class ParameterInterceptor implements Interceptor { private final Map mappedStatementIdMap = new HashMap(); @Override public Object intercept(Invocation invocation) throws InvocationTargetException, IllegalAccessException { Object[] queryArgs = invocation.getArgs(); MappedStatement mappedStatement = null; if (queryArgs[0] instanceof MappedStatement) { mappedStatement = (MappedStatement) queryArgs[0]; } if (null != mappedStatement && mappedStatementIdMap.containsKey(mappedStatement.getId())) { // 獲取SQL BoundSql boundSql = mappedStatement.getBoundSql(queryArgs[1]); List parameterMappingList = boundSql.getParameterMappings(); if (parameterMappingList.isEmpty()) { Profiler.businessAlarm(mappedStatement.getId(), "查詢參數(shù)為空"); throw new BadArgumentException("查詢參數(shù)為空,請確認(rèn)入?yún)⑹欠裼兄?); } } return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { for (Object key : properties.keySet()) { mappedStatementIdMap.put(key, 1); } } }
4.3、查詢切從庫
主庫QPS高峰期達(dá)30k/s,長期處于高位運行,需要梳理出TOP10的查接口來切從庫查詢,而應(yīng)用中接口眾多,無法逐個接口查各接口的調(diào)用量,可以利用JSF的filter功能結(jié)合UMP業(yè)務(wù)監(jiān)控來統(tǒng)計provider的調(diào)用次數(shù),再通過Python程序獲取統(tǒng)計數(shù)據(jù)生產(chǎn)統(tǒng)計報表。
JSF的配置文件新增filter
JsfInvokeFilter的代碼:
import com.jd.jsf.gd.filter.AbstractFilter; import com.jd.jsf.gd.msg.RequestMessage; import com.jd.jsf.gd.msg.ResponseMessage; import com.jd.jsf.gd.util.RpcContext; import com.jd.ump.profiler.proxy.Profiler; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; /** * JSF filter * JSF服務(wù)的調(diào)用次數(shù)統(tǒng)計 */ @Component public class JsfInvokeFilter extends AbstractFilter { /** * 按API接口統(tǒng)計方法調(diào)用量 - 業(yè)務(wù)監(jiān)控KEY */ private static final String API_PROVIDER_METHOD_COUNT_KEY = "api.jsf.provider.method.count.key"; private static final String API_CONSUMER_METHOD_COUNT_KEY = "api.jsf.consumer.method.count.key"; @Override public ResponseMessage invoke(RequestMessage requestMessage) { String key; if (RpcContext.getContext().isProviderSide()) { key = API_PROVIDER_METHOD_COUNT_KEY; } else { key = API_CONSUMER_METHOD_COUNT_KEY; } String method = requestMessage.getClassName() + "." + requestMessage.getMethodName(); Map tags = new HashMap(2); tags.put("bMark", method); tags.put("bCount", "1"); Profiler.sourceDataByStr(key, tags); return getNext().invoke(requestMessage); } }
業(yè)務(wù)監(jiān)控點列表

明細(xì)項

Python腳本
import os
import openpyxl
import json
import requests
from cookies import Cookie
import time
headers = {
'Cookie': Cookie,
'Content-Type': 'application/json',
'token': '******',
'erp': '******'
}
def get_jsf(start_time, end_time):
url = 'http://xxx.taishan.jd.com/api/xxx/xxx/xxx/'
body = {}
params = {'startTime': start_time,
'endTime': end_time,
'endPointKey': 'api.jsf.provider.method.count.key',
'quickTime': int((end_time - start_time) / 1000),
'markFlag': 'true',
'markLimit': 500}
res = requests.post(url=url, data=json.dumps(body), params=params, headers=headers)
print('url: ', res.request.url) # 查看發(fā)送的url
# print('response: ', res.text) # 返回請求結(jié)果
res_json = json.loads(res.text)
title = ['序號', 'jsf key', '次數(shù)', '占比%', '峰值', '次/秒', '峰值時間']
i = 0
keys = {}
marks = res_json['response_data']['marks']
for mark in marks:
keys.setdefault(mark, [0, 0, 0, ''])
data = []
records = res_json['response_data']['monitorData']
print(len(records))
for key, value in records.items():
count = 0
max_val = 0
max_time = ''
for val in value:
v = val['value']
count += v
if v > max_val:
max_val = v
max_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(val['dateTime'] / 1000)))
keys[key] = [count, max_val, int(max_val / 1200), max_time]
key_list = sorted(keys.items(), key=lambda x: x[1], reverse=True)
# print(key_list)
all_count = key_list[0][1][0]
for key in key_list:
values = [i, key[0], key[1][0], str(round(key[1][0] / all_count * 100, 2)) + '%', key[1][1], key[1][2], key[1][3]]
data.append(values)
i += 1
#
# # print(data)
#
path = r"/Users/xxx/Documents/治理/QPS治理/"
os.chdir(path) # 修改工作路徑
workbook = openpyxl.Workbook()
sheet = workbook.active
sheet.title = 'JSF接口調(diào)用次數(shù)統(tǒng)計'
sheet.append(title)
for record in data:
sheet.append(record)
workbook.save('JSF接口調(diào)用次數(shù)統(tǒng)計-' + str(start_time / 1000) + '-' + str(end_time / 1000) + '.xlsx')
def change_time(dt):
# 轉(zhuǎn)換成時間數(shù)組
time_array = time.strptime(dt, "%Y-%m-%d %H:%M:%S")
# 轉(zhuǎn)換成時間戳
timestamp = time.mktime(time_array)
return int(timestamp * 1000)
if __name__ == '__main__':
start_time = '2024-03-06 12:20:00'
end_time = '2024-03-07 12:20:00'
get_jsf(change_time(start_time), change_time(end_time))
Cookie的代碼如下:
Cookie = '*****'

分析Top10接口的切從庫方案:
| 序號 | 接口 | 日調(diào)用量 | 占比% | 次/秒 | 涉及到的表 | 是否可以切從庫 | 切從庫方案 |
| 0 | 總調(diào)用量 | 69787485 | 100.0% | 1114 |
? |
? |
? |
| 1 | com.jd.xxx.service.xxx.getLwbMainAndRelatedInfoByLwbNo | 35366937 | 50.68% | 747 | lxxx_main xxx_goods_item extend_info xxx_extend | 是 | 單查詢,在Service層加注解走從庫查詢 |
| 2 | com.jd.xxx.service.xxx.getLwbMainByLwbNo | 12212805 | 17.5% | 235 | xxx_main xxx_main_ext_coldchain xxx_product_code xxx_extend | 是 | 有很多地方引用這個方法,切從庫需要新增API接口,在Service新增的方法上加走從庫注解 |
| 3 | com.jd.xxx.open.xxx.getLwbMainPartByLwbNo | 4138702 | 5.93% | 102 | xxx_main | 是 | 在Service層加注解走從庫查詢 |
| 4 | com.jd.xxx.open.xxx.gotoB2BSWbMainAllTrack | 3929935 | 5.63% | 70 | xxx_main 兩次 xxx_main_ext_coldchain | 是 | 在Service層加注解走從庫查詢 |
| 5 | com.jd.xxx.btp.taskfunnel.handler.Handler.doFilter | 2206697 | 3.16% | 37 |
? |
否 | 接單框架(實現(xiàn)方法太多) |
| 6 | com.jd.xxx.service.xxx.findLwbMainByCondition | 1435493 | 2.06% | 32 | xxx_main 列表查詢 xxx_item 是否查明細(xì) package_added_service package_added_service_item 取舊服務(wù) xxx_pay_main xxx_extend xxx_product_code xxx_main_ext_coldchain | 是 | 有很多地方引用這個方法,切從庫需要新增API接口,在Service新增的方法上加走從庫注解 |
| 7 | com.jd.xxx.open.OmsOrientedService.queryWayBillByLwbNo | 1059754 | 1.52% | 33 | xxx_main freights_info xxx_enquiry_main xxx_status 兩次 xxx_b2b_box_item xxx_coupon 兩次 xxx_extend 積分 | 是 | 在Service層加注解走從庫查詢 |
| 8 | com.jd.xxx.open.SellerOrientedService.getFreightsInfoFromTable | 1008603 | 1.45% | 66 | xxx_main xxx_b2b_package xxx_extend xxx_product_code xxx_main_ext_coldchain xxx_main_ext_site freights_info fee_detail xxx_b2b_box_item | 是 | 在Service層加注解走從庫查詢 |
| 9 | com.jd.xxx.service.xxx.getLwbMain | 817341 | 1.17% | 24 | xxx_main xxx_b2b_package xxx_extend xxx_product_code xxx_main_ext_coldchain xxx_main_ext_site | 是 | 有很多地方引用這個方法,切從庫需要新增API接口,在Service新增的方法上加走從庫注解 |
| 10 | com.jd.xxx.open.OmsOrientedService.getWayBillSettleMode | 730328 | 1.05% | 18 | 無數(shù)據(jù)庫查詢 |
? |
? |
通過優(yōu)化讀操作切換至從庫查詢,降低了主庫30%的QPS流量,白天峰值從25k降低到17.5k;

治理前QPS(峰值25k)

治理后QPS(峰值17.5k)
4.4、慢SQL治理
通過對慢SQL設(shè)定有針對性的治理,成功地徹底消除10s以上的慢SQL;5s以上的,消除80%;1s以上的消除60%。
關(guān)于慢SQL的治理不過多介紹,采用的都是通用分析和治理方法,有很多的文章都有介紹。需要注意的是在治理過程中要做好灰度,完全驗證后再全量上線運行。
五、寫在最后
可能有同學(xué)會想到分庫分表,一個是在規(guī)劃中提前部署分庫分表,一個是現(xiàn)在使用分庫分表技術(shù)進(jìn)行治理;關(guān)于前一個問題由于時間久遠(yuǎn)咱們不做過多討論,關(guān)于未使用分庫分表進(jìn)行治理的原因是業(yè)務(wù)規(guī)劃的問題,目前此應(yīng)用業(yè)務(wù)較為穩(wěn)定,如采用分庫分表治理動作比較大風(fēng)險較高,ROI不高,故以上治理方案以穩(wěn)定為主降低風(fēng)險為輔。
還有一個治理方案是遷云,利用云計算的彈性及快速恢復(fù)等特性降低來運行風(fēng)險,因為業(yè)務(wù)的不可中斷性,此方案必須是在線遷移,涉及雙數(shù)據(jù)庫從雙寫到雙讀,再到單讀,最后單寫,還有數(shù)據(jù)一致性檢查和同步等,成本較高。同時云數(shù)據(jù)庫未能有如此大的磁盤容量和CPU核數(shù),所以此方案需要結(jié)合分庫分表方案同時進(jìn)行,更增加了成本和風(fēng)險,但此方案目前是在計劃中的,如業(yè)務(wù)有較大幅度增長,以上治理也已無法滿足時,將采用遷云加分庫分表,且分庫和分表是分期進(jìn)行推進(jìn)。
六、探討
大家在日常及大促中有其他好的治理方案的話,歡迎發(fā)在評論區(qū)一起探討。
審核編輯 黃宇
-
SQL
+關(guān)注
關(guān)注
1文章
789瀏覽量
46718 -
數(shù)據(jù)庫
+關(guān)注
關(guān)注
7文章
4020瀏覽量
68369 -
MySQL
+關(guān)注
關(guān)注
1文章
906瀏覽量
29560
發(fā)布評論請先 登錄
labview能否對動態(tài)數(shù)組的元素每循環(huán)一次就比較一次大小
一次呼叫典型流程
華為的一次大膽嘗試燒起了一把火,所有手機(jī)都借鑒這個功能
【學(xué)習(xí)打卡】記一次給OpenHarmony提交代碼的過程
記腳本小子的一次滲透全過程
記一次大庫大表的治理過程
評論