中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何設計并實現存儲QoS

發布時間:2021-11-23 21:42:29 來源:億速云 閱讀:217 作者:柒染 欄目:云計算

如何設計并實現存儲QoS,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。

1. 資源搶占問題

隨著存儲架構的調整,眾多應用服務會運行在同一資源池中,對外提供統一的存儲能力。資源池內部可能存在多種流量類型,如上層業務的IO流量、存儲內部的數據遷移、修復、壓縮等,不同的流量通過競爭的方式確定下發到硬件的IO順序,因此無法確保某種流量IO服務質量,比如內部數據遷移流量可能占用過多的帶寬影響業務流量讀寫,導致存儲對外提供的服務質量下降,由于資源競爭結果的不確定性無法保障存儲對外能提供穩定的集群環境。

如下面交通圖所示,車輛逆行、加塞隨心隨遇,行人橫穿、閑聊肆無忌憚,最終出現交通擁堵甚至安全事故。

如何設計并實現存儲QoS

2. 如何解決資源搶占

類比上一幅交通圖,如何規避這樣的現象大家可能都有自己的一些看法,這里先引入兩個名詞

  • QoS,即服務質量,根據不同服務類型的不同需求提供端到端的服務質量。

  • 存儲QoS,在保障服務帶寬與IOPS的情況下,合理分配存儲資源,有效緩解或控制應用服務對資源的搶占,實現流量監控、資源合理分配、重要服務質量保證以及內部流量規避等效果,是存儲領域必不可少的一項關鍵技術。

那么QoS應該怎么去做呢?下面還是結合交通的例子進行介紹說明。

2.1 流量分類

從前面的圖我們看到不管是什么車,都以自我為中心,不受任何約束,我們首先能先到的辦法是對道路進行分類劃分,比如分為公交車專用車道、小型車專用車道、大貨車專用車道、非機動車道以及人行橫道等,正常情況下公交車車道只允許公交車運行,而非機動車道上是不允許出現機動車的,這樣我們可以保證車道與車道之間不受制約干擾。

如何設計并實現存儲QoS

同樣,存儲內部也會有很多流量,我們可以為不同的流量類型分配不同的 “車道”,比如業務流量的車道我們劃分寬一些,而內部壓縮流量的車道相對來說可以窄一些,由此引入了QoS中一個比較重要的概覽就是流量分類,根據分類結果可以進行更加精準個性化的限流控制。

2.2 流量優先級

僅僅依靠分類是不行的,因為總有一些特殊情況,比如急救車救人、警車抓人等,我們總不能說這個車道只能跑普通私家小轎車把,一些特殊車輛(救護車,消防車以及警車等)應該具有優先通行的權限。

如何設計并實現存儲QoS

對于存儲來說業務流量就是我們的特殊車輛,我們需要保證業務流量的穩定性,比如業務流量的帶寬跟IOPS不受限制,而內部流量如遷移、修復則需要限定其帶寬或者IOPS,為其分配固定的“車道”。在資源充足的情況下,內部流量可以安安靜靜的在自己的車道上行駛,但是當資源緊張,比如業務流量突增或者持續性的高流量水位,這個時候需要限制內部流量的道理寬度,極端情況下可以暫停。當然,如果內部流量都停了還是不能滿足正常業務流量的讀寫需求,這個時候就需要考慮擴容的事情了。

QoS中另外一個比較重要的概念就是優先級劃分,在資源充足的情況下執行預分配資源策略,當資源緊張時對優先級低的服務資源進行動態調整,進行適當的規避或者暫停,在一定程度上可以彌補預分配方案的不足。

2.3 流量監控

前面提到當資源不足時,我們可以動態的去調整其他流量的閾值,那我們如何知道資源不足呢?這個時候我們是需要有個流量監控的組件。

如何設計并實現存儲QoS

我們出行時經常會使用地圖,通過選擇合適的線路以最快到達目的地。一般線路會通過不同的顏色標記線路擁堵情況,比如紅色表示堵車、綠色表示暢通。

存儲想要知道機器或者磁盤當前的流量情況有兩種方式:

  • 統計機器負載情況,比如我們經常去機器上通過iostat命名查看各個磁盤的io情況,這種方式與機器上的應用解耦,只關注機器本身

  • 統計各個應用下發的讀寫流量,比如某臺機器上部署了一個存儲節點應用,那我們可以統計這個應用下發下去的讀寫帶寬及IOPS

