使用.NET Core搭建分散式音頻效果處理服務(四)選擇垂直擴展還是水平擴展?

来源:https://www.cnblogs.com/SteveLee/archive/2018/08/14/9477780.html
-Advertisement-
Play Games

眾所周知垂直擴展是提升單機的性能的方式,比如提升雙路、四路的CPU運算能力,加大記憶體,更換速度更快的SSD,或者從代碼根本上進行優化和性能提升。水平擴展是提供多台多種伺服器分離單機性能的方式,比如集群,主從,隊列,負載平衡等等。 白話的垂直擴展 現在伺服器都是雲伺服器,單純從單機的硬體性能提升整體性 ...


眾所周知垂直擴展是提升單機的性能的方式,比如提升雙路、四路的CPU運算能力,加大記憶體,更換速度更快的SSD,或者從代碼根本上進行優化和性能提升。水平擴展是提供多台多種伺服器分離單機性能的方式,比如集群,主從,隊列,負載平衡等等。

 

 白話的垂直擴展

現在伺服器都是雲伺服器,單純從單機的硬體性能提升整體性能,可能已經不太適用,而從代碼上,其實還有些功課可以做,即使不多:

  1. 優化多線程協調模式,優化多線程下資源共用問題,以免出現奇怪的運行時錯誤。
  2. 改同步為非同步:此方法提升的是吞吐率,性能並不能提升,不過對於客戶端響應也算是件好事吧。
  3. 使用磁碟預讀模式,極小幅度提升IO性能。
  4. 使用單機任務隊列,強制任務有序進行:此方法在單機上不會提升性能,甚至會減少原本的單機吞吐率,但是卻能保證任務在同一時刻的完整性。

我們先從垂直擴展中壓榨單機的性能,同時還要保證穩定性,甚至穩定性比單機極限性能更加重要,為何?因為多線程(web伺服器都是多線程模型)資源互斥問題,會讓你查找問題的時候抓狂(當然,如果你要訪問的資源只是單個,就另當別論了)。因此,很多時候我們通常會加鎖來避免這類事情發生(鎖的問題和功能我們這裡不討論),雖然犧牲了性能,但卻換來了每次高請求所帶來的穩定性。

其次,我們知道,web伺服器都屬於多線程模型,這樣設計的目的是為了提高該伺服器的整體吞吐量(不同伺服器語言採用不同的線程開闢模式,例如java使用的是系統級的線程),當一個線程正在接近滿負荷的處理當前的任務,緊接著馬上又來一個請求(系統不會因為當前正在運行任務而終止新的請求),那麼將是雪上加霜的,多個任務同時長時間在搶占同一個CPU資源,無疑是對整體影響甚大的。

言歸正傳,我們在單機上面針對這類問題,既要儘可能的減少處理時間,又要絕對保證整體運行期間的穩定性(後期會介紹如何使用熔斷機制提升多台伺服器系統的整體穩定性)。

上一節,我們已經創建了一個同步的介面,下麵我們將這個介面稍作改動,使其成為包含非同步任務方法的介面,整體代碼就不貼上來了,以免影響篇幅

  1. 如果你喜歡手動創建與管理任務,那麼你可以new一個Task<TResult>實例。
  2. 如果你喜歡讓系統為你管理該任務狀態,那麼你可以Task.Factary.StartNew來新建一個實例。

 

 白話的水平擴展

當一條街道上的小區越來越多,用水越來越大,而住戶反應水壓卻越來越小,你是考慮增加主管道通水量大小、還是考慮增加每個小區的增壓泵的功率、還是考慮增加主管道的數量(目前筆者小區就遇到水壓不夠的情況)。

在軟體工程項目中,其實伺服器TPS跟水壓是同一個概念。

  1. 加大主水管道(如同提升CPU、記憶體)始終會有一個極限;
  2. 增加每個小區增壓泵的功率(如同客戶端使用大量的輪詢,可主管道出口就只有那麼點點量)始終要求比得到的多得多;

因此換句話說:

  1. 增加伺服器數量(畢竟伺服器比自來水廠容易建設:-)),提升管道入口的處理能力;
  2. 增加不同伺服器類型,例如隊列伺服器,負載伺服器,緩存伺服器等等中間伺服器,分攤和分離不同功能分到而行,如同主管道的分流閥,節流閥,增壓泵等等;
  3. 增加帶寬(這個是肯定的,提升TPS帶寬肯定也是主要的);

 

