記錄服務上線一年來的點點滴滴

2015年12月,也就是在一年前,開發了半年的雲存儲服務上線。這對於付出了半年努力的咱們來講,是一件鼓舞人心的事件。由於這個服務在咱們手上經歷了從0到1的過程。這是咱們本身的一小步,倒是整個雲存儲服務的一大步。mysql

咱們開發的是一款視頻監控類的軟件,分爲視頻採集端跟觀看端。採集端能夠是專業攝像頭,手機,無人機等各種智能設備,觀看端通常是手機或者電腦。最基礎的功能,就是視頻觀看,採集端實時採集圖像,編碼,傳輸,觀看端進行點播服務。同時採集端能夠監測視頻畫面的運動幅度,而後觸發報警,而且會錄製報警視頻。咱們的雲存儲服務就是將錄製的報警視頻上傳到雲端,而且在觀看端提供查看功能 nginx

2.0 石器時代

第一個版本叫2.0,至於爲何叫2.0,或許這只是一個代號而已。web

整個系統的框架以下:redis

 

整個系統由客戶端, web服務器, 數據庫, 文件存儲服務器構成。文件服務器使用的是亞馬遜的S3,對於小公司來講,選擇亞馬遜比自建存儲的成本要低得多。sql

咱們要求系統要儘量及時的上傳報警視頻。一個報警視頻大概錄製30s,及時意味着報警一旦觸發就要開始上傳,而不是等報警視頻錄製結束了再上傳錄製下來的報警文件。並且在有些設備上,如攝像頭,是能夠沒有存儲卡的,可是也得能上傳,因此選擇上傳報警視頻文件的方式就不可取了。而在s3服務使用的是http協議上傳文件,必須在上傳文件以前告訴服務器文件的大小,即http頭裏面的content-length信息。爲了解決這個問題,咱們使用了分片上傳的方式。就是首先根據視頻的分辨率大小,計算出一個文件size,這個大約能存儲10s左右的視頻。在上傳過程當中,計算已經上傳的數據量大小,當一個分片存儲滿以後,再開始另外一個分片。在最後一個分片時,可能報警視頻已經錄製結束了,可是分片還沒存滿,這時候就用空數據填充。固然空數據的位置也得記錄下來,這樣觀看端在播放時,就不至於把空數據看成正常數據,致使播放失敗。除了正常的視頻數據,在每段報警視頻的最後還得記錄視頻中的I幀位置信息,主要是用於在播放時拖動,尋找位置信息。這一點是參考mp4文件的錄製方式,因爲咱們使用的並非標準的mp4格式,因此在上傳視頻的過程當中,得將I幀的位置信息記錄下來,待整個視頻上傳結束後,將位置信息存儲在視頻的尾部,最後不足一個分片的部分,再用空數據填上。數據庫

整個採集端來講,上傳文件到亞馬遜S3的過程就是如此,那麼跟web服務器又是怎麼交互的呢?緩存

第一步,採集端在觸發了一個報警時,要向web服務器申請一個EVENTID,做爲這個報警事件的惟一標識,在以後上傳文件都跟這個EVENTID綁定。觀看端在播放時,根據這個EVENTID查到它對應的視頻文件,而後去亞馬遜S3上下載播放。安全

第二步,當採集端向亞馬遜上傳一個分片文件時,須要生成一個uri,而後才能向這個uri PUT數據。uri的生成,採集端能夠直接向亞馬遜申請,可是考慮到申請uri須要攜帶亞馬遜的帳戶祕鑰,放在客戶端作不安全,因此申請uri仍是放在web服務器上。當採集端須要上傳文件,向web服務器去申請。每次採集端申請uri時,帶上EVENTID,以及一個分片index,即告訴web服務器你要申請的是哪一個eventid的第一個分片。生成的uri格式以下服務器

http://xxxxxxxxxxxxxxxxxxxxxxxx/eventid/index.avi。前面的xxxx表示你在 s3上面建立的存儲桶,index便是第幾個文件, avi是文件的後綴名(這裏是一個假設,叫什麼均可以)。每開始一個新的分片,index自動加1,這樣在只須要記錄一個最終的index便可。下載時,根據最終的index大小,就能夠把全部的文件都下載下來。當申請到uri以後,採集端就能夠經過http協議向這個uri上傳數據了。網絡