第二種方式相對第一種可以實現應用內部更細的流量分類,比如前面提到的一個存儲應用節點,就包含了多種流量,我們不能通過機器的粒度對所有流量統一限流。

3. 常見QoS限流算法

3.1 固定窗口算法

  • 按時間劃分為多個限流窗口,比如1秒為一個限流窗口大小;

  • 每個窗口都有一個計數器,每通過一個請求計數器會加一;

  • 當計數器大小超過了限制大小(比如一秒內只能通過100個請求),則窗口內的其他請求會被丟棄或排隊等待,等到下一個時間節點計數器清零再處理請求。

如何設計并實現存儲QoS

固定窗口算法的理想流量控制效果如上左側圖所示,假定設置1秒內允許的最大請求數為100,那么1秒內的最大請求數不會超過100。

但是大多數情況下我們會得到右側的曲線圖,即可能會出現流量翻倍的效果。比如前T1~T2時間段沒有請求,T2~T3來了100個請求,全部通過。下一個限流窗口計數器清零,然后T3T4時間內來了100個請求,全部處理成功,這個時候時間段T4T5時間段就算有請求也是不能處理的,因此超過了設定閾值,最終T2~T4這一秒時間處理的請求為200個,所以流量翻倍。

小結

  • 算法易于理解,實現簡單;

  • 流量控制不夠精細,容易出現流量翻倍情況;

  • 適合流量平緩并允許流量翻倍的模型。

3.2 滑動窗口算法

前面提到固定窗口算法容易出現流量控制不住的情況(流量翻倍),滑動窗口可以認為是固定窗口的升級版本,可以規避固定窗口導致的流量翻倍問題。

  • 時間窗口被細分若干個小區間,比如之前一秒一個窗口(最大允許通過60個請求),現在一秒分成3個小區間,每個小區間最大允許通過20個請求;

  • 每個區間都有一個獨立的計數器,可以理解一個區間就是固定窗口算法中的一個限流窗口;

  • 當一個區間的時間用完,滑動窗口往后移動一個分區,老的分區(T1~T2)被丟棄,新的分區(T4~T5)加入滑動窗口,如圖所示。

如何設計并實現存儲QoS

小結

  • 流量控制更加精準,解決了固定窗口算法導致的流量翻倍問題;

  • 區間劃分粒度不易確定,粒度太小會增加計算資源,粒度太大又會導致整體流量曲線不夠平滑,使得系統負載忽高忽低;

  • 適合流量較為穩定,沒有大量流量突增模型。

3.3 漏斗算法

  • 所有的水滴(請求)都會先經過“漏斗”存儲起來(排隊等待);

  • 當漏斗滿了之后,多余的水會被丟棄或者進入一個等待隊列中;

  • 漏斗的另外一端會以一個固定的速率將水滴排出。

如何設計并實現存儲QoS

對于漏斗而言,他不清楚水滴(請求)什么時候會流入,但是總能保證出水的速度不會超過設定的閾值,請求總是以一個比較平滑的速度被處理,如圖所示,系統經過漏斗算法限流之后,流量能保證在一個恒定的閾值之下。

小結

  • 穩定的處理速度,可以達到整流的效果,主要對下游的系統起到保護作用;

  • 無法應對流量突增情況,所有的請求經過漏斗都會被削緩,因此不適合有流量突發的限流場景;

  • 適合沒有流量突增或想達到流量整合以固定速率處理的模型。

3.4 令牌桶算法

令牌桶算法是漏斗算法的一種改進,主要解決漏斗算法不能應對流量突發的場景

  • 以固定的速率產生令牌并投入桶中,比如一秒投放N個令牌;

  • 令牌桶中的令牌數如果大于令牌桶大小M,則多余的令牌會被丟棄;

  • 所有請求到達時,會先從令牌桶中獲取令牌,拿到令牌則執行請求,如果沒有獲取到令牌則請求會被丟棄或者排隊等待下一次嘗試獲取令牌。

如何設計并實現存儲QoS

如圖所示,假設令牌投放速率為100/s,桶能存放最大令牌數200,當請求速度大于另外投放速率時,請求會被限制在100/s。如果某段時間沒有請求,這個時候令牌桶中的令牌數會慢慢增加直到200個,這是請求可以一次執行200,即允許設定閾值內的流量并發。