一:點對點——原裝

當然,如果介面已經成為了非同步模式(本質其實是提前返回請求,但並沒有返回請求所處理的結果),那麼還需要一個介面來告訴客戶端處理的結果,客戶端通過該介面的輪詢獲得實時的結果。

在筆者介紹的這個服務中,流程架構如下:

  非常簡單的點對點模式,用戶請求一次,等待伺服器處理響應完成後釋放,所有內容均採用同步方式進行,得到結果是:

 非常簡單的點對點模式,用戶請求一次,等待伺服器處理響應完成後釋放,所有內容均採用同步方式進行,得到結果是:

用戶等待時間 = 伺服器處理時間

如果用戶上傳10秒,而伺服器處理需要4秒,那麼這個等待對於用戶來說,是極為煎熬的。

也許聰明的你會說,在客戶端給個友好的提示,比如讓一個“風火輪”不停的轉動,當處理完成後隱藏掉。的確,這從另一個角度上看確實也行得通(比如成本因素),但我們不討論用戶的視覺和等待等感覺上的東西,只討論從技術上如何讓這個響應時間更快,能快到幾乎讓用戶察覺不到。

 

二:使用並行任務——小幅提升

單機並行模式大家應該都明白,畢竟現在CPU都是多核的了,幹嘛要讓其他CPU閑著呢,不管是JAVA還是C#,目前主流語言都可以完美的執行並行任務(python開多進程其實也算),各種語法請自行Google,既然文章標題是Net,那麼筆者就少量的複製一下C#的代碼。

Parallel.Invoke(() => { },() => { });

 

哇撒,真的很少,就是C#中的一個並行執行的語句而已,自己需要並行執行的代碼放入花括弧中就行,換成流程結構圖如下:

畫的很搓,歡迎拍磚。

筆者採用的CPU是I7-2700K,並行任務狀態確實使用了起來,但減少時間卻只有1秒,很不可思議,或許是筆者的代碼優化不夠好吧(並行原理和理想結果為何有出入請自行Google),所以就不毛遂自薦的貼上來了:-),但是這個3秒時間我會跟他死磕到底——用戶不能等。

 

三:分離用戶請求和耗時處理——非同步

 當朋友們看到這裡的時候,或許心裡早就想到用非同步的方式來實現C/S的介面請求了,對,但我們還是需要走一下流程,梳理一下思路。請繼續接著看。

非同步其實就是多線程,只是目前由於高級語言的發展,已經將線程的難點給隱藏掉了,在一個請求主線程中,新建一個非同步線程(或任務),分離主線程的長時間處理耗時,將這塊難啃的骨頭交給子線程去做,自己只管輕鬆的執行到return,是的確很舒服哦(筆者也夢想擁有這樣的碼磚方式,o(∩_∩)o 哈哈),new一個線程我們不做介紹,畢竟他的管理模式是純手動的、並且是複雜的,我們只介紹new一個任務來分離主線程之間的關聯。

正如之前提到,微軟巴巴已經將這種模型給封裝好了,只需要在介面處理函數內、將處理模塊塞進Task中即可,不用再去new一個線程、管理這個線程的狀態、什麼時候調度、什麼時候阻塞等等一些較底層的操作。萬事有好必有壞,多線程模型創建是很簡單了,相應的實現細節對於很多入門的朋友就看不懂了。

不過,當一個對外介面(或者內部函數)採用非同步模式,那麼調用端也需要進行輪詢(非同步同步無所謂,看調用如何實現)處理結果,這個模式相比原來常規同步複雜許多,需要建立任務、執行任務、存儲任務狀態和結果等等,不廢話,上圖:

通過將“伺服器處理耗時”進行分離,請求主線程只需要將相應的參數傳遞給子線程(或任務),主線程就直接返回到客戶端,如果忽略子任務之前的邏輯時間複雜度,完全可以達到瞬間返回到客戶端,具體時長根據不同的平臺和架構不同而不同,正如之前國外有人對NET CORE和GO進行過空業務響應對比(具體鏈接得找找),在請求數高於100W(包括併發)和沒有任何邏輯代碼的前提下,直接請求某個介面,NET CORE只比GO慢了近40ms(相同單機)。這樣的性能還是非常看好的。

