<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          我如何用兩行代碼節(jié)省了30%的CPU

          共 10866字,需瀏覽 22分鐘

           ·

          2023-07-07 14:34

          將滴滴技術(shù)設(shè)為“ 星標(biāo)??

          第一時(shí)間收到文章更新


          ClickHouse 是一個(gè)開(kāi)源的用于實(shí)時(shí)數(shù)據(jù)分析高性能列式分布式數(shù)據(jù)庫(kù), 支持向量化計(jì)算引擎、多核并行計(jì)算、高壓縮比等,在分析型數(shù)據(jù)庫(kù)中單表查詢性能第一。滴滴從2020年開(kāi)始引進(jìn)Clickhouse,服務(wù)網(wǎng)約車及日志檢索等核心業(yè)務(wù),節(jié)點(diǎn)數(shù)300+,每天PB級(jí)別的數(shù)據(jù)寫(xiě)入,每天千萬(wàn)級(jí)別的查詢量,其中最大的集群有200+節(jié)點(diǎn)。本篇文章主要介紹Clickhouse在性能優(yōu)化上的一個(gè)點(diǎn),從發(fā)現(xiàn)問(wèn)題到最后解決問(wèn)題的過(guò)程,并獲取較好的收益。


          01



          發(fā)現(xiàn)問(wèn)題


          線上節(jié)點(diǎn)負(fù)載比較高,需要定位CPU主要用在什么地方。首先需要確認(rèn)的是哪個(gè)模塊占用了CPU,在Clickhouse中比較耗CPU的主要是查詢、寫(xiě)入和Merge等模塊。使用top命令定位出占用CPU最高的進(jìn)程,定位到進(jìn)程后在使用??top -Hp pid 命令, 查看占用 CPU 最高的線程,如下圖:

          3e4ab4f827878221cbe29e49a9c0b57a.webp


          1、排在第一是BackgrProcPool線程是負(fù)責(zé)執(zhí)行ReplicatedMergeTree表的merge和mutation任務(wù),需要處理大量的數(shù)據(jù)。


          2、排在第二是HTTPHandler線程是負(fù)責(zé)處理客戶的http請(qǐng)求,包括查詢解析、優(yōu)化及執(zhí)行計(jì)劃的生成等,最終生成的物理執(zhí)行計(jì)劃會(huì)交由QueryPipelineEx線程來(lái)執(zhí)行。


          3、接著往下看,會(huì)發(fā)現(xiàn)連續(xù)6個(gè)BackgrProcPool線程分別占用30%多的CPU,他們主要是負(fù)責(zé)磁盤(pán)間的數(shù)據(jù)移動(dòng),當(dāng)磁盤(pán)使用率超過(guò)了設(shè)定的閥值(默認(rèn)是90%),BgMoveProcPool線程就會(huì)將該磁盤(pán)上的Part文件移動(dòng)到其他的磁盤(pán),同時(shí)如果對(duì)表設(shè)置了Move TTL,當(dāng)Part的數(shù)據(jù)過(guò)期后就會(huì)將該P(yáng)art移動(dòng)到目標(biāo)磁盤(pán),主要用來(lái)實(shí)現(xiàn)數(shù)據(jù)的冷熱分離。BgMoveProc線程池默認(rèn)最大的線程數(shù)是8,負(fù)責(zé)所有MergeTree表磁盤(pán)間數(shù)據(jù)的移動(dòng)。


          4、圖中剩下的ZookeeperSend線程和ZookeeperRecv線程分別是負(fù)責(zé)發(fā)送對(duì)ZK的操作請(qǐng)求及接收對(duì)應(yīng)操作的響應(yīng),ReplicatedMergeTree 表的副本同步機(jī)制就依賴ZK來(lái)實(shí)現(xiàn)的。Clickhouse中還有很多其他的線程,這里就不再一一的介紹。


          top 命令持續(xù)監(jiān)聽(tīng)了一段時(shí)間,發(fā)現(xiàn)這8個(gè)BgMoveProPool線程的CPU占用幾乎一直是排在前面的,難道有磁盤(pán)的使用率已經(jīng)達(dá)到90%了,所有的Move線程都在磁盤(pán)間搬遷 數(shù)據(jù)? 但是線上磁盤(pán)使用到了80%就會(huì)報(bào)警,難道報(bào)警有問(wèn)題?


          使用?df -h 命令查看了磁盤(pán)的使用情況,執(zhí)行后發(fā)現(xiàn)12塊磁盤(pán)的使用率都在50%左右,這就很奇怪了,磁盤(pán)的空間是充足的且線上的集群也沒(méi)有配置冷熱分離,按道理BgMoveProcPool線程就不應(yīng)該占用CPU,究竟在做什么呢?


          02



          確認(rèn)問(wèn)題


          為了搞清楚BgMoveProcPool線程到底在執(zhí)行什么,使用pstack pid命令抓取此時(shí)的堆棧,多次打印堆棧發(fā)現(xiàn)BgMoveProcPool線程都處于MergeTreePartsMover::selectPartsForMove方法中,堆棧如下:

                    
                      #0  0x00000000100111a4 in DB::MergeTreePartsMover::selectPartsForMove(std::__1::vector<DB::MergeTreeMoveEntry, std::__1::allocator<DB::MergeTreeMoveEntry> >&, std::__1::function<bool (std::__1::shared_ptr<DB::IMergeTreeDataPart const> const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >*)> const&, std::__1::lock_guard<std::__1::mutex> const&) ()
                    
                    
                      #1  0x000000000ff6ef5a in DB::MergeTreeData::selectPartsForMove() ()
                    
                    
                      #2  0x000000000ff86096 in DB::MergeTreeData::selectPartsAndMove() ()
                    
                    
                      #3  0x000000000fe5d102 in std::__1::__function::__func<DB::StorageReplicatedMergeTree::startBackgroundMovesIfNeeded()::{lambda()#1}, std::__1::allocator<{lambda()#1}>, DB::BackgroundProcessingPoolTaskResult ()>::operator()() ()
                    
                    
                      #4  0x000000000ff269df in DB::BackgroundProcessingPool::workLoopFunc() ()
                    
                    
                      #5  0x000000000ff272cf in _ZZN20ThreadFromGlobalPoolC4IZN2DB24BackgroundProcessingPoolC4EiRKNS2_12PoolSettingsEPKcS7_EUlvE_JEEEOT_DpOT0_ENKUlvE_clEv ()
                    
                    
                      #6  0x000000000930b8bd in ThreadPoolImpl<std::__1::thread>::worker(std::__1::__list_iterator<std::__1::thread, void*>) ()
                    
                    
                      #7  0x0000000009309f6f in void* std::__1::__thread_proxy<std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, void ThreadPoolImpl<std::__1::thread>::scheduleImpl<void>(std::__1::function<void ()>, int, std::__1::optional<unsigned long>)::{lambda()#3}> >(std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, void ThreadPoolImpl<std::__1::thread>::scheduleImpl<void>(std::__1::function<void ()>, int, std::__1::optional<unsigned long>)::{lambda()#3}>) ()
                    
                    
                      #8  0x00007ff91f4d5ea5 in start_thread () from /lib64/libpthread.so.0
                    
                    
                      #9  0x00007ff91edf2b0d in clone () from /lib64/libc.so.6
                    
                  


          多次抓取BgMoveProcPool線程都在執(zhí)行selectPartsForMove方法,說(shuō)明selectPartsForMove方法耗時(shí)很長(zhǎng),通過(guò)方法名可以了解這個(gè)方法是在查找可以move的Part,接著查詢system.part_log表查看MovePart的記錄。

                    
                      SELECT * FROM system.part_log WHERE event_time > now() - toIntervalDay(1) AND event_type = 'MovePart'
                    
                  


          執(zhí)行上述SQL查詢最近一天的MovePart的記錄,沒(méi)有匹配到一條。到這里我們幾乎可以確定BgMoveProcPool線程一直在查詢可以移動(dòng)的Part,但結(jié)果都空,CPU一直在做無(wú)效的計(jì)算。根據(jù)上面的分析已經(jīng)定位到出現(xiàn)問(wèn)題的代碼,接下來(lái)就是研究selectPartsForMove的源碼,如下:

                    
                      bool MergeTreePartsMover::selectPartsForMove(MergeTreeMovingParts & parts_to_move, const AllowedMovingPredicate & can_move, const std::lock_guard<std::mutex> & /* moving_parts_lock */) {
                    
                    
                          std::unordered_map<DiskPtr, LargestPartsWithRequiredSize> need_to_move;
                    
                    
                          ///  1. 遍歷所有的disk,將使用率超過(guò)閥值的disk添加need_to_move中
                    
                    
                          if (!volumes.empty()) {
                    
                    
                              for (size_t i = 0; i != volumes.size() - 1; ++i) {
                    
                    
                                  for (const auto & disk : volumes[i]->getDisks()) {
                    
                    
                                      UInt64 required_maximum_available_space = disk->getTotalSpace() * policy->getMoveFactor(); /// move_factor默認(rèn)0.9
                    
                    
                                      UInt64 unreserved_space = disk->getUnreservedSpace();
                    
                    
                                      if (unreserved_space < required_maximum_available_space)
                    
                    
                                          need_to_move.emplace(disk, required_maximum_available_space - unreserved_space);
                    
                    
                                  }
                    
                    
                              }
                    
                    
                          }
                    
                    
                          /// 2. 遍歷所有的part,首先如果Part的MoveTTL已過(guò)期則添加到需要移動(dòng)的集合parts_to_move中,否則為超過(guò)閾值的disk添加候選Part
                    
                    
                          time_t time_of_move = time(nullptr);
                    
                    
                          for (const auto & part : data_parts) {
                    
                    
                              /// 檢查該part能否被move, 
                    
                    
                              if (!can_move(part, &reason))
                    
                    
                                  continue;
                    
                    
                      
                        

          /// 檢查part的move_ttl auto ttl_entry = data->selectTTLEntryForTTLInfos(part->ttl_infos, time_of_move); auto to_insert = need_to_move.find(part->volume->getDisk()); if (ttl_entry) { /// 已過(guò)期,則需要移動(dòng)到目標(biāo)磁盤(pán) auto destination = data->getDestinationForTTL(*ttl_entry); if (destination && !data->isPartInTTLDestination(*ttl_entry, *part)) reservation = data->tryReserveSpace(part->getBytesOnDisk(), data->getDestinationForTTL(*ttl_entry)); } if(reservation) /// 需要移動(dòng) parts_to_move.emplace_back(part, std::move(reservation)); else { /// 候選Part if (to_insert != need_to_move.end()) to_insert->second.add(part); } } /// 3. 為候選的Part申請(qǐng)空間并添加到需要移動(dòng)的集合parts_to_move中 for (auto && move : need_to_move) { for (auto && part : move.second.getAccumulatedParts()) { auto reservation = policy->reserve(part->getBytesOnDisk(), min_volume_index); if (!reservation) ????????????????break;
          parts_to_move.emplace_back(part, std::move(reservation)); ++parts_to_move_by_policy_rules; parts_to_move_total_size_bytes += part->getBytesOnDisk(); } }


          SelectPartsForMove方法主要做3件事:

          • 首先遍歷所有的disk,將使用率超過(guò)閥值的disk添加到need_to_move中。

          • 然后遍歷所有的part,首先如果Part的MoveTTL已過(guò)期則添加到需要移動(dòng)的集合parts_to_move中,否則為超過(guò)閾值的disk添加候選Part。

          • 最后為候選的Part申請(qǐng)空間并添加到需要移動(dòng)的集合parts_to_move中。


          其中耗時(shí)最長(zhǎng)的是第二步,會(huì)隨著表Part數(shù)的增加而增加,接著查詢了system.parts,發(fā)現(xiàn)總共有30多萬(wàn)的part,最大的表有6萬(wàn)多個(gè)part,為什么那么耗時(shí)就不奇怪了。


          到這里問(wèn)題就很明顯了,BgMoveProcPool線程不斷的在檢查這30多萬(wàn)個(gè)part是否符合移動(dòng)的條件,但每次都沒(méi)有一個(gè)part符合條件,一直在做無(wú)效的計(jì)算。


          03



          解決問(wèn)題


          線上節(jié)點(diǎn)磁盤(pán)空間很充足且未設(shè)置數(shù)據(jù)的冷熱分層,就不需要浪費(fèi)CPU去檢查每個(gè)part。

          當(dāng)沒(méi)有磁盤(pán)使用率達(dá)到90%得到的need_to_move為空,沒(méi)有設(shè)置冷熱分層,即move_ttl為空,當(dāng)兩個(gè)條件都成立的時(shí)候是不是就可以不用去檢查所有的part,就能節(jié)省大量的重復(fù)計(jì)算了,于是在遍歷檢查part之前添加下面兩行代碼,當(dāng)need_to_move為空且move_ttl為空,就直接返回false。

                    
                      if (need_to_move.empty() && !metadata_snapshot->hasAnyMoveTTL())
                    
                    
                          return false;
                    
                  



          04



          實(shí)際效果


          發(fā)布到國(guó)內(nèi)公共集群,接著使用top命令觀察各個(gè)線程消耗的CPU,可以發(fā)現(xiàn)在前面已經(jīng)找不到BgMoveProcPool線程了,8個(gè)BgMoveProcPool線程占用的CPU也從之前的30%左右都降到了4%以下。


          9b273d7392c4a60d85b164610c371b02.webp


          再來(lái)觀察一下機(jī)器整體的CPU,可以清晰的發(fā)現(xiàn)CPU由升級(jí)前的20%左右降到了10%左右,并且尖刺沒(méi)那么高了。


          5f08d6fecc34995a6c5a24a3104ebe3f.webp


          并將這個(gè)優(yōu)化貢獻(xiàn)給了社區(qū),已經(jīng)被merge到master。


          05



          后續(xù)思考


          很多時(shí)候代碼在數(shù)據(jù)量小、并發(fā)低的時(shí)候不會(huì)有問(wèn)題,一旦數(shù)據(jù)量、并發(fā)上來(lái)了就會(huì)出現(xiàn)很多問(wèn)題,在寫(xiě)代碼的過(guò)程中敬畏每一行代碼,讓程序更加健壯。后續(xù)Clickhouse將持續(xù)在日志檢索場(chǎng)景發(fā)力,打造穩(wěn)定、低成本、高吞吐、高性能的PB級(jí)日志檢索系統(tǒng)。?


          瀏覽 57
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲婷婷精品国产 | 成人大香蕉在线 | 精品福利一区二区三区 | 日韩一级黄色免费电影网站 | 骚逼视频官网 |