第三步,在每一個uri上傳結束以後,向web服務器report一次 event信息。這個event信息,便是第一步開始時申請的eventid。彙報的信息,包括這個event 的觸發時間,類型,視頻時長,視頻分辨率,音頻的採樣率,以及index。能夠看到,每一個uri上傳結束都彙報一次的信息,其實也只有index的值不一樣,其餘的值都同樣。原本是能夠等到在一個視頻徹底上傳結束以後,一次性彙報一次event信息就OK了。可是考慮到,當一個視頻正在上傳的過程當中,採集端軟件crash了,或者小偷進來后里面將監控設備砸了,因此要每上傳一個分片都要彙報一次。這樣,觀看端查看時,就能夠看到一個未完成的視頻了。除了這點外,也要注意到可能一個分片都沒上傳上去,就發生意外,因此咱們在每次報警一觸發,就當即抓一幅圖片,上傳到S3上。

上面基本就是整個系統上傳部分的流程。web服務器負責生成eventid, 申請uri,以及寫數據庫。數據庫只要存儲一張event表項就能夠了,表項裏面記錄了這個event 的詳細信息。

在2.0版本中,雖然使用了redis緩存,用來下降mysql的訪問壓力,可是緩存的使用很簡單,僅僅存儲了一個採集端天天的event個數。這樣觀看端查詢時,能夠一次性獲取到最近30天,天天的event個數。由於咱們只給用戶保留最近30天的數據,在redis上作了個數統計,就不用再去數據庫讀表統計了。

接下來再說說觀看端的查詢流程

首先,就是去查詢採集端最近一個月天天的event個數。

而後,再具體查看某一天的報警時,帶上日期,起 始時間段,去服務器查詢event列表。在返回結果以後,將event信息做本地緩存。若是下次再查詢,先查看本地緩存中是否存在,若是有就直接返回。

最後,根據web服務器返回的event信息,包括了這個event對應着亞馬遜服務器上的uri,經過uri下載視頻數據播放。同時也將視頻數據緩存到本地文件中,供下次查看時使用。

 

3.0 青銅時代

2.0版本完成了0到1 的跨越,可是整個系統與服務還處於初級階段。在剛上線以後,就開始了3.0的開發工做。

3.0版本的主要目的是完成視頻數據與事件的分離。在2.0 版本中,咱們以事件爲單位,向AWS 上傳文件,這種業務模型有着必定侷限性,文件數據強依賴事件。理想的狀態應該是,文件數據應該是一個總體,而不該該按照事件來劃分。事件只須要記錄,其對應的文件數據便可。對於一個事件,咱們只須要在數據庫保存它的一些基本信息(好比時間,類型等等),而後記錄下這個事件對應的數據在雲端的位置。這樣作有兩個好處:

1 數據與事件解耦,雲端存儲的只是一堆文件,易於維護

2 數據能夠複用,好比兩個事件發生的時間有重疊,在2.0版本,重疊的數據就要上傳兩次,浪費了存儲空間

 

 

如圖所示,咱們在上傳本地數據文件時,依然使用分片方式上傳。每讀取一幀數據,判斷一下數據的時間戳有沒有到達事件的開始時間。若是到達,那麼就向web服務器彙報一次事件信息,而且記錄下這個事件的開始在該分片文件中所處的位置。一樣,判斷當前正在處理的事件,比較時間戳,是否已經達到結束時間。若是已經結束,一樣記錄一個結束位置。一個分片文件可能對應多個event,有些event在這個分片文件的某個地方開始,有些event在這個分片文件的某個地方結束,還有些event可能佔有整個分片文件。當一個分片文件上傳結束時,須要向web服務器彙報分片文件信息,包括一些基本信息(大小,媒體參數,以及文件的uri等),以及分片文件與event的映射關係,即event的位置信息。在數據庫的設計中,event存儲一個表項,分片文件存儲一個表項,映射關係存儲一個表項。

關係以下圖所示:

 

在event與file的映射表項中,存儲了event與file id,以及這個event的開始位於file的位置(start_pos)以及結束位置file中的位置(end_pos)。若是這個event不在這個file中開始,也不在這個file中結束,那麼說明這個file處於這個event的中間,既不是第一個分片,也不是最後一個分片,那麼start_pos就是0,end_pos就是分片文件大小,即分片的結束。index就是這個分片文件是該event的第幾個分片文件。

當咱們觀看某個雲視頻時,只須要在數據庫中按照event進行查找,便可以返回這個event的全部分片文件。觀看端拿到這些分片文件信息去亞馬遜S3下載,就行播放。