另外,如果處理時間過長,而且子任務不能及時返回,那將產生越來越多的任務阻塞,畢竟一個CPU是有極限的,並且伴隨著或多或少的運行時錯誤,而這種錯誤是最讓我們程式員頭疼的,因此,這時我們需要加入單機隊列,來限制和防止處理和請求達到瞬時波峰,文章結尾提供一份單機隊列的代碼供大家參考:

實際證明,這樣對於用戶來說,是瞬間的,不用等待的,極大的提升了用戶體驗。不過呢,如果請求數越多,那麼越後進來的請求,等待的時間將越長,對於客戶端輪詢的時間也將變得更長。

輪詢時間 = 請求數(單機隊列數) * 單個處理耗時時間

好像比原來的點對點更糟糕了,實際我們根據這個架構進行擴展,將得到更好的體驗,請繼續接著看。

 

四:讓多台機器一起工作吧——集群

先看張圖:

哇塞,一下子變得這麼複雜,好捉急啊。其實並不難理解,我們來看一看做了哪些變化:

  1. 單機的隊列擴展為了使用伺服器做隊列集群;
  2. 增加調度任務;
  3. 將多個處理服務分配到多台機器上運行;
  4. 單機緩存增加到緩存集群;

其他也就沒什麼花頭了。當請求任務過高,放入隊列中,分離前級請求和後級處理,後級處理伺服器的數量將直接影響整個平臺的非同步處理時間。如果非要對比單機模式,性能是隨處理伺服器的數量增加而提高的。下一節我們將詳細討論這套架構方案。

 

感謝閱讀!

 

