網龍是一家遊戲公司,以前是做網路線上遊戲的,現在開始慢慢轉型,開始從事線上教育。 線上教育已經做了5-6年時間了。為什麼我們會用Cassandra呢?那我們就來介紹今天的議題。 首先介紹我們的業務背景, 第二部分深入介紹使用場景,然後介紹運維監控。 最後,我們實踐過程中有踩一些坑,這些坑我們拿來分享一下。
關於業務背景。網龍現在是一家線上教育公司。我們的很多app, 比如說“網教通” IM推送服務,還有一些針對智慧校園和智慧教室的物聯網服務, 這些服務每天會產生資料達十億條, 我們設計選型上曾經考慮過MongoDB,但是它無法處理這樣大的資料量,所以我們就需要上一套大規模支援這種業務場景的NoSQL叢集,因此選擇了 Cassandra資料庫。
Cassandra資料庫部署靈活。 我們有些To G 專案是針對海外的國家或地方政府,他們對資料安全性要求非常高,他們都要求我們去他們當地做私有化部署。私有化部署的出差成本比較高。用一些其他比較複雜的NoSQL資料庫,比如HBase的話,運維和部署會比較困難。Cassandra在這方面比較靈活,部署比較簡單, 可以節約很多部署成本。還有,我們的業務需要一些混部, 比如私有云和公有云的混部,或是跨資料中心的全球化部署, Cassandra在資料同步和跨DC 混合部署方面是比較靈活的。
還有就是它極致的寫效能。針對我們的產品IM、PUSH、IOT,這種帶有時間序列的資料,寫效能要求非常高,可能達到每秒幾十萬的寫效能。但我們又不需要太複雜的查詢, 就是做一些KV或者範圍的查詢。這些查詢Cassandra是可以輕鬆應付的。針對這個場景,Cassandra是非常適合我們的,所以我們選擇了它。
下面介紹我們的儲存場景。儲存有使用者和裝置的資料,有使用者資訊和裝置資訊。我們IM的收件箱,像微信等app, 每個使用者有自己的收件箱。我們會把收件箱寫在Cassandra裡面。還有通訊產生的訊息內容,這也是海量的,我們也把它儲存到Cassandra裡面。還有IoT裝置上報的監控資料,比如學生的平板電腦,學生的手錶,或是智慧教室的其他IoT (燈, 投影儀,會議系統,還有其他邊緣計算的裝置)。它們會實時產生一些監控資料,對這些資料我們要儲存一段時間, 這些我們也收集到Cassandra裡面來。
下面介紹我們的裝置表是如何設計的。裝置資訊表,裡面有app_id, shard_idx, dev_token。下面這一列是裝置的一些屬性,比如說token, 別名,還有標籤,訂閱的主題, 包名。 還有ack_id。 我們設計的主鍵是app_id, 和shard_idx作為副維主鍵。 新增shard_idx是為避免熱點和分割槽資料過大問題。通過shard分割槽,把它打散。比如某一個app_id資料量非常大,一個app_id可以分成64份,這樣可以解決這個問題。
一些查詢會根據裝置碼查詢裝置資訊,這樣就沒有建立二級索引,我們通過逆正規化新增裝置查詢表。如果使用Cassandra二級索引,用起來會有一些問題,比如寫失敗。在擴容時,二級索引會非常困難。所以我們沒有使用二級索引。
還有,我們使用了map來儲存比較靈活的KV資料,比如標籤資料。我們會給裝置打一個標籤,比如地區的標籤、版本的標籤、語言的標籤、或者使用者自定義的標籤。我們也可以把訂閱的主題以map的形式組織起來。這是我們的裝置資訊表。
我們的收件箱表分成兩份,一個是“廣播收件箱表”, 一個是“個人收件箱表”。 廣播收件箱是對整個app_id進行推送的時候, 這個app_id下的所有裝置或所有的使用者都能收到的資訊。這樣的話,只需要寫一條收件箱,不需要寫每個人的收件箱,避免了一條訊息被無限地放大。我們對收件箱按月份進行了分表,一年分成12個月,我們會有12張表儲存1年的資料。為什麼這麼做?主要是因為表太大的話,歸檔起來會比較困難。 如果沒有分表的話,可能好幾年好幾月的資料都放在一起了。(分表了的話,)早一些年的資料你想隨時調出來,或把它刪掉,或把它清理出去,就可以根據這些歸檔好的分表進行操作,只需要針對一張表操作就可以了,不會影響線上的業務。
我們也會新增shard_idx,避免熱點問題。對於分割槽大小和值的數量,Cassandra有一些限制:分別是100MB和20億的限制。(我們的)排序鍵DESC是逆序的形式,保證讀到最新資料,在range scan時可以減輕磁碟壓力,提升讀效能。時間序列資料大部分場景都是這樣設計的。
“個人收件箱”的設計和“廣播收件箱”是差不多的。每個人有一個收件箱。一對一的聊天時把訊息發給對方的話,只有一個人會收到。“個人收件箱”也和“廣播收件箱”一樣,這裡和廣播收件箱表一樣,app_id和shard_idx也是作為複合主鍵,而稍有不同的是,dev_token和msg_id作為副鍵。
“訊息表“有2種。一種是訊息原文表,一種是IoT監控表。訊息表我們也是通過年份進行歸檔,防止表太大導致運維困難。資料過期時可以直接TRUNCATE整張表。
對Msg_data,我們先壓縮完再寫入,因為資料太大的話對磁碟空間還是有影響的。 我們希望磁碟可以節約空間,不必經常擴容。
我們設定ttl, 可以讓資料自動過期。 這些是可以和業務繫結的ttl。 有些使用者推完訊息後,希望接收方一天內收到訊息是有效的,或者是1個月內收到訊息是有效的,所以這張表就有ttl了。ttl就有墓碑的問題,我們需要及時清理掉這些墓碑。 我們把gc_grace_seconds設定為1天。預設是10天。 我們儘早在壓縮時及時清理墓碑資料。
Flag 由QoS & retain flag 兩列合併成一列。Cassandra是列儲存的,寫入的時候列越多,實際的效能是越差的,所以我們把能合併的列儘量合併成一列。(這樣可以節約空間和提升寫入效能。)
我們的IoT監控表也是差不多的設計。為了更節約空間,直接把列縮寫成2個字母,這樣的好處是也能節約一部分的空間。但是這張表的可讀性就更差了,自己還需要一個資料字典去對照。
我們怎麼連線資料庫? 我們現在是用 Driver 3.x 版本。 Driver 3.x 版本我們已經用了好幾年了,不敢把它替換掉,它相對比較穩定。連線資料庫時, 我們一般會開啟執行緒池,最大節點會設定4個連線,一個連線最大請求配置為9120,預設是1024,(預設)可能會不夠用。
負載均衡預設 DCAwareRoundRobinPolicy ,看一致性級別配置shuffleReplicas。如果為false,就是同一個token的資料都請求到同一節點,這樣可以在一定程度上解決資料一致性問題。為什們這麼說呢? 比如說, 我刪除 UID=1, 時間是2的資料,刪除完後我又寫,寫完我又再讀, 讀完之後如果沒有配置的話,它可能會飄到其他節點。因為時間非常短, 前面寫入,到另一個節點去讀的時候, 會讀到不一樣的資料。 如果把資料請求到同一節點,讀到的資料就是最新的。
但這有個問題,如果機器出現問題,它會重試,當你出錯的時候,你還是會請求到這個節點, 所以無論你如何重試, 還是會失敗的。 所以有一個預設是 true, 適合超時重試的場景, 需要通過其他的方式解決資料一致性問題。比如使用QUORUM讀寫,也就是寫的時候先寫2個副本,讀的時候再讀2個副本, 來解決這樣的問題。
下面看一下Driver 4.x版本的高效能客戶端。 這個客戶端,我們希望叢集能支援設定代理服務。當Cassandra叢集部署到內網,想在公網去訪問它的時候,可以通過設定代理地址來實現。Driver 4.x是高效能客戶端,預設開啟執行緒池,它對此進行了一些優化,全部都是非同步請求的。
連線完資料庫,我們進行執行操作。使用預處理,提升效率,避免SQL注入。我們沒有使用非同步請求,因為我們覺得同步請求的效能已經足夠了。 如果使用非同步請求的話,非同步可以提升一部分效能,可能10-20%, 但是非同步在服務端壓力大的時候容易丟擲異常- NoHostAvailableException 主機不可用。這是我們實際遇到的問題,別人可能不一定遇到。
寫入和查詢有一個很重要的慢查詢診斷工具。像MySQL或者Postgres其他資料庫有查詢分析器,用來診斷查詢語句,有沒有走索引等問題。這些利器可以幫助診斷線上的問題。 比如你經常丟擲超長時間的請求, 有這個工具就可以檢視在哪個階段出現這樣的問題, Cassandra的driver 提供了這個查詢跟蹤功能。 怎麼用? 要開啟tracing就能使用,看示例程式碼 -- stmt.enableTracing( )。
當你開啟的時候,每個請求就會產生 session id 和 event id,這個id 會帶我們跟蹤的,會把每個請求的階段,資料列印出來,比如解析這條語句花了55微秒, prepare statement 花了93微秒, 從其他節點讀資料過來花了200多微秒,這一整串資料下來我們就知道我們這個請求傳送到Cassandra服務端它是怎麼處理的, 處理的每個步驟耗費了多少時間, 就一目瞭然了。 當我們線上出現慢查詢的時候, 就可以通過這種方式寫程式碼,探測一下發生了什麼事情, 為什麼會這麼慢。
寫入和查詢的時候要避免一些東西。 比如,避免使用批處理。 批處理Batch,就是一次提交多個修改操作,節省傳輸請求的資源消耗。同時也可以理解為一種事務的解決方案——all or nothing,這些操作可以保證要麼都成功,要麼都不成功,即原子性。但Cassandra 的批處理Batch這個功能不是用來提升效能使用的,特別是提交到不同分割槽,效能會有一定拖累。如果資料不是很重要,沒有必要原子性, 程式碼就可以這麼寫,但要關閉batchlog或者改成非同步提交。
怎麼關閉batchlog? 就用BatchStatement batch = new BatchStatement (Type.UNLOGGED), 這樣就不記錄batchlog, 它就通過非同步提交的形式提交上去。這時候它在效能方面就會好一點。但我還是建議,如果你的業務場景一定要求你是原子性的,你就用批處理;但如果你的業務場景不是原子性的,只是單純的為了提升效能,就沒有必要,會得不償失。
我們可以看到我們的日誌裡,經常會有一些警告,我們的業務, 比如說收件箱它提交的5520大小的批處理, 超過了它的預設配置,日誌裡就會看到一堆的警告,到一定程度上, 到叢集負載比較高的時候,它會拖累整個叢集, 造成整個叢集效能的下降。所以我們不建議這麼用。
還有就是查詢的時候,我們經常會很不負責任地去寫一個查詢,SELECT * FROM表,WHERE, 然後ORDER BY 它的時間序, LIMIT 1。這看似一個很簡單的查詢,我們只讀最小的一條資料或最大的一條資料。 一條很簡單的語句,實際看起來不會有什麼效能問題,我們的業務有很多人這樣使用。大家剛開始這麼用的時候很開心,寫程式碼很簡單,也沒有什麼效能問題。但是執行久了之後, 比如執行了1年,2年之後,就會發現這條語句怎麼變得越來越慢?
那是因為這條語句本身就是有問題的。 因為它沒有明確指定這條資料的範圍, 沒有指定這條資料在哪個 sstable 裡。 這樣寫的話, 像這條語句ORDER BY inserttime, 只要每個分割槽裡有這個userid =2, 每個sstable 都有可能被讀到。這時你就會被無限地放大。 比如說這張 inbox表,本來在叢集裡每個節點上可能有100個sstable,你可能每個sstable都要去讀,你就會放大100倍。這樣讀出來就很慢。
因為讀的時候, 它首先會被過濾一下。 過濾失敗,它會在 db.index 裡去找, 在 db.index 裡首先尋找分割槽鍵, 分割槽鍵裡只要查詢到userid =2的資料, 它就會到資料庫sstable 裡把最小和最大的查出來, 可能會把每個sstable都查出來,查100條資料,然後再進行對比, 極易導致GC。
我們在這邊的截圖可以看到,“Select * FROM exam.inbox WHERE userid=2 ORDER BY insertime ASC LIMIT 1”。我們測試的這個資料庫有3個sstable, 但是這3個sstable都被命中了, “skipped 0”, 就是說,它查了3個sstable。
(另一個截圖裡,) 這邊是,ORDER BY insertime DSC。它也是3個都命中了。 所以說,在這種情況下就把查的結果放大了好幾倍。 這樣極易導致GC。這裡我們給Cassandra提一個建議,能否優化查詢, SliceQueryFilter可以根據上次sstable查詢結果的key值拿到下一個sstable,把下一個過濾一下,這樣避免查詢下一個 sstable 。
我們建議在寫入和查詢的時候,使用ORDER BY 的時候儘量指定 key range。像前面那條語句, “SELECT * FROM exam.inbox WHERE userid =2 AND insertime >3 ORDER BY insertime DESC LIMIT 1”, 這裡我們就指定了”insertime >3”的範圍。 這裡我們可以看到結果,它可以skip 2個sstable, 效能有所提升。 因為它只查詢了1個sstable, 只到一個sstable上去讀資料,沒有讀3個sstable。
最精準的查詢,我們可以指定一個完整的key,就是KV查詢。這時候 Bloom Filter就可以很好地起作用了,會把每一個sstable先作一次過濾。這條資料有沒有? 沒有的話就不會繼續查詢了。
我們可以看到Cassandra資料庫的SSTable的組成檔案,這裡有Filter.db和Index.db。 查詢資料先是Bloom Filter過濾一下,過濾完之後如果存在,它會通過Index.db去查詢一下這個資料的邊緣範圍。所以查詢的時候, 我們要儘量使Filter.db和Index.db幫我們作一個前置判斷,幫我們做一些資料的過濾,使我們的查詢更精準。越精準的話,我們查詢的效能就越好,就不會發生一些線上的問題。
前面講完了建表和查詢的一些使用,現在講一些運維的事情。運維可以分成幾項。 首先考慮安全性。 安全永遠是所有工作裡第0項工作, 最重要的一項工作。 首先許可權最小化。 只能有一個superuser, 一個使用者對應一個庫名,不能跨庫訪問。這樣的話,防君子不防小人。我們根據使用者的業務控制最小許可權,生產環境禁止DROP許可權。因為一旦執行了DROP,可能後面恢復,七七八八就會遇到一些很大的問題。這是一般其他所有的資料庫都有的限制。
JMX 埠內網只開放於本機,有條件開啟證書驗證。JMX 除了可以讀取一些監控指標,還可以對你的叢集做一些操作,所以這個埠開放還是有一定危險的。
還有就是Opscenter。這是DataStax提供基於Web的監控運維操作的工具。我們曾經用過,但發生了一些不愉快的事情。Opscenter 許可權非常大,可以刪表刪庫清快照,若被入侵,後果不堪設想。如果非得使用Opscenter,必須禁止把它開放到公網,要定時監控Nginx日誌,看看是否有入侵痕跡。
為什麼要慎用?因為我們的一個線上叢集曾經Opscenter被入侵過,後果就是我們這個叢集上有一個庫,所有的資料都被刪掉了, 我們配置的使用者名稱密碼也被洩露了。只有運維人員才知道, 但我們不知道對方是如何黑進來的。 我們根據查詢Nginx日誌發現,從美國過來的IP對我們的密碼進行了不斷的探測, 然後做了一些埠穿透。所以用Opscenter還是挺危險的, 因為我們不知道它隱藏了什麼樣的漏洞。 所以建議不要用。
關於監控。沒有Opscenter,我們就要靠自己的一些系統。 我們現在用的是小米開源的open-falcon,通過jmx採集叢集的基本指標,通過IM/郵件/簡訊/電話通知責任人。我們採集的一些(伺服器)指標是磁碟空間,網路,和CPU 。根據我們的經驗,磁碟是60%預警,網路超過20MB預警,CPU超過80%預警。
磁碟為什麼是60%預警? 因為sstable很大的時候, 它的壓縮需要很大的空間。如果磁碟80%才報警,一個超大的sstable可能就沒有辦法進行壓縮,它就會一直存在裡面。所以磁碟60%要預警,然後及時擴容。
服務的可用性監控我們也要做,監控一下它的程式,監控這些預設埠9042,7000的服務可用性。
還有效能的監控。 我們要實時地監控每秒的讀/寫請求數,讀/寫延遲,比如P99,以便實時的知道線上叢集的質量和訪問情況。
表監控。比如表的墓碑情況,表的 sstable有多少,最大的sstable有多少,表的設計會不會有問題,執行久了之後會不會有熱點和超大分割槽等問題。 右圖是我們採用的Cassandra的一些監控指標。我們可以看到DownEndpointCount, UpEndpointCount, 這個指標可以知道我們叢集裡面掛了幾臺。
(另外,)這裡(還)有Heap的一些資訊。 這是監控具體的一張表,invalid access token。這是sstable有多少,空間用了多少,讀請求使用了多少,讀延遲,寫延遲等等。所以說,這些監控指標我們都要把它們採集下來。
日常運維。我們要定期repair業務核心的表,其它一般日誌類的表可以不repair。因為repair非常耗資源,而且時間可能會非常長,但是它又不得不做。所以我們就把業務比較重要的表做repair。Repair的時候我們要指定範圍,使用Range repair 指定token範圍。當它repair到你的範圍失敗的時候,你可以從那裡重新開始,不用做一些重複的操作。而且 Range Repair的壓力沒那麼大,可以適當控制,今天修幾個,明天修幾個,這樣的話不用一次性指定幾個節點修的話,極有可能導致線上的一些效能的問題。
備份。我們的sstable要進行備份。我們在刪表的時候,要開啟自動建立快照,同時定期備份主要的表,表的結構,還有節點token。我們是通過S3cmd備份到ceph叢集。如果在其他的環境上,比如說在AWS或Azure的環境,我們就備份到AWS S3或雲端網盤或其它伺服器上。
擴容。在磁碟空間滿60%預警時要及時擴容。擴容的時間一般會比較長, 而且經常擴容失敗,這是線上上之前經常遇到的一個問題。我們大致分析了一下擴容失敗的原因,就是墓碑比較多。墓碑比較多的時候, 我們擴容寫資料的時候會把墓碑先加到memtable裡去,然後再寫入磁碟,這時memtable會非常大,很容易導致OOM。這個時候,根據我們的經驗,如果我們的記憶體夠大,就把memtable放到offheap_buffers去,就可以充分利用大記憶體的優勢, 把這些東西加進去,防止記憶體溢位。
資料遷移。Cassandra提供了Cassandra bulk loader工具來將資料從MySQL遷移到Cassandra。如果你沒用Cassandra的話,想用它,我可以推薦你使用一些工具來做遷移,把你的資料從異構資料庫遷移過來。
最後我們來分享我們用Cassandra的時候遇到的坑。這些坑有大有小。有些是低階的錯誤。比如說,第一個,System_auth系統副本數沒修改。系統使用者表預設副本數為1,當掛一個節點時就會導致部分賬號無法驗證通過。這個我相信我們都遇到過。所以叢集部署完了之後,我們第一件事要做的就是把這個副本改成3。
第二點就是插入一些空值。空值Cassandra會當成墓碑處理。在repair的時候也會讀入大量墓碑。如果空值非常多非常大的話,可能會引發一些記憶體溢位的問題。所以寫入一行的時候如果某個列為空值,必須給他指定一個預設的值,比如寫入這樣一個空字串符號(‘ ’)進去,這樣Cassandra不會當作空值處理,業務上可以讓客戶端去處理。
最令人頭疼的就是墓碑問題。墓碑太多會造成各種問題,如慢查詢、repair的時候OOM。所以要把墓碑控制在一個可控制的範圍。墓碑會在壓縮的時候被從磁碟上擦除,所以我們在建表的時候要把GC_GRACE_SECONDS 的時間設短一點。預設是10天。如果你經常 repair或者保證你的叢集不會出什麼問題的話,可以把時間再縮短一點。這樣在壓縮的時候它就會及時清理掉。還有一種策略就是配置時間窗壓縮策略,把舊的墓碑及時清理。但是這個策略也有一定問題,也有一定侷限性。
還有二級索引。我的建議是能不用就儘量不用,特別是有墓碑的表和資料量大的表。我們遇到的問題是,我們使用者中心有一張 token表,記錄每個使用者的 token。 這個token我們建了一個二級索引。那張表資料量非常大,token的特性是7天會過期,有的是一個月過期。所以這張表的資料量又大,墓碑又多,擴容的時候這張表會不斷地丟擲,墓碑非常多,擴容失敗。
還有就是我們有一些撥測賬號,一分鐘去請求一個token, 一分鐘之內又刪除這個token, 久而久之,幾個月之後這個撥測賬號就會生成幾十萬,上百萬的token。 這個賬號再撥的時候,一撥服務就掛了,一撥服務就掛了。所以二級索引,還有有墓碑的二級索引儘量不要使用。
高併發的時候慎用計數器couter功能。一個是它不是很精準,因為你可能會超時。超時的時候會重發,重發的時候它會額外多計一次。比如說這條資料是增加1的,因為超時會變成增加2。還有就是,計數器是ACID, 它的一致性很高,在高併發的情況下效能也不好。