小結

  • 流量平滑;

  • 允許特定閾值內的流量并發;

  • 適合整流并允許一定程度流量突增的模型。

就單純的以算法而言,沒有哪個算法最好或者最差的說法,需要結合實際的流量特征以及系統需求等因素選擇最合適的算法。

四、存儲QoS設計及實現

4.1 需求

一般而言一臺機器會至少部署一個存儲節點,節點負責多塊磁盤的讀寫請求,而存儲請求由分為多種類型,比如正常業務的讀寫流量、磁盤損壞的修復流量、數據刪除出現數據空洞后的空間壓縮流量以及多為了降低多副本存儲成本的糾刪碼(EC)遷移流量等等,不同流量出現在同一個存儲節點會相互競爭搶占系統資源,為了更好的保證業務服務質量,需要對流量的帶寬以及IOPS進行限制管控,比如需要滿足以下條件:

如何設計并實現存儲QoS

  • 可以同時限制流量的帶寬跟IOPS,單獨的帶寬或者IOPS限制都會導致另外一個參數不受控制而影響系統穩定性,比如只控制了帶寬,但是沒有限制IOPS,對于大量小IO的場景就會導致機器的ioutil過高;

  • 可以實現磁盤粒度的限流,避免機器粒度限流導致磁盤流量過載,比如圖所示,ec流量限制節點的帶寬最大值為10Mbps,預期效果是想每塊磁盤分配2Mbps,但是很有可能這10Mbps全部分配到了第一個磁盤;

  • 可以支持流量分類控制,根據不同的流量特性設置不同的限流參數,比如業務流量是我們需要重點保護的,因此不能對業務流量進行限流,而EC、壓縮等其他流量均為內部流量,可以根據其特性配置合適的限流閾值;

  • 可以支持限流閾值的動態適配,由于業務流量不能進行流控,對于系統而言就像一匹“脫韁野馬”,可能突增、突減或持續高峰,針對突增或持續高峰的場景系統需要盡可能的為其分配資源,這就意味著需要對內部流量的限流閾值進行動態的打壓設置是暫停規避。

4.2 算法選擇

前面提到了QoS的算法有很多,這里我們結合實際需求選擇滑動窗口算法,主要有以下原因:

  • 系統需要控制內部流量而內部流量相對比較穩定平緩;

  • 可以避免流量突發情況而影響業務流量;

QoS組件除了滑動窗口,還需要添加一個緩存隊列,當請求被限流之后不能被丟棄,需要添加至緩存隊列中,等待下一個時間窗口執行,如下圖所示。

如何設計并實現存儲QoS

4.3 帶寬與IOPS同時限制

為了實現帶寬與IOPS的同時控制,QoS組件將由兩部分組成:IOPS控制組件負責控制讀寫的IOPS,帶寬控制組件負責控制讀寫的帶寬,帶寬控制跟IOPS控制類似,比如帶寬限制閾值為1Mbps,那么表示一秒最多只能讀寫1048576Bytes大小數據;假定IOPS限制為20iops,表示一秒內最多只能發送20次讀寫請求,至于每次讀寫請求的大小并不關心。

如何設計并實現存儲QoS

兩個組件內部相互隔離,整體來看又相互影響,比如當IOPS控制很低時,對應的帶寬可能也會較小,而當帶寬控制很小時對應的IOPS也會比較小。

如何設計并實現存儲QoS

下面以修復流量為例,分三組進行測試

  1. 第一組:20iops-1Mbps

  2. 第二組:40iops-2Mbps

  3. 第三組:80iops-4Mbps

測試結果如上圖所示,從圖中可以看到qos模塊能控制流量的帶寬跟iops維持在設定閾值范圍內。

4.4 流量分類限制

為了區分不同的流量,我們對流量進行標記分類,并為不同磁盤上的不同流量都初始化一個QoS組件,QoS組件之間相互獨立互不影響,最終可以達到磁盤粒度的帶寬跟IOPS控制。

如何設計并實現存儲QoS

4.5 動態閾值調整