(附上單機隊列的實現,僅供參考)

  1 /// <summary>
  2 /// 非同步任務隊列
  3 /// </summary>
  4 public class AsyncTaskQueue : IDisposable
  5 {
  6     private bool _isDisposed;
  7     private readonly ConcurrentQueue<AwaitableTask> _queue = new ConcurrentQueue<AwaitableTask>();
  8     private Thread _thread;
  9     private AutoResetEvent _autoResetEvent;
 10 
 11     /// <summary>
 12     /// 非同步任務隊列
 13     /// </summary>
 14     public AsyncTaskQueue()
 15     {
 16         _autoResetEvent = new AutoResetEvent(false);
 17         _thread = new Thread(InternalRuning) {IsBackground = true};
 18         _thread.Start();
 19     }
 20 
 21     private bool TryGetNextTask(out AwaitableTask task)
 22     {
 23         task = null;
 24         while (_queue.Count > 0)
 25         {
 26             if (_queue.TryDequeue(out task) && (!AutoCancelPreviousTask || _queue.Count == 0)) return true;
 27             task.Cancel();
 28         }
 29 
 30         return false;
 31     }
 32 
 33     private AwaitableTask PenddingTask(AwaitableTask task, int maxQueueCount = 1000)
 34     {
 35         lock (_queue)
 36         {
 37             if (_queue.Count >= maxQueueCount)
 38             {
 39                 throw new Exception($"超出最大隊列數量,maxQueueCount={maxQueueCount}");
 40             }
 41 
 42             Debug.Assert(task != null);
 43             _queue.Enqueue(task);
 44             _autoResetEvent.Set();
 45         }
 46 
 47         return task;
 48     }
 49 
 50     private void InternalRuning()
 51     {
 52         while (!_isDisposed)
 53         {
 54             if (_queue.Count == 0)
 55             {
 56                 _autoResetEvent.WaitOne();
 57             }
 58 
 59             while (TryGetNextTask(out var task))
 60             {
 61                 if (task.IsCancel) continue;
 62 
 63                 if (UseSingleThread)
 64                 {
 65                     task.RunSynchronously();
 66                 }
 67                 else
 68                 {
 69                     task.Start();
 70                 }
 71             }
 72         }
 73     }
 74 
 75     /// <summary>
 76     /// 是否使用單線程完成任務.
 77     /// </summary>
 78     public bool UseSingleThread { get; set; } = true;
 79 
 80     /// <summary>
 81     /// 自動取消以前的任務。
 82     /// </summary>
 83     public bool AutoCancelPreviousTask { get; set; } = false;
 84 
 85     /// <summary>
 86     /// 執行任務
 87     /// </summary>
 88     /// <param name="action"></param>
 89     /// <param name="maxQueueCount"></param>
 90     /// <returns></returns>
 91     public AwaitableTask Run(Action action, int maxQueueCount = 1000)
 92         => PenddingTask(new AwaitableTask(new Task(action, new CancellationToken(false))), maxQueueCount);
 93 
 94     /// <summary>
 95     /// 執行任務
 96     /// </summary>
 97     /// <typeparam name="TResult"></typeparam>
 98     /// <param name="function"></param>
 99     /// <param name="maxQueueCount"></param>
100     /// <returns></returns>
101     public AwaitableTask<TResult> Run<TResult>(Func<TResult> function, int maxQueueCount = 1000)
102         => (AwaitableTask<TResult>) PenddingTask(new AwaitableTask<TResult>(new Task<TResult>(function)),
103             maxQueueCount);
104 
105 
106     /// <inheritdoc />
107     public void Dispose()
108     {
109         Dispose(true);
110         GC.SuppressFinalize(this);
111     }
112 
113     /// <summary>
114     /// 析構任務隊列
115     /// </summary>
116     ~AsyncTaskQueue() => Dispose(false);
117 
118     private void Dispose(bool disposing)
119     {
120         if (_isDisposed) return;
121         if (disposing)
122         {
123             _autoResetEvent.Dispose();
124         }
125 
126         _thread = null;
127         _autoResetEvent = null;
128         _isDisposed = true;
129     }
130 
131     /// <summary>
132     /// 可等待的任務
133     /// </summary>
134     public class AwaitableTask
135     {
136         private readonly Task _task;
137 
138         /// <summary>
139         /// 初始化可等待的任務。
140         /// </summary>
141         /// <param name="task"></param>
142         public AwaitableTask(Task task) => _task = task;
143 
144         /// <summary>
145         /// 任務的Id
146         /// </summary>
147         public int TaskId => _task.Id;
148 
149         /// <summary>
150         /// 任務是否取消
151         /// </summary>
152         public bool IsCancel { get; private set; }
153 
154         /// <summary>
155         /// 開始任務
156         /// </summary>
157         public void Start() => _task.Start();
158 
159         /// <summary>
160         /// 同步執行開始任務
161         /// </summary>
162         public void RunSynchronously() => _task.RunSynchronously();
163 
164         /// <summary>
165         /// 取消任務
166         /// </summary>
167         public void Cancel() => IsCancel = true;
168 
169         /// <summary>
170         /// 獲取任務等待器
171         /// </summary>
172         /// <returns></returns>
173         public TaskAwaiter GetAwaiter() => new TaskAwaiter(this);
174 
175         /// <summary>Provides an object that waits for the completion of an asynchronous task. </summary>
176         [HostProtection(SecurityAction.LinkDemand, ExternalThreading = true, Synchronization = true)]
177         public struct TaskAwaiter : INotifyCompletion
178         {
179             private readonly AwaitableTask _task;
180 
181             /// <summary>
182             /// 任務等待器
183             /// </summary>
184             /// <param name="awaitableTask"></param>
185             public TaskAwaiter(AwaitableTask awaitableTask) => _task = awaitableTask;
186 
187             /// <summary>
188             /// 任務是否完成.
189             /// </summary>
190             public bool IsCompleted => _task._task.IsCompleted;
191 
192             /// <inheritdoc />
193             public void OnCompleted(Action continuation)
194             {
195                 var This = this;
196                 _task._task.ContinueWith(t =>
197                 {
198                     if (!This._task.IsCancel) continuation?.Invoke();
199                 });
200             }
201 
202             /// <summary>
203             /// 獲取任務結果
204             /// </summary>
205             public void GetResult() => _task._task.Wait();
206         }
207     }
208 
209     /// <summary>
210     /// 可等待的任務
211     /// </summary>
212     /// <typeparam name="TResult"></typeparam>
213     public class AwaitableTask<TResult> : AwaitableTask
214     {
215         /// <summary>
216         /// 初始化可等待的任務
217         /// </summary>
218         /// <param name="task">需要執行的任務</param>
219         public AwaitableTask(Task<TResult> task) : base(task) => _task = task;
220 
221 
222         private readonly Task<TResult> _task;
223 
224         /// <summary>
225         /// 獲取任務等待器
226         /// </summary>
227         /// <returns></returns>
228         public new TaskAwaiter GetAwaiter() => new TaskAwaiter(this);
229 
230         /// <summary>
231         /// 任務等待器
232         /// </summary>
233         [HostProtection(SecurityAction.LinkDemand, ExternalThreading = true, Synchronization = true)]
234         public new struct TaskAwaiter : INotifyCompletion
235         {
236             private readonly AwaitableTask<TResult> _task;
237 
238             /// <summary>
239             /// 初始化任務等待器
240             /// </summary>
241             /// <param name="awaitableTask"></param>
242             public TaskAwaiter(AwaitableTask<TResult> awaitableTask) => _task = awaitableTask;
243 
244             /// <summary>
245             /// 任務是否已完成
246             /// </summary>
247             public bool IsCompleted => _task._task.IsCompleted;
248 
249             /// <inheritdoc />
250             public void OnCompleted(Action continuation)
251             {
252                 var This = this;
253                 _task._task.ContinueWith(t =>
254                 {
255                     if (!This._task.IsCancel) continuation?.Invoke();
256                 });
257             }
258 
259             /// <summary>
260             /// 獲取任務結果
261             /// </summary>
262             /// <returns></returns>
263             public TResult GetResult() => _task._task.Result;
264         }
265     }
266 }
View Code

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 教材:《彙編語言》 王爽 第三版 Charpter 1. 基礎知識 1.1 : 機器語言 1.2 : 彙編語言的產生 1.3 : 彙編語言的組成 1.4 : 存儲器 1.5 : 指令和數據 1.6 : 存儲單元 1.7 : CPU對於存儲器的讀寫 1.8 : 地址匯流排 1.9 : 數據匯流排 1.10 ...
  • JMM簡介 Java Memory Model簡稱JMM, 是一系列的Java虛擬機平臺對開發者提供的多線程環境下的記憶體可見性、是否可以重排序等問題的無關具體平臺的統一的保證。(可能在術語上與Java運行時記憶體分佈有歧義,後者指堆、方法區、線程棧等記憶體區域)。併發編程有多種風格,除了CSP(通信順序 ...
  • 一.標識符 標識符:在java程式中,有些名字是我們自己定義的,那麼這些我們自己定義的名字就叫做自定義的標識符 標識符的命名規則: 1.標識符是由字母(a-z A-Z)、數字、下劃線(_)、美元符號($)組成的 2.標識符不能以數字開頭 3.標識符是嚴格區分大小寫的 4.標識符是沒有長度限制的 5. ...
  • 在C#中消息有兩個指向,一個指向Message,一個指向INotify。這裡主要講INotify。 INotify也有人稱之為[通知],不管叫消息還是通知,都是一個意思,就是傳遞信息。 消息的定義 INotify消息其實是一個介面,介面名叫INotifyPropertyChanged。介面定義如下: ...
  • 持續集成配置之Nuget Intro 本文是基於微軟的 VSTS(Visual Studio Team Service) 做實現公眾類庫的自動打包及發佈。 之前自己的項目有通過 Github 上的 Travis 和 Appveyor,這次主要是用 VSTS 來做的,對比 appveyor 和 vst ...
  • 概要 相信很多朋友在程式生涯中,或多或少都會遇到處理媒體流的需求,而且是採用S端處理,排除代碼上課優化的極限,仍然還是需要很長的時間時,比如: 1:百度網盤在播放視頻的時候,如非VIP會員還需要更長甚至直接斷開流; 2:任何直播視頻在轉碼的時候,不論是否VIP,都會有段緩衝時間,已至於觀看者無法達到 ...
  • 什麼是NoSql NoSQL(Not Only SQL),泛指非關係型的資料庫,是對不同於傳統的關係型資料庫的資料庫管理系統的統稱,強調Key-Value Stores和文檔資料庫的優點。為瞭解決大規模數據集合多重數據種類帶來的挑戰而興起的資料庫。有著模式自由,逆規範化,多分區存儲,彈性可擴展,多副 ...
  • 使用ILMerge工具,將C#項目debug目錄下的exe及其依賴的dll文件打包成一個exe文件,直接雙擊就可運行。 使用工具: ILMerge :http://www.microsoft.com/en-us/download/details.aspx?id=17630 ILMerge-GUI:h ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...