對於數據庫的影響:

2.0版本中,對於一個event在上傳一個分片文件以後,就要向web服務器彙報一次。web服務器判斷該event是不是第一次彙報,若是是在數據庫插入一行新的表項;若是不是,則要更新以前插入的表項

3.0版本中,分片文件每次彙報,只須要插入表項便可,沒有更新操做。event信息在開始的時候彙報一次,在結束的時候須要更新一次。

總體來講,3.0版本中減小了數據庫的update操做。搞過數據庫的人都知道,更新操做比插入對數據庫的消耗大得多,從某種意義上來講也變相減輕了數據庫的負載。

在3.0版本中,咱們修改了redis的使用策略。2.0版本僅僅用redis來統計天天的event數量,可是其實在查詢的時候,咱們並不須要關心有多個數量。移動端查詢時,是按業來查詢的,每次查詢10個,每次向下翻頁就再查詢10個,沒法再翻頁時,就說明已經查詢出當天全部數據了。爲了提升查詢性能,咱們將event的信息存儲在redis裏面。包括event 的觸發時間,時長,icon信息。按照日期+cid(採集端的id,惟一標識)+type(event類型)做爲key, value是一個list類型的值,保存當天全部的event id信息。而後再用eventid做key, value保存event的詳細信息。這樣在查詢時,先按照cid+日期+類型找到列表key,從裏面讀取一頁的數據。而後再根據這一頁的數據,去查詢裏面每一個event的詳細信息。這樣在查詢列表時就不要再訪問數據庫了。

濃縮視頻,壓倒數據庫的最後一根稻草

3.0版本上線三個月以後,系統運行的還算良好,可是咱們發現數據庫表項在飛速膨脹。咱們的雲服務用戶已經有幾萬個,每一個採集端天天平均都要上傳幾十條視頻,因此按照這種速度,單表記錄很快就來到了將近1000w。在mysql上,1000萬幾乎就是單表記錄上限了。搞web的兄弟發現這一趨勢後,作了分表方案。按照採集端的cid尾數 即(0-9),將event,file,以及映射表分紅了10張表。雖然是解決了存儲方面的問題,可是隨着使用雲服務的用戶在不斷增長,數據庫的訪問壓力也在漸增。在3.0版本,咱們新增了濃縮視頻功能,就是將一天中的視頻變化壓縮成很短的幾分鐘。因爲短視頻天天才產生一個,因此咱們在當天錄製完以後,次日的0點以後開始上傳前一天產生的濃縮視頻。這個功能在3.0版本上運行了一段時間,剛開始沒有問題。可是在不知不覺中,卻爲本身刨了一個大坑。那段時間運營部門搞促銷活動,用戶登陸送積分,用積分贈送雲服務。忽然有一天,測試人員早上過來後發現前一天的濃縮視頻沒有上傳,翻開採集端日誌一看,在凌晨0點以後那段時間,全部的web請求所有失敗了。讓運維同窗查看了下凌晨那段時間發生了啥,一看驚呆了,在0點0分0秒那一刻,瞬間涌入了上萬的請求。web服務器還好,有負載均衡,可是數據庫只有一臺,1s以內成千上萬的請求,數據庫不死纔怪。因爲在採集端作了失敗重試,請求失敗以後又會接着再次請求,數據庫幾乎一直在"臥倒"狀態。幸虧的是,採集端作了重試次數限制,因此基本在凌晨1點以後請求數也就慢慢降下來了。而這一切,都是因爲濃縮視頻集中在凌晨那段時間上傳致使的。作促銷活動的那幾天,天天都會送出1w多的雲服務,一會兒就把數據庫壓垮了。其實解決這個問題的方法很簡單,對於濃縮視頻來講,咱們只要保證上傳了就能夠,不必非得所有擠在0點這個時間。咱們把上傳的時間隨機延長至0~5點之間任何一個時間點,保證用戶在早上起來後能查看到便可。很快就出了更新版本,服務器的訪問壓力隨即降了下來,服務也迴歸正常。可是仍是有一種隱約的不安,由於用戶還在快速增加,不知道哪一天服務器又會遇到相似的問題。

 

4.0 火炮時代