前面提到的QoS限流方案,雖然能夠很好的控制內部流量帶寬或者IOPS在閾值范圍內, 但是存在以下不足

  • 不感知業務流量現狀,當業務流量突增或者持續高峰時,內部流量與業務流量仍然會存在資源搶占,不能達到流量規避或暫停效果。

  • 磁盤上不同流量的限流相互獨立,當磁盤的整體流量帶寬或者IOPS過載時,內部流量閾值不能動態調低也會影響業務流量的服務質量。

所以需要對QoS組件進行一定的改進,增加流量監控組件,監控組件主要監控不同流量類型的帶寬與IOPS,動態QoS限流方案支持以下功能:

如何設計并實現存儲QoS

  • 通過監控組件獲取流量增長率,如果出現流量突增,則動態調低滑動窗口閾值以降低內部流量;當流量恢復平緩,恢復滑動窗口最初閾值以充分利用系統資源。

  • 通過監控組件獲取磁盤整體流量,當整體流量大小超過設定閾值,則動態調低滑動窗口大小;當整體流量大小低于設定閾值,則恢復滑動窗口至初始閾值。

下面設置磁盤整體流量閾值2Mbps-40iops,ec流量的閾值為10Mbps-600iops

當磁盤整體流量達到磁盤閾值時會動態調整其他內部流量的閾值,從測試結果可以看到ec的流量受動態閾值調整存在一些波動,磁盤整體流量下去之后ec流量閾值又會恢復到最初閾值(10Mbps-600iops),但是可以看到整體磁盤的流量并沒有控制在2Mbps-40iops以下,而是在這個范圍上下波動,所以我們在初始化時需要保證設置的內部流量閾值小于磁盤的整體流量閾值,這樣才能達到比較穩定的內部流量控制效果。

如何設計并實現存儲QoS

4.6 偽代碼實現

前面提到存儲QoS主要是限制讀寫的帶寬跟IOPS,具體應該如何去實現呢?IO讀寫主要涉及以下幾個接口。

Read(p []byte) (n int, err error)ReadAt(p []byte, off int64) (n int, err error)Write(p []byte) (written int, err error)WriteAt(p []byte, off int64) (written int, err error)

所以這里需要對上面幾個接口進行二次封裝,主要是加入限流組件。

帶寬控制組件實現

Read實現

// 假定c為限流組件func (self *bpsReader) Read(p []byte) (n int, err error) {
 size := len(p)  size = self.c.assign(size) //申請讀取文件大小
 n, err = self.underlying.Read(p[:size]) //根據申請大小讀取對應大小數據  self.c.fill(size - n) //如果讀取的數據大小小于申請大小,將沒有用掉的計數填充至限流窗口中  return}

Read限流之后會出現以下情況

  • 讀取大小n<len(p)且err=nil,比如需要讀4K大小,但是當前時間窗口只能允許讀取3K,這個是被允許的

這里也許你會想,Read限流的實現怎么不弄個循環呢?如直到讀取指定大小數據才返回。這里的實現我們需要參考標準的IO的讀接口定義,其中有說明在讀的過程中如果準備好的數據不足len(p)大小,這里直接返回準備好的數據,而不是等待,也就是說標準的語義是支持只讀部分準備好的數據,因此這里的限流實現保持一致。

// Reader is the interface that wraps the basic Read method.//// Read reads up to len(p) bytes into p. It returns the number of bytes// read (0 <= n <= len(p)) and any error encountered. Even if Read// returns n < len(p), it may use all of p as scratch space during the call.// If some data is available but not len(p) bytes, Read conventionally// returns what is available instead of waiting for more.// 省略//// Implementations must not retain p.type Reader interface {  Read(p []byte) (n int, err error)}

ReadAt實現

下面介紹下ReadAt的實現,從接口的定義來看,可能覺得ReadAt與Read相差不大,僅僅是指定了數據讀取的開始位置,細心的小伙伴可能發現我們這里實現時多了一層循環,需要讀到指定大小數據或者出現錯誤才返回,相比Read而言ReadAt是不允許出現*n<len(p)且err==nil*的情況

