Source: http://www.cnblogs.com/myd620/p/6209061.html

2015年12月,也就是在一年前,開發了半年的云存儲服務上線。這對于付出了半年努力的我們來說,是一件鼓舞人心的事件。因為這個服務在我們手上經歷了從0到1的過程。這是我們自己的一小步,卻是整個云存儲服務的一大步。

我們開發的是一款視頻監控類的軟件,分為視頻采集端跟觀看端。采集端可以是專業攝像頭,手機,無人機等各類智能設備,觀看端一般是手機或者電腦。最基礎的功能,就是視頻觀看,采集端實時采集圖像,編碼,傳輸,觀看端進行點播服務。同時采集端可以監測視頻畫面的運動幅度,然后觸發報警,并且會錄制報警視頻。我們的云存儲服務就是將錄制的報警視頻上傳到云端,并且在觀看端提供查看功能 

2.0 石器時代

第一個版本叫2.0,至于為什么叫2.0,或許這只是一個代號而已。

整個系統的框架如下:

 

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

我們要求系統要盡可能及時的上傳報警視頻。一個報警視頻大概錄制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。可見對業務本身的優化,比基礎設施的建設更加重要。業務優化應該達到兩個目的:第一,使你的代碼運行性能更高;第二,使得整體的業務架構易于擴展。談集群,分布式部署,也不是一蹴而就。在開發代碼時,就要考慮到能夠水平擴展等因素。這樣在未來,擴展集群時,便也輕松了許多


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 AutoPoster 的頭像
    AutoPoster

    互聯網 - 大數據

    AutoPoster 發表在 痞客邦 留言(0) 人氣()