做網站seo優(yōu)化百度一下網頁搜索
導言:
京東零售技術團隊通過真實線上案例總結了針對海量數(shù)據批處理任務的一些通用優(yōu)化方法,除了供大家借鑒參考之外,也更希望通過這篇文章呼吁大家在平時開發(fā)程序時能夠更加注意程序的性能和所消耗的資源,避免在流量突增時給系統(tǒng)帶來不必要的壓力。
業(yè)務背景:
站外廣告投放平臺在做推廣管理狀態(tài)優(yōu)化重構的時候,引入了四個定時任務。分別是單元時間段更新更新任務,計劃時間段更新任務,單元預算撞線恢復任務,計劃預算撞線恢復任務。導言:京東零售技術團隊通過真實線上案例總結了針對海量數(shù)據批處理任務的一些通用優(yōu)化方法,除了供大家借鑒參考之外,也更希望通過這篇文章呼吁大家在平時開發(fā)程序時能夠更加注意程序的性能和所消耗的資源,避免在流量突增時給系統(tǒng)帶來不必要的壓力。
時間段更新更新任務:
由于單元上可以設置分時段投放,最小粒度是半個小時,每天沒半個小時都已可以被廣告主設置為可投放或者不可投放,當個廣告主修改了,這個時間段,我們可以通過binlog來異步更新這個狀態(tài),但是,隨著時間的流逝,單元有可能在上半個小時處于可投放狀態(tài),來到下半個小時就處于不可投放狀態(tài)。此時我們的程序是無法感知的,只能通過定時任務,計算每個單元在當前時間段是否需要被更新子狀態(tài)。計劃時間段更新任務類似,也需要半個小時跑一次。
單元預算恢復任務:
當單元的當天日預算被消耗完之后,我們接收到計費的信號后會把該單元的狀態(tài)更新為預算已用完子狀態(tài)。但是到第二天凌晨,隨著時間的到來,需要把昨天帶有預算已用完子狀態(tài)的單元全部查出來,然后計算當前是否處于撞線狀態(tài)進行狀態(tài)更新,此時大部分預算已用完的單元都處于可播放狀態(tài),所以這個定時任務只需要一天跑一次,計劃類似。
本次以單元和計劃的時間段更新為例,因為時間段每半個小時需要跑一次,且數(shù)據量多。
數(shù)據庫:
我們的數(shù)據庫64分片,一主三從,分片鍵user_id(用戶id)。
定時任務數(shù)據源:
我們選取只有站外廣告在用的表dsp_show_status作為數(shù)據源,這個表總共8500萬(85625338)條記錄。包含三層物料層級分別是計劃,單元,創(chuàng)意通過type字段區(qū)分,包含四大媒體(字節(jié),騰訊,百度,快手)和京東播放的物料,可以通過campaignType字段區(qū)分。
機器配置和垃圾回收器:
單臺機器用的8C16G
-Xms8192m -Xmx8192m -XX:MaxMetaspaceSize=1024m -XX:MetaspaceSize=1024m -XX:MaxDirectMemorySize=1966m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=8
定時任務處理邏輯
對于單元,
第一步、先查出來出來dsp_show_status 最大主鍵區(qū)間MaxAutoPk和最小區(qū)間MinAutoPk。
第二步、根據Ducc里設置的步長,和條件,去查詢dsp_show_status表得出數(shù)據。其中條件包含層級單元,騰訊渠道(只有騰訊渠道的單元上有分時段投放),不包含投放已過期的數(shù)據(已過期的單元肯定不在投放時間段)
偽代碼:
startAutoPk=minAutoPk;
while (startAutoPk <= maxAutoPk) {//每次循環(huán)的開始區(qū)間startAutoPkFinal = startAutoPk;//每次循環(huán)的結束區(qū)間endAutoPkFinal = Math.min(startAutoPk + 步長, maxAutoPk);List<showSatusVo> showSatusVoList =showStatusConsumer.betweenListByParam(startAutoPkL, endAutoPkL, 條件(type=2單元層級,不包含已過期的數(shù)據,騰訊渠道))startAutoPk = endAutoPkFinal + 1;
}
第三步、遍歷第二步查詢出來showSatusVoList,得到集合單元ids,然后根據集合ids去批量查詢單元擴展表,取出單元擴展表里每個單元對應的start_time,end_time,time_range_price_coef字段。進行子狀態(tài)計算。
計算邏輯偽代碼:
1、當前時間<start_time, 子狀態(tài)為 單元未開始投放
2、end_time <當前時間 ,子狀態(tài)為 單元投放已結束
3、start_time<當前時間<end_time 且當前時間不在投放時間段 ,子狀態(tài)為單元不在投放時間段
4、其他,移除單元未開始投放,單元投放已結束,單元不在投放時間段 三個子狀態(tài)
然后對這批單元按上面的四種情況進行分組,總共分為四組。如果查詢來的dsp_show_status表的子狀態(tài)和算出來的子狀態(tài)一樣則不加入分組,如果不一樣則加入相應分組。
最后對這批單元對應的dsp_show_status表里的記錄進行四次批量更新。
計劃時間段任務處理邏輯類似,但是查詢出來的數(shù)據源不包含騰訊渠道的,因為騰訊的渠道的時間段在單元上,計劃上沒有。
任務執(zhí)行現(xiàn)象:
(一階段)任務執(zhí)行時間長且CPU利用率高
按某個pin調試任務,邏輯上落數(shù)據沒有問題,但是任務時長在五分鐘左右。當時是說產品可以接受這個時間子狀態(tài)更新延遲。
但當不按pin調試進行計劃時間段任務更新時,相對好點,十分鐘左右,cpu不到50%。
進行單元時間段任務更新時,機器的cpu是這樣的:
cpu80%,且執(zhí)行了半個小時才執(zhí)行完成。如果這樣,按業(yè)務需求,這個批次執(zhí)行完成就要繼續(xù)執(zhí)行下一次了,肯定是不滿需求的。
那怎么縮短CPU利用率,縮短任務執(zhí)行時間呢?聽我慢慢講解。
(二階段)分析數(shù)據源,調大步長縮短任務運行時間
上面這個情況肯定滿足不了業(yè)務需求的。
第一感覺優(yōu)化的方向應該往著數(shù)據分布上想,于是去分析dsp_show_status表里的數(shù)據,發(fā)現(xiàn)表里數(shù)據稀疏主要是因為兩個點。
(1)程序問題。這個表里不僅存在站外的數(shù)據,還因為某些程序問題無意落了站內的數(shù)據。我們查詢數(shù)據的時候卡了計劃類型,不會處理站內的數(shù)據。但是表里存在會增大主鍵區(qū)間。導致我們每個批次出來的數(shù)據比較稀疏。
(2)業(yè)務背景。由于百度量小,字節(jié)則最近進行了升級,歷史物料不多,快手之前完全處于停投。所以去除出騰訊渠道,計劃需要處理的數(shù)據量比較少18萬(182934)。但是騰訊側一直沒有進行升級,而且量大,所以需要處理的單元比較多130萬左右(1309692 )。
于是我們?yōu)榱吮苊饷總€批次查出來要處理數(shù)據比較少,導致空跑,調大了步長。
再次執(zhí)行任務
果然有效,計劃時間段任務計,cpu雖然上去了,但是任務5分鐘就執(zhí)行完了。
執(zhí)行執(zhí)行單元時間段更新的時候,時間縮短到十幾分鐘,但是cpu卻是這樣的,頂著100%cpu跑任務。
道路且長,那我們怎么解決這個cpu問題呢,請看下一階段。
(三階段)減少臨時對象大小和無效日志,避免多次ygc
這個cpu確實令人悲傷。當時我們
第一想法是,為了盡快滿足產品需求,先用我們的組件事件總線進行負載(底層是用的mq)到多臺機器。這樣不但解決了cpu利用率高的問題,還能解決任務執(zhí)行時間長的問題。這個想法確實能解決問題,但是還是耗用機器資源。
第二想法是,由于時間段在表里是個json存儲,在執(zhí)行查詢的時候不好進行條件查詢。于是想著單獨在建一張表,拉平時間段,在進行查詢的時候直接查新建的表,不再查詢存儲json時間段的表。但是這張表相當于異構了數(shù)據源,不但要新建表還要考慮這張表的維護。
于是我們繼續(xù)分析cpu高用在哪里,理論上這個定時任務是IO型任務,cpu利用率應該比較低。在執(zhí)行任務的時候,我們仔細觀察了機器的監(jiān)控,發(fā)現(xiàn)在執(zhí)行單元時段更新任務時,機器每分鐘不斷地進行多次ygc。之前剛和組內同學分享過gc相關知識。這里說一下,雖然我們的機器用的是G1垃圾回收器,沒有進行full gc,但是G1在ygc的時候會比jdk1.8默認的垃圾回收器要更耗資源,因為G1還要mixgc兼顧回收老年代的垃圾。G1用于響應優(yōu)先,默認的垃圾回收器吞吐量優(yōu)先。這樣的批量任務其實更適合用默認垃圾回收器。
不斷進行ygc肯定是因為我們在執(zhí)行任務的時候產生大量的臨時對象導致的。
這里我們采取了兩條有效措施:
(1)去掉無效日志。由于調試時加了大量日志,java進行序列化的時候會產生比原來的對象占用更多內存的臨時變量。于是我們去掉了所有的無效日志。
(2)減少臨時對象占用的內存。代碼對象的個數(shù)肯定不能減少,于是我們我們減少對象的的大小。之前是我們用的proxy工程現(xiàn)成接口,把表里的每個字段都查出來了,但是表里那么多字段,實際我們每張表也就用2-3個字段。于是我們?yōu)檫@個定時任務寫了專用的查詢接口,每個接口只查我們需要的字段。
結果果然有效,單元時間段更新任務從原來的頂著100%cpu跑了十幾分鐘,瞬間降到了cpu不到60%,五分鐘執(zhí)行完成。ycg次數(shù)也有明顯的下降。
刷數(shù)任務:這兩個措施到底多有效呢,說另一個栗子也與這個需求相關。在沒有減少臨時變量大小(把單元表和單元擴展表中的所有字段都查出來)把單元表的啟停狀態(tài)和單元擴展表的審核狀態(tài)刷到dsp_show_status時,涉及1400百萬數(shù)據,刷了兩個小時也沒刷完,最后怕影響物料傳輸工程查詢數(shù)據庫給停了。之后減少臨時變量后,九分鐘就刷完了。
經過上述的優(yōu)化看似皆大歡喜,但還存在很大的問題。給大家看一個監(jiān)控圖。
看完這個監(jiān)控圖,我們慌了,計劃和單元更新時間段任務每半個小時運行一次,都給數(shù)據庫帶來了200萬qpm的增長,這無疑給我們的數(shù)據庫帶來了巨大隱患。
此時總結下來存在兩個問題有待解決。
(1)、怎么減少與數(shù)據庫的交互次數(shù),消除給數(shù)據庫帶來的安全隱患。
(2)、怎么降低任務的執(zhí)行的時間,五分鐘的子狀態(tài)更新延遲是不可以接受的。對廣告主來說更是嚴重的bug。
這兩個問題讓我們覺得這個任務還有很大的優(yōu)化空間,于是我們繼續(xù)分析優(yōu)化。下一階段的措施很好的解決了這兩個問題。
(四階段)基于游標查詢數(shù)據源,基于數(shù)據庫分片批量更新,降低數(shù)據庫交互次數(shù),避免空跑縮短任務運行時間。
對于上面的問題(1),我們分析這么大的調用量主要用在了哪里。
發(fā)現(xiàn)由于站內數(shù)據的存在和歷史數(shù)據的刪除以及dsp_show_status和其他表公用一個主鍵id生成序列,導致dsp_show_status表的MaxAutoPk到達90多億。
也就是所及時我們步長達到2萬,光查詢數(shù)據調用次數(shù)就達到了45萬次,在加上每次都有可能產生小于四次的更新操作。也就是一個定時任務都會產生高大100萬的qpm,兩個任務產生200萬也就符合預期了。于是我們把步長調整為4萬,qpm降到了130萬左右,但還是很高。
于是我們繼續(xù)分析,就單元時間段更新任務而言,其實我們需要查出來的數(shù)據也就是上面提到的騰訊的130萬左右(1309692 )。但是我們查詢了45萬次且步長是2萬。也就是說我們每次查出來的數(shù)據還是很稀疏且個數(shù)不確定,如果忙盲目的調大步長,很可能由于某個區(qū)間數(shù)據量特別多導致負載不均衡,還有可能rpc超時。
那怎么才能做到每次查出來數(shù)據個數(shù)就是我們的設置的步長呢,我們想到了mysql里面的游標查詢。但是jed彈性數(shù)據庫并不支持,于是我們就要手動實現(xiàn)游標的邏輯。此時我們考慮dsp_show_status是否有唯一主鍵能標識唯一記錄。假如主鍵不唯一,就有可能出現(xiàn)漏查和重復查詢的情況。幸運的是我們的jed數(shù)據庫所有的表里都有唯一主鍵。于是我們手寫了一個游標查詢。
(1)游標查詢
偽代碼如下
//上層業(yè)務代碼
Long maxId = null;
do {showStatuses = showStatusConsumer.betweenListByParam(startAutoPkL, endAutoPkL, maxId,每次批次要查出來的數(shù)據,其他條件(type=2單元層級,不包含已過期的數(shù)據,騰訊渠道))if (CollectionsJ.isEmpty(showStatuses)) {//如果為空的,直接推出,代表已經查到最后了。break;}//循環(huán)變量值疊加,查出來的數(shù)據最后一行的id,數(shù)據庫進行了升序,也就是這批記錄的最大idmaxId = showStatuses.get(showStatuses.size() - 1).getId();//處理查出來的數(shù)據processShowStatuses( showStatuses);} while (CollectionsJ.isNotEmpty(showStatuses));//下層sql</select>SELECTid,cga_id,status_bitmap1,user_idFROM dsp_show_status<where>id BETWEEN #{startAutoPk,jdbcType=BIGINT} AND #{endAutoPk,jdbcType=BIGINT}//param.maxId 上一批次查出數(shù)據的最大maxId<if test="param.maxId != null">AND id >#{param.maxId,jdbcType=BIGINT}</if><----!其他條件------></where>order by id<if test="param.batchSize != null">//上層傳過來的每個批次要查詢的出來的數(shù)據量limit #{param.batchSize}</if></select>
這里可以思考一下基于游標的查詢方式在什么場景下有效? 如果有效需要滿足一下兩個條件
1、jed表里有唯一鍵,且基于唯一鍵查詢排序
2、區(qū)間滿足查詢條件的記錄越稀疏越有效
這里要一定注意排序的順序,是升序不是降序。如果你無意間按降序排序,那么每次查詢的都是最后的滿足條件的batch大小的數(shù)據。
(2)深度分頁引起慢sql
此時組內同學提出了一個疑問,深度分頁引起慢sql問題。這里解釋一下到底會不會產生慢sql。
當進行分頁的時候一般sql會這樣寫
select *
from dsp_show_status
where 其他查詢條件
limit 50000000 , 10;
當limit 的初始位置非??亢髸r,即使壓中查詢條件里的二級索引,也需從二級索引得到的主鍵索引去加載所有的磁盤記錄,然后掃描50000000行記錄取50000000到-50000010條返回,這里涉及到記錄的掃描,和多次磁盤到內存的IO,所以比較耗時。
但是我們的sql
select *
from dsp_show_status
where 其他查詢條件
and id >maxId
oder by id
limit 100
當maxId非常大時,比如50000000 時,mysql壓中查詢條件的里的二級索引,得到主鍵索引。然后MySQL會直接過濾掉 id<50000000 的主鍵id,然后從主鍵50000000開始查詢數(shù)據庫得到滿足條件的100條記錄。所以他會非???#xff0c;并不是產生慢sql。實際sql執(zhí)行只需要37毫秒。
(3) 按數(shù)據庫分片進行批量更新
但是又遇到了另一個數(shù)據庫長事務問題,由于使用了基于游標的方式,查出來的數(shù)據都是需要進行計算的數(shù)據,且任務運行時間縮短到到30秒。那在進行數(shù)據更新時,每次批量更新都比之前(不使用游標的方式)更新的數(shù)據量要多,且并發(fā)度高。其次由于批量更新的時候更新多個單元id,這些id不一定屬于某一個user_id,所以在執(zhí)行更新的時候沒有帶分片鍵,此時數(shù)據庫jed網關又出現(xiàn)了問題。
當時業(yè)務日志的報錯的信息是這樣的,出現(xiàn)了執(zhí)行時間超過了30秒的sql,被kill掉:
{"error":true,"exception":{"@type":"org.springframework.jdbc.UncategorizedSQLException","cause":{"@type":"com.mysql.cj.jdbc.exceptions.MySQLQueryInterruptedException","errorCode":1317,"localizedMessage":"transaction rolled back to reverse changes of partial DML execution: target: dsp_ads.c4-c8.primary: vttablet: (errno 2013) due to context deadline exceeded, elapsed time: 30.000434219s, killing query ID 3511786 (CallerID: )","message":"transaction rolled back to reverse changes of partial DML execution: target: dsp_ads.c4-c8.primary: vttablet: (errno 2013) due to context deadline exceeded, elapsed time: 30.000434219s, killing query ID 3511786 (CallerID: )","sQLState":"70100","stackTrace":[{"className":"com.mysql.cj.jdbc.exceptions.SQLError","fileName":"SQLError.java","lineNumber":126,"methodName":"createSQLException","nativeMethod":false},{"className":"com.mysql.cj.jdbc.exceptions.SQLError","fileName":"SQLError.java","lineNumber":97,"methodName":"createSQLException","nativeMethod":false},
數(shù)據庫的監(jiān)控也發(fā)現(xiàn)了異常,任務執(zhí)行的時候出現(xiàn)了大量的MySQL rollbakc:
當時聯(lián)系dba suport ,dba排查后告訴我們,我們的批量更新sql在數(shù)據庫執(zhí)行非???#xff0c;但是我們用了長事務超過30秒沒有提交,所以被kill掉了。但是我們檢查了我們的代碼,發(fā)現(xiàn)并沒有使用事務,且我們的事務是單庫跨rpc事務,從發(fā)起事務到提交事務對于數(shù)據庫來說執(zhí)行時間非???#xff0c;并不會出現(xiàn)長事務。我們百思不得其解,經過思考我們覺得可能是jed網關出現(xiàn)了問題,jed網關的同學給的答復是。由于沒有帶分片鍵導致jed網關會把sql分發(fā)到64分片,如果某個分片上沒有符合條件的記錄,就會產生間隙鎖,其他sql更新的時候一直鎖更待從而導致事務一直沒有提交出現(xiàn)長事務。
對于網關同學給我們的答復,我們仍然持有懷疑態(tài)度。本來我們想改下數(shù)據庫的隔離級別驗證一下這個回復,但是jed并不支持數(shù)據庫隔離級別的更改。
但是無論如何我們知道了是因為我們批量更新時不帶分片鍵導致的,但是如果按userId進行更新,將會導致原來只需要一次進行更新,現(xiàn)在需要多次更新。于是我們想到循環(huán)64分片數(shù)據庫進行批量更新。但是jed并不支持執(zhí)行sql時指定分片,于是我們給他們提了需求。
后來我們想到了折中的方式,我們按數(shù)據庫分片對要執(zhí)行的單元id進行分組,保證每個分組對應的單元id落到數(shù)據庫的一個分片上,并且執(zhí)行更新的時候加上userId集合。這個方案要求jed網關在執(zhí)行帶有多個分片鍵sql時能進行路由。這邊jed的同事驗證了一下是可以的。
于是我們在進行更新的時候對這些ids按數(shù)據庫分片進行了分組。
偽代碼如下:
//按數(shù)據庫分片進行分組
adgroups.stream().collect(Collectors.groupingBy(Adgroup::shardKey));
// 按計算每個userId對象的數(shù)據庫分片,BinaryHashUtil是jed網關的jar包
public String shardKey() {try {return BinaryHashUtil.getShardByVindex(ShardEnum.SIXTY_FOUR_SHARDS, this.userId);} catch (SQLException ex) {throw new ApplicationException(ex);}}
在上述的刷數(shù)任務中能夠執(zhí)行那么快,并且更新數(shù)據沒有報錯,一方面也得益于這個按數(shù)據庫分片進行分組更新數(shù)據。
(4)優(yōu)化效果
經過基于游標查詢的方式進行任務優(yōu)化,就單元時間段更新時。從原來的五分鐘,瞬間降為30秒完成。cpu不到65%。由于計劃記錄更稀疏,所以更快。
對數(shù)據庫的查詢更新操作,也從原來的也從原來的200萬qpm降為2萬多(早上高峰的時候),低峰的時候甚至不到兩萬。當我們把batchSize設置為100時,通過計算單元的130多萬/100 +計劃的18萬/100=1.4萬次qpm 也是符合預期的。
查詢db監(jiān)控:
更新db的監(jiān)控,也符合預期
雖然引入基于游標的方式進行查詢非常有效,把原來的200萬qpm數(shù)據庫交互降到了2萬,把任務運行時間從5分鐘降到了30秒。但是仔細分析你還會發(fā)現(xiàn),還存在如下問題。
1、單臺機器cpu高,仍然在60%,對于健康的程序來說,這個數(shù)值仍然不被接受。
2、查詢和更新數(shù)據量嚴重不符,每次定時任務更新只更新了上萬行記錄,但是我們卻查出來了上百萬(130萬)行記錄進行子狀態(tài),這無疑還在浪費CPU和磁盤IO資源。
監(jiān)控如下
每次查詢出來的記錄數(shù):
每次需要更新的記錄數(shù):
經過上面的不斷優(yōu)化,我們更加相信,資源不能被浪費,作為程序員應該追求極致。于是我們還繼續(xù)優(yōu)化。解決上面兩個問題
(五階段)異構要更新狀態(tài)的數(shù)據源,降低數(shù)據庫交互次數(shù),降低查詢出來的數(shù)據量,降低機器cpu利用率。
為了減少無效數(shù)據查詢和計算,我們還是決定冗余數(shù)據,但是不是像前面提到的新建一張表,而是在dsp_show_status 表里冗余一個nextTime字段,來存儲這個物料下一次需要被定時任務拉起更改狀態(tài)的時間戳,(也就是物料在投放時間段子狀態(tài)和不在投放時間段子狀態(tài)轉變的時間戳),舉個栗子,廣告主設置某個單元早上8點開始投放,晚上8點結束投放,其他時間不投放。那早8點的時候,這個單元就會被我們的定時任務掃描到,然后計算更新這個單元從不投放變?yōu)橥斗?#xff0c;同時計算比較投放時間段,下一個狀態(tài)變更的時間段,經過計算得知,廣告主在晚上8點需要狀態(tài)變更,也就是從投放變?yōu)椴煌斗?#xff0c;那nextTime字段就落晚上8點的時間戳。這個字段的維護邏輯分為兩部分,一部分是廣告主主動更改了時間段需要更新計算這個nextTime,另一部分是定時任務拉起這個物料更改完子狀態(tài)后,再次計算下一次需要被拉起的nextTime。
這樣我們定時任務在查詢數(shù)據源的時候只需新增一個查詢條件(因為是存的是時間戳,所以需要卡個范圍)就可以查出我們需要真正要更新的數(shù)據了。
當維護投放時間段這個異構數(shù)據,就要考慮異構數(shù)據和源數(shù)據的一致性問題。假如某次定時任務執(zhí)行失敗了,就會導致nextTime 和投放時間段數(shù)據不一致,此時我們的解決辦法時,關閉基于nextTime的優(yōu)化查詢,進行上一階段(第四階段)基于游標的全量更新。
sql查詢增加條件:
next_time_change between ADDTIME(#{param.nextTimeChange}, '-2:0:0')
and ADDTIME(#{param.nextTimeChange}, '0:30:0')
優(yōu)化之后我們每次查詢出來的記錄從130萬降到了1萬左右。
11點的時候計劃和單元總共查出來6000個,監(jiān)控如下:
11點的時候計劃和單元總共更新5000個,由于查詢數(shù)據源的時候卡了時間戳范圍,所以符合預期,查出來的個數(shù)基本就是要更新的記錄。監(jiān)控如下:
查詢次數(shù)也從原來的1萬次降到了200次。監(jiān)控如下:
機器的監(jiān)控如下cpu只用了28%,且只ygc了1次,任務執(zhí)行時間30秒內完成。
這個增加next_time 這個字段進行查詢的思路,和之前做監(jiān)控審核中的創(chuàng)意定時任務類似。創(chuàng)意表20億行數(shù)據,怎么從20億行記錄表里實時找出哪些創(chuàng)意正在審核中。當時的想法也是維護一個異構的redis數(shù)據源,送審的時候把數(shù)據寫入redis,審核消息過來后再移除。但是當我們分析數(shù)據源的時候,幸運的發(fā)現(xiàn)審核中的創(chuàng)意在20億數(shù)據中只占幾萬,大部分創(chuàng)意都是在審核通過和審核駁回,之前大家都了解到建立索引要考慮索引的區(qū)分度,但是在這種數(shù)據分布嚴重不均勻的場景,我們建立yn_status聯(lián)合索引,在取數(shù)據源的時候,直接壓數(shù)據庫索引取出數(shù)據,sql執(zhí)行的非???#xff0c;20毫秒左右就能執(zhí)行完成,避免走了很多彎路。
你以為優(yōu)化結束了? 不,合格的程序員怎么允許系統(tǒng)中存在cpu不穩(wěn)定的場景存在,即使只增加28%。
(六階段)負載均衡,消除所有風險,讓系統(tǒng)程序穩(wěn)定運行。
消除單臺機器cpu不穩(wěn)定的最有效辦法就是,把大任務拆分為小任務,然后分發(fā)到不同的機器上進行執(zhí)行。我們的定時任務本來就是按批次進行查詢計算的,所以本身就是小任務。剩下的就是分發(fā)任務,很多人想到的就是利用mq的負載進行分發(fā),但是mq不可控,不可控制失敗重試時間。如果一個小任務失敗了,下次什么時候被拉起重試就不得而知了,或許半個小時以后?這里用到了我們非常牛逼的一個組件,可重試總線進行負載,支持自定義重試頻率,支持自動識別無效重試,防止重試疊加。
負載后的機器cpu是這樣的
優(yōu)化效果數(shù)據匯總:
這里列一下任務從寫出來到被優(yōu)化后的數(shù)據對比。
優(yōu)化前,cpu增加80%,任務運行半個小時,查詢數(shù)據庫次數(shù)百萬次,查詢出來130萬行記錄。
優(yōu)化后,cpu增加1%,任務30秒以內,查詢數(shù)據庫200次,查詢出來1萬行記錄。
寫到最后:
通過本次優(yōu)化讓我收獲許多,最大的收獲是讓我深刻明白了,對于編碼人員,要時刻考慮資源的消耗。舉個不太恰當?shù)睦踝?#xff0c;假如每個人在工程里都順手打印一行無效日志,隨著時間的積累整個工程都會到處打印在無效日志。毫不夸張的講,或許只是因為你多打印了一行l(wèi)og.info日志,在請求量猛增達到一定程度時都會導致機器和應用的不良連鎖反應。建議大家在開發(fā)的時候在關鍵點加上關鍵日志,并且合理利用Debugger,結合ducc進行動態(tài)日志調整排查問題。
作者:京東零售廣告研發(fā) 董舒展
來源:京東零售技術?轉載請注明來源