func (self *bpsReaderAt) ReadAt(p []byte, off int64) (n int, err error) {  for n < len(p) && err == nil {    var nn int    nn, err = self.readAt(p[n:], off)    off += int64(nn)    n += nn  }  return}
func (self *bpsReaderAt) readAt(p []byte, off int64) (n int, err error) {  size := len(p)  size = self.c.assign(size)  n, err = self.underlying.ReadAt(p[:size], off)  self.c.fill(size - n)  return}
// ReaderAt is the interface that wraps the basic ReadAt method.//// ReadAt reads len(p) bytes into p starting at offset off in the// underlying input source. It returns the number of bytes// read (0 <= n <= len(p)) and any error encountered.//// When ReadAt returns n < len(p), it returns a non-nil error// explaining why more bytes were not returned. In this respect,// ReadAt is stricter than Read.//// Even if ReadAt returns n < len(p), it may use all of p as scratch// space during the call. If some data is available but not len(p) bytes,// ReadAt blocks until either all the data is available or an error occurs.// In this respect ReadAt is different from Read.//省略//// Implementations must not retain p.type ReaderAt interface {  ReadAt(p []byte, off int64) (n int, err error)}

Write實現

Write接口的實現相對比較簡單,循環寫直到寫完數據或者出現錯誤

func (self *bpsWriter) Write(p []byte) (written int, err error) {  size := 0  for size != len(p) {    p = p[size:]    size = self.c.assign(len(p))
   n, err := self.underlying.Write(p[:size])    self.c.fill(size - n)    written += n    if err != nil {      return written, err    }  }  return}
// Writer is the interface that wraps the basic Write method.//// Write writes len(p) bytes from p to the underlying data stream.// It returns the number of bytes written from p (0 <= n <= len(p))// and any error encountered that caused the write to stop early.// Write must return a non-nil error if it returns n < len(p).// Write must not modify the slice data, even temporarily.//// Implementations must not retain p.type Writer interface {  Write(p []byte) (n int, err error)}

WriteAt實現

這里的實現跟Write類似

func (self *bpsWriterAt) WriteAt(p []byte, off int64) (written int, err error) {  size := 0  for size != len(p) {    p = p[size:]    size = self.c.assign(len(p))
   n, err := self.underlying.WriteAt(p[:size], off)    self.c.fill(size - n)    off += int64(n)    written += n    if err != nil {      return written, err    }  }  return}
// WriterAt is the interface that wraps the basic WriteAt method.//// WriteAt writes len(p) bytes from p to the underlying data stream// at offset off. It returns the number of bytes written from p (0 <= n <= len(p))// and any error encountered that caused the write to stop early.// WriteAt must return a non-nil error if it returns n < len(p).//// If WriteAt is writing to a destination with a seek offset,// WriteAt should not affect nor be affected by the underlying// seek offset.//// Clients of WriteAt can execute parallel WriteAt calls on the same// destination if the ranges do not overlap.//// Implementations must not retain p.type WriterAt interface {  WriteAt(p []byte, off int64) (n int, err error)}

IOPS控制組件實現

IOPS控制組件的實現跟帶寬類似,這里就不詳細介紹了

Read接口實現
func (self *iopsReader) Read(p []byte) (n int, err error) {  self.c.assign(1) //這里只需要獲取一個計數,如果當前窗口一個都沒有,則會一直等待直到獲取到一個才喚醒執行下一步  n, err = self.underlying.Read(p)  return}
ReadAt接口實現
func (self *iopsReaderAt) ReadAt(p []byte, off int64) (n int, err error) {  self.c.assign(1)  n, err = self.underlying.ReadAt(p, off)  return}

想想這里的ReadAt為啥不需要跟帶寬一樣循環讀了呢?


Write接口實現
func (self *iopsWriter) Write(p []byte) (written int, err error) {  self.c.assign(1)  written, err = self.underlying.Write(p)  return}

WriteAt

func (self *iopsWriterAt) WriteAt(p []byte, off int64) (n int, err error) {  self.c.assign(1)  n, err = self.underlying.WriteAt(p, off)  return}

看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

qos
AI

盈江县| 池州市| 衡南县| 鄂托克前旗| 离岛区| 盐源县| 孝昌县| 沁源县| 奎屯市| 民权县| 广灵县| 韩城市| 蛟河市| 安阳县| 房产| 辽阳县| 香格里拉县| 阳新县| 永昌县| 兴宁市| 兴海县| 怀安县| 高青县| 舞阳县| 定南县| 安吉县| 静乐县| 子洲县| 澄迈县| 全椒县| 海淀区| 石渠县| 万安县| 申扎县| 江陵县| 商洛市| 肇州县| 常宁市| 陇川县| 蛟河市| 唐河县|