3.0版本告一段落以後,隨即開始了4.0版本的規劃。4.0版本主要要解決的,就是服務器的訪問壓力,包括web服務器以及數據庫。主要的性能瓶頸還在數據庫上, web服務器做水平擴容很簡單,由於在web服務器前面有nginx做爲接入層作負載均衡,新增一臺web服務器直接在nginx上加個配置就好了。可是數據庫由於尚未作分庫,因此只能先優化單臺數據庫的性能。使用Innodb引擎寫性能每秒幾百個,還能再撐一段時間。運行雲存儲服務的採集端大約有幾萬臺,每秒鐘的併發請求量還沒那麼大。可是數據量增加太快倒是一個問題,雖然已經按照採集端的cid作了分表,可是表項的數據按照如今的增加速度很快又會到千萬。分表也不可能這樣無限制的作下去,可是分表策略倒是能夠調整的。其實咱們的雲服務有一個特色,就是數據只保存30天,查詢的時候也是按天來查詢,因此優先應該選擇按天來分表纔對。30天事後,直接刪除掉老的表項,這樣數據就不會無限量的膨脹。天天建一張表,數據量也不會達到單表上限。僅僅是這樣實現一下其實也不復雜,可是考慮到版本兼容就沒那麼簡單了。數據庫仍是隻有一臺,用戶若是仍是使用3.0的版本,咱們也得按照新的分表方式來寫表。這樣就帶來一個問題,即按時間分表,究竟是按照event的觸發時間來分表,仍是按照event的上傳時間來分表?這到底有什麼區別呢。通常狀況下,採集端在觸發報警時,要立立刻傳視頻。可是若是當時斷網了,咱們也會緩存在本地,等到網絡恢復了再上傳。因此有可能在當天觸發的報警視頻在次日才能上傳,也有可能更晚。剛開始想按照event的上傳時間來作分表,這樣作只要在服務器端判斷下當前時間,將請求直接插入到對應日期的表項中就好了。可是這種作法,查詢性能就比較差了。查詢的時候按日期查詢,這個日期是event的觸發時間。咱們並不能確切地知道這一天的報警視頻到底被存儲在哪些表項當中。只能遍歷這一天的先後幾張表,都查詢一遍。很顯然這會影響到查詢性能。因而就考慮按照event的觸發時間來作分表。可是又有另一個問題,每一個event在剛開始上傳時,須要向web服務器彙報一次event信息,結束時要再彙報一次,更新event的上傳狀態和總時長。在開始彙報時,帶了event的觸發時間信息,可是在結束彙報時並無帶時間信息,只有event id。由於在3.0版本中,是根據cid來分表的,在結束彙報時帶了cid信息。可是按照4.0版本的分表方式,老版本的採集端在結束時彙報,緊靠cid信息就不知道到哪張表裏去更新了。簡單的方法就是從當天的表項,往前遍歷,直到查到爲止。可是這樣效率就很低了,更新一次帶來的性能壓力太大。後來想到了利用redis緩存,其實在event第一次彙報信息時,咱們就已經將這些信息記錄在redis裏面了,因此只要根據eventid 在redis裏面查到event的觸發時間,而後就能夠直接插入到數據庫中。這是爲了兼容3.0版本的策略,可是在4.0版本中,咱們直接在申請eventid時,就帶上了日期信息,保證獲取到的eventid的前面幾位就是event的觸發時間日期。這樣根據eventid就能夠知道分表信息了,省略了查詢緩存的過程。4.0版本的優化大概就是這樣了。可是這還遠未結束,僅僅的分表策略終究是有它的極限的,單臺數據庫的讀寫性能就擺在那裏,下一步要作分庫才行。爲了提升性能,還可使用異步化寫入,即數據先保存到緩存中,而後批量寫數據庫,下降數據庫的峯值壓力。

 

總結:

不少時候, 咱們談到高併發 高負載,就會想到集羣 ,分佈式等一些高大上的名詞。可是若是連單機性能都沒有作好,談那些也就是空中樓閣了。記得以前看到,說訪問量排名全世界前20的網站stackoverflow,只有區區20多臺服務器,並且用的是.net。可見對業務自己的優化,比基礎設施的建設更加劇要。業務優化應該達到兩個目的:第一,使你的代碼運行性能更高;第二,使得總體的業務架構易於擴展。談集羣,分佈式部署,也不是一蹴而就。在開發代碼時,就要考慮到可以水平擴展等因素。這樣在將來,擴展集羣時,便也輕鬆了許多

相關文章
相關標籤/搜索