MongoDB 臨時表橫空出現1萬+,這條語句執行前請準備好翻車的姿勢

2020-08-30 架構薈萃

解決問題之前,先在腦海中演繹一下當時場景

某日早上八點半,筆者接到客戶反饋,門戶首頁待辦訪問異常緩慢,經常出現「訪問異常,點擊重試」。當時直覺告訴我,應該是大量用戶高並發訪問 mongodb 庫,導致 MongoDB 庫連接池出問題了,因為上線發版時,功能是正常的。

由於是上周五晚上發版驗證後,周六日使用門戶的用戶比較少,一直沒發現問題,直到下周一才集中爆發門戶訪問不可用。

請開始我的表演

一開始運維組認為是加了 MongoDB 審計日誌造成的,因為有大量針對 MongoDB 做寫審計日誌寫操作,確實會降低伺服器性能。 通過查看服務日誌,也發現非常多的 MongoDB 訪問 timeout 異常信息。

com.mongodb.MongoTimeoutException: Timed out after 30000 ms while waiting for a server that matches ReadPreferenceServerSelector{readPreference=primary}. Client view of cluster state is {type=UNKNOWN, servers=[{address=10.236.2.183:27017, type=UNKNOWN, state=CONNECTING, exception={com.mongodb.MongoSocketOpenException: Exception opening socket}, caused by {java.net.ConnectException: Connection refused (Connection refused)}}, {address=10.236.2.184:27017, type=UNKNOWN, state=CONNECTING, exception={com.mongodb.MongoSocketOpenException: Exception opening socket}, caused by {java.net.ConnectException: Connection refused (Connection refused)}}, {address=10.236.2.185:27017, type=UNKNOWN, state=CONNECTING, exception={com.mongodb.MongoSocketOpenException: Exception opening socket}, caused by {java.net.ConnectException: Connection refused (Connection refused)}}] at com.mongodb.connection.BaseCluster.createTimeoutException(BaseCluster.java:377) ~[mongodb-driver-core-3.4.2.jar!/:na] at com.mongodb.connection.BaseCluster.selectServer(BaseCluster.java:104) ~[mongodb-driver-core-3.4.2.jar!/:na] at com.mongodb.binding.ClusterBinding$ClusterBindingConnectionSource.<init>(ClusterBinding.java:75) ~[mongodb-driver-core-3.4.2.jar!/:na] at com.mongodb.binding.ClusterBinding$ClusterBindingConnectionSource.<init>(ClusterBinding.java:71) ~[mongodb-driver-core-3.4.2.jar!/:na] at com.mongodb.binding.ClusterBinding.getReadConnectionSource(ClusterBinding.java:63) ~[mongodb-driver-core-3.4.2.jar!/:na] at com.mongodb.operation.OperationHelper.withConnection(OperationHelper.java:402) ~[mongodb-driver-core-3.4.2.jar!/:na] at com.mongodb.operation.MapReduceWithInlineResultsOperation.execute(MapReduceWithInlineResultsOperation.java:381) ~[mongodb-driver-core-3.4.2.jar!/:na] at com.mongodb.operation.MapReduceWithInlineResultsOperation.execute(MapReduceWithInlineResultsOperation.java:70) ~[mongodb-driver-core-3.4.2.jar!/:na] at com.mongodb.Mongo.execute(Mongo.java:836) ~[mongodb-driver-3.4.2.jar!/:na] at com.mongodb.Mongo$2.execute(Mongo.java:823) ~[mongodb-driver-3.4.2.jar!/:na] at com.mongodb.DBCollection.mapReduce(DBCollection.java:1278) ~[mongodb-driver-3.4.2.jar!/:na] at org.springframework.data.mongodb.core.MongoTemplate.mapReduce(MongoTemplate.java:1406) ~[spring-data-mongodb-1.10.1.RELEASE.jar!/:na] at org.springframework.data.mongodb.core.MongoTemplate.mapReduce(MongoTemplate.java:1383) ~[spring-data-mongodb-1.10.1.RELEASE.jar!/:na] at com.unicom.portal.taskquery.common.TaskManager.selectPendingCountByType(TaskManager.java:1002) ~[classes!/:1.0.0] at com.unicom.portal.taskquery.service.impl.TaskServiceImpl.getPendingInfo(TaskServiceImpl.java:233) ~[classes!/:1.0.0] at com.unicom.portal.taskquery.controller.TaskPendController.getPendingInfo(TaskPendController.java:164) ~[classes!/:1.0.0]

同時運維人員通過監控告警發現 MongoDB 資料庫的連接數達到 10499(平時監控為幾百),而 MongoDB 資料庫憑空多出驚人的一萬多張臨時表記錄。

查看 K8s 容器平臺伺服器資源情況,發現待辦服務 CPU 資源使用高達 7G 多,內存使用高達 12G。平常待辦服務的CPU 資源使用都是 0.00 幾,明顯感覺不正常。

這似乎更加驗證了是加了審計日誌造成的,於是運維組開始了非常耗時的 Mongos 停止並重啟操作,但很遺憾的是「 Mongos 重啟後不久又自動停止了」( 後來跟運維組溝通,加的審計日誌跟 MongoDB 半毛錢關係都沒有)。 由於這個誤導,導致門戶大概四十分鐘不可用。筆者沒辦法,只能仔細分析 docker 容器日誌,發現大部分錯誤由同一個方法造成的。

com.unicom.portal.taskquery.common.TaskManager.selectPendingCountByType(TaskManager.java:1002)

通過代碼走讀發現 TaskManager.selectPendingCountByType 這個方法用到了 MongoDB 的 mapReduce 方法。

org.springframework.data.mongodb.core.MongoTemplate.mapReduce

通過查閱 MongoDB 官方文檔知悉, mapReduce 方法類似於 MySQL 中的 group by 語句,非常適合做表欄位聚合(分組)分類統計功能。 了解 Hadoop 的同學知道,Hadoop 中的 Map 和 Reduce 會拆成多個子任務進行後臺跑批計算的。而 MongoDB 的 mapReduce 方法同樣如此,不同的是 mapReduce 方法會把子任務發送到不同的分片(sharding)伺服器上去執行,而這個過程是非常耗時的。 平常幾十個人使用這個功能不會覺察到訪問有問題,但是門戶每天近 12W 的用戶同時在八點半之後訪問這個功能,後果就不堪設想了。 結果是「 修改後的待辦待閱查詢服務在讀取/存儲過程中會創建大量臨時表,高並發時會造成待辦 MongoDB 資料庫頻繁執行和刪表操作,致使伺服器資源異常佔滿,MongoDB 資料庫進程異常關閉。

筆者初步定位是這個 TaskManager.selectPendingCountByType 統計方法出問題後,果斷要求運維組把後臺服務恢復到上一個版本後,門戶訪問正常,待辦數據能夠正常顯示,問題解決!

查看待辦服務容器資源使用情況,CPU 使用率明顯下降近 7 倍。

心中預案,處理泰然

08:20 運維人員通過監控告警發現 MongoDB 資料庫的連接數達到 10499(平時監控為幾百),開始檢查處理。

08:31 運維人員檢查發現 Mongos 進程停止,嘗試重新啟動,發現重啟後不久又自動停止了。

08:37 運維人員分析可能因5月9日晚後臺開啟了門戶 MongoDB 審計日誌導致資料庫開銷較大,故開始回退 6 臺 mongoDB 上的審計日誌功能。

08:50 回退審計日誌操作完成,再次啟動 Mongos 進程發現不久又自動停止。

09:06 嘗試先停止待辦查詢應用服務,阻斷應用 Mongos的調用,再啟動 Mongos 進程。

09:20 西鹹機房維護人員配合檢查 MongoDB 的伺服器資源使用情況後反饋無問題。同時資料庫運維人員複查關閉 MongoDB 審計日誌回退操作是確認已經回退成功。

09:31 項目組分析5月9日晚上發版的「待辦待閱數量查詢接口優化」功能可以與此故障有關,因此開始嘗試回退待辦查詢應用代碼。

09:36 待辦查詢應用代碼服務回退成功,同時測試發現門戶待辦業務恢復正常。

09:40 觀測前臺業務和後臺服務穩定後,上報故障恢復。

16:00 聯繫 17 個全國應用系統完成 9 位一級 VIP 和信息化 3 位領導的待辦待閱差異比對,共處理 3 條待辦差異。

知其然也要知其所以然

Mongodb 官網對 MapReduce 函數介紹:

Map/reduce in MongoDB is useful for batch processing of data and aggregation operations. It is similar in spirit to using something like Hadoop with all input coming from a collection and output going to a collection. Often, in a situation where you would have used GROUP BY in SQL, map/reduce is the right tool in MongoDB.

大致意思:

Mongodb中的Map/reduce主要是用來對數據進行批量處理和聚合操作,有點類似於使用Hadoop對集合數據進行處理,所有輸入數據都是從集合中獲取,而MapReduce後輸出的數據也都會寫入到集合中。通常類似於我們在SQL中使用 Group By語句一樣。

MongoDB 有兩種數據計算 聚合操作,一種是 Pipeline,另一種是 MapReduce。 Pipeline 的優勢在於查詢速度比 MapReduce 要快,但是 MapReduce 的強大之處在於它能夠在多臺分片(Sharding)節點上並行執行複雜的聚合查詢邏輯。

那 MapReduce 有哪些優缺點呢?

  • MapReduce 優點在於能夠充分利用 CPU 多核資源(多線程),並發執行 Sharding 分片任務,做分組統計。另外對於一些聚合函數,如 SUM、AVG、MIN、MAX,需要通過 mapper 和 reducer 函數來定製化實現。
  • MapReduce 缺點在於非常耗 CPU 資源並且非常吃內存,其邏輯是首先執行分片查詢任務計算線程,計算結果先放內存(吃內存),然後把計算結果存放到 MongoDB 臨時表,最後由 finalize 方法統計結果並刪除臨時表記錄。

MapReduce 執行流程

MapReduce 工作分為兩步,一是映射,即 map,將數據按照某一個規則映射到一個數組裡,比如按照 type 或者 name映射,同一個 type 或者 name 的數據形成一個數組,二是規約,即 reduce,它接收映射規則和數組,然後計算。舉例如下:

使用 MapReduce 要實現兩個函數:Map 和 Reduce。 Map 函數調用 emit(key,value) 遍歷集合中所有的記錄,將 key與 value 傳給 Reduce 函數進行處理。Map 函數和 Reduce 函數是使用 JavaSript 編寫的,其內部也是基於 JavaSript V8 引擎解析並執行,並可以通過 db.runCommand 或 mapreduce 命令來執行 MapReduce 操作。

並發性

我們都知道,Mongodb 中所有的讀寫操作都會加鎖(意向鎖),MapReduce 也不例外。MapReduce 涉及到 mapper、reducer,中間過程還會將數據寫入臨時的 collection 中,最終將 finalize 數據寫入 output collection。

read 階段將會使用讀鎖(讀取 chunks 中的數據),每處理 100 條 documents 後重新獲取鎖(yields)。寫入臨時 collectin 使用寫鎖,這個不會涉及到鎖的競爭,因為臨時 collection 只對自己可見。

在創建 output collection 時會對 DB 加寫鎖,如果 output collection 已經存在,且 action 為 replace 時,則會獲取一個 global 級別的寫鎖,此時將會阻塞 mongod 上的所有操作(影響很大),主要是為了讓數據結果為 atomic ;如果 action 為 merge 或者 reduce,且 nonAtomic為 true 是,只會在每次寫入數據時才會獲取寫鎖,這對性能幾乎沒有影響。

來個復盤吧

總的來說,還是對 Mongodb 的 MapReduce 方法了解不夠深入;同時代碼評審時沒有重視代碼評審的質量,伺服器監控方面也待加強。另外對於高並發的地方沒有做必要的接口壓力測試。 所以,接下來需要加強項目組危機意識,不管是管理流程,代碼質量,還是伺服器資源監控以及必要的性能測試等。上線發版前,做好事前控制,事中做好服務監控,事後做好復盤,避免下次犯同樣的錯誤。

參考

https://www.csdn.net/article/2013-01-07/2813477-confused-about-mapreduce https://blog.csdn.net/iteye_19607/article/details/82644559

相關焦點

  • 52條SQL語句性能優化策略,建議收藏|sql|mysql|oracle|索引|臨時表...
    16、使用表的別名(Alias):當在 SQL 語句中連接多個表時,請使用表的別名並把別名前綴於每個 Column 上。這樣一來,就可以減少解析的時間並減少那些由 Column 歧義引起的語法錯誤。  17、使用「臨時表」暫存中間結果 :  簡化 SQL 語句的重要方法就是採用臨時表暫存中間結果。
  • 帶臨時表的SQL查詢語句的優化方法
    現在回顧,那次處理該問題,前後嘗試了好幾種方法:  1 重新採集該SQL語句中涉及到的表的統計信息,但不包括臨時表(由於是在另外的進程裡作的採集操作,而此時的臨時表是沒有數據的,採集了也沒意義),採集完後重新運行報表,發現執行計劃不變,說明方法無效。
  • Node js 連接 MySQL 與 MongoDB
    MySQL1、設計表首先通過可視化工具進行表的設計,然後添加幾條測試數據:2、安裝 Node.js 連接 MySQL 的包npm i mysql -d複製代碼3、連接 MySQLMySQL.js// 引入 mysql 包const mysql = require('mysql
  • 十分鐘了解Mongodb資料庫
    1.MongoDB的簡介。2.mongodb的安裝。3.mongodb查詢指令,大家可以根據選擇進行閱讀。​ MongoDB 的官方網站地址是:http://www.mongodb.org/1.3 MongoDB特點​ MongoDB 最大的特點是他支持的查詢語言非常強大,其語法有點類似於面向對象的查詢語言,幾乎可以實現類似關係資料庫單表查詢的絕大部分功能,而且還支持對數據建
  • 52 條 SQL 語句性能優化策略,建議收藏
    本文會提到52條SQL語句性能優化策略。1、對查詢進行優化,應儘量避免全表掃描,首先應考慮在where及order by涉及的列上建立索引。2、應儘量避免在where子句中對欄位進行null值判斷,創建表時NULL是默認值,但大多數時候應該使用NOT NULL,或者使用一個特殊的值,如0,-1作為默認值。3、應儘量避免在where子句中使用!
  • MySQL的Online DDL語句
    而如果我擔心它選擇了鎖而導致我們的表不能讀也不能寫,顯然這不是我們想要的結果,我們希望:如果選擇了鎖就不要執行,直接退出執行;如果沒有選擇鎖就執行。想要達到我們希望的這個效果,該怎麼做呢?可以在執行我們的online DDL語句的時候,使用ALGORITHM和LOCK關鍵字,這兩個關鍵字在我們的DDL語句的最後面,用逗號隔開即可。
  • MySQL 臨時表空間數據過多的問題排查-愛可生
    這條 SQL 進行了三表關聯,每個表都有幾十萬行數據,三表關聯並沒有在 where 條件中設置關聯欄位,形成了笛卡爾積,所以會產生大量臨時數據;而且都是全表掃描,加載的臨時數據過多;還涉及到排序產生了臨時數據;這幾方面導致 ibtmp1 空間快速爆滿。
  • MySQL 臨時表空間數據過多導致磁碟空間不足的問題排查
    這條 SQL 進行了三表關聯,每個表都有幾十萬行數據,三表關聯並沒有在 where 條件中設置關聯欄位,形成了笛卡爾積,所以會產生大量臨時數據;而且都是全表掃描,加載的臨時數據過多;還涉及到排序產生了臨時數據;這幾方面導致 ibtmp1 空間快速爆滿。
  • Mysql中一條SQL查詢語句是如何執行的?
    1.前言在深入學習前我們應該跳出來,從整體的架構上來了解一下MySQL,這樣更有利於我們學習。2.查詢流程解析select * from table1 where ID=10;這條語句相信大家再熟悉不過了,下面我們就看看這一條語句在mysql中是怎麼執行的。第一步:一條sql語句要經過連接器,客戶端要和mysql建立連接。
  • mybatis 逆向工程使用姿勢不對,把表清空了,心裡慌的一比
    使用mybatis逆向工程的時候,delete方法的使用姿勢不對,導致表被清空了,在生產上一刷新後發現表裡沒數據了,一股涼意從腳板心直衝天靈蓋。於是開發了一個攔截器,並寫下這篇文章記錄並分享。用起來真的很方便,我用了好幾年了,但是前段時間翻車了。具體是怎麼回事呢,我給大家擺一下。先說一下需求吧。就是在做一次借據數據遷移的過程中,要先通過 A 服務的接口拿到所有的借據和對應的還款計劃數據,然後再對這些借據進行核查,如果不滿足某些添加,就需要從表中刪除借據和對應的還款計劃。借據和對應的還款計劃存放在兩張表中,用借據號來關聯。
  • MongoDB注入
    二、注入簡單的語句執行,注入這裡我們有一個mongodb的資料庫,資料庫為test點擊此處添加圖片說明文字執行了語句username[xx]=test&password=test則$username就是一個數組而mongodb對於多維數組的解析使最終執行了如下語句:
  • 源碼時代java課堂:Mongodb進階之Mongodb使用
    2、如果show dbs沒有出現新創建的資料庫,則往裡面存入一條數據。④multi : 可選,mongodb 默認是false,只更新找到的第一條記錄,如果這個參數為true,就把按條件查出來多條記錄全部更新。
  • MySQL的一條SQL語句是怎麼執行的?
    我們先看一條SQL語句:select * from user where id = 1.通常情況下我們看到的只是輸入一條SQL語句,然後執行返回一個結果,卻不知道這條語句在MySQL內部是怎樣的一個執行過程。
  • MySQL臨時表深入理解
    MySQL臨時表類型:外部臨時表,通過create temporary table語法創建的臨時表,可以指定存儲引擎為memory,innodb, myisam等等,這類表在會話結束後,會被自動清理。如果臨時表與非臨時表同時存在,那麼非臨時表不可見。
  • 資深DBA整理MySQL基礎知識四:大神們都忽略的SQL語句執行的順序
    前幾篇說了一些SQL語句的基礎知識,SQL語句也是一種程式語言,語言執行是有順序的。在學習SQL語言的時候一定要知道他執行的順序,這樣才能能好的理解SQL,學好SQL。下面進入正題,先放兩張圖。如果是 join 那麼這一步就將添加外部行,left jion 就把左表在第一步中on過濾的添加進來,如果是right join 那麼就將右表在第y一步中的on過濾掉的行添加進來,這樣生成虛擬表 vt3 ,如果 from 子句中的表數目多餘兩個表,那麼就將vt3和第三個表連接從而計算笛卡爾乘積,生成虛擬表,該過程就是一個重複1-2的步驟,最終得到一個新的虛擬表 vt3
  • 源碼時代java課堂:Mongodb進階之Mongodb使用
    1. MongoDB數據的使用2.1 mongodb中的概念Mongodb因為是最像關係型資料庫的非關係型資料庫,所以我們可以對照著mysql來認識Mongodb中的一些概念。2.2 mongodb常用語法1、顯示庫列表:show dbs2、使用庫:use dbname注意:1、該命令可隱式創建資料庫,即如果資料庫不存在則創建資料庫,否則切換到指定的資料庫。2、如果show dbs沒有出現新創建的資料庫,則往裡面存入一條數據。
  • 多場景下MySQL臨時表有什麼用?
    >1.外部臨時表,通過create temporary table語法創建的臨時表,可以指定存儲引擎為memory,innodb, myisam等等,這類表在會話結束後,會被自動清理。如果臨時表與非臨時表同時存在,那麼非臨時表不可見。show tables命令不顯示臨時表信息。可通過informationschema.INNODBTEMPTABLEINFO系統表可以查看外部臨時表的相關信息,這部分使用的還是比較少。
  • 詳解一條查詢select語句和更新update語句的執行流程
    這是因為MySQL的緩存使用條件非常苛刻,是通過一個大小寫敏感的哈希值去匹配的,這樣就是說一條查詢語句哪怕只是有一個空格不一致,都會導致無法使用緩存。而且一旦表裡面有一行數據變動了,那麼關於這種表的所有緩存都會失效。所以一般我們都是不建議使用緩存,MySQL最新的8.0版本已經將緩存模塊去掉了。
  • MySql 入門到精通-sql查詢語句的執行過程,你真的知道嗎?
    如下sql 查詢:mysql> select * from T where ID=10;對於這條查詢語句,我們再腦海裡面肯定能知道它能返回 T 表內 ID=10 的數據,但是,我們並不知道它在 MySQL 內部是怎麼執行的。
  • 優化SQL查詢:如何寫出高性能SQL語句
    執行計劃是資料庫根據SQL語句和相關表的統計信息作出的一個查詢方案,這個方案是由查詢優化器自動分析產生的,比如一條SQL語句如果用來從一個 10萬條記錄的表中查1條記錄,那查詢優化器會選擇「索引查找」方式,如果該表進行了歸檔,當前只剩下5000條記錄了,那查詢優化器就會改變方案,採用 「全表掃描」方式。 可見,執行計劃並不是固定的,它是「個性化的」。