最佳化呼叫WebService的頻率
常常有些時候需要大量呼叫WebService來查詢一些資料,但是並不是一秒之內呼叫完畢所有要查詢的資料(例如:有一百萬筆資料需要呼叫WebService去查詢,結果竟然跑迴圈在幾秒鐘之內通通呼叫完畢),這樣反而導致WebService的站台(例如:iis)會過於忙碌,導致無法接受後續的查詢請求,最好是一批一批的查詢去進行呼叫,一批完成了再去進行下一批,才會獲得最佳的效能(例如:每分鐘內可以查詢成功的筆數最多可以??筆)
先講結果:未做最佳化之前,每分鐘查詢成功的筆數大約80筆,最佳化之後則是將近兩百筆,差一倍喔。
本例以Winform為例去呼叫web service,被呼叫的WebService的種類則為wcf,並測試出最佳的每分鐘呼叫wcf次數:
首先先拉出簡單的畫面,讓我們方便確認每分鐘呼叫wcf執行成功有幾筆:
有個查詢按鈕按下去之後便開始一直呼叫web service, 而進度就是用來顯示:目前執行成功的筆數/總共需要執行的筆數
為了要一批一批的資料進行查詢,並且隨時監控執行的進度,就會需要宣告Timer來進行固定週期(例如:每三秒執行一次)性的檢查(Timer是以另開執行緒的方式執行,與UI不同執行緒):tpeProgrssBarTimer是每3秒就會檢查目前執行成功的筆數,tpeQueryTPETimer則是每3秒就會再去檢查是否需要再呼叫下一批資料來查詢,tpeAvgSpeedTimer則是每分鐘就會去檢查該分鐘的平均執行速度。
ps.這是以全域變數的方式宣告喔
System.Windows.Forms.Timer tpeProgrssBarTimer = new System.Windows.Forms.Timer();
System.Windows.Forms.Timer tpeQueryTPETimer = new System.Windows.Forms.Timer();
System.Windows.Forms.Timer tpeAvgSpeedTimer = new System.Windows.Forms.Timer();
一批一批的資料呼叫wcf查詢的過程中,總是會需要有個幾十秒的時間(例如:一次呼叫兩百次wcf查詢資料,從開始呼叫到結束呼叫,也是需要約三十秒的時間),為了避免呼叫的過程中又被呼叫(口語的說就是:你已經正在進行中了,有人還在那邊一直催你趕快進行),需要宣告一個旗標,顯示你目前是否為執行中的狀態:
bool tpeWCFCalling = false;
ps.這是以全域變數的方式宣告喔
為了要每一批確認執行完畢之後才會繼續呼叫下一批wcf,因此需要紀錄 當下執行成功的筆數、這批資料呼叫之前的查詢成功的筆數、總筆數
int tpeNowCount;//現在執行完畢的筆數
int tpeOldCount;//
int tpeTotalCount;//總筆數
ps.也是全域變數
然後也需要記錄每分鐘執行的效率,因此宣告變數來計算每分鐘執行成功的筆數:
int tpeOldMinuteCount; //一分鐘前執行完畢的筆數
int tpeNowMinuteCount; //目前執行完畢的筆數
ps.上面也是全域變數
然後在新增一個function做上列timer的初始化:主要是要設定Timer的執行頻率拉,也一起幫上面的全域變數做初始值設定:這個function可以在form_load的時候一起做即可
private void init()
{
using (myDbEntities db = new myDbEntities())
{
//畫面上總筆數設定
TPETotalCountLbl.Text = db.pft23.Where(p => p.city_code == tpeCityCode).Count().ToString();
//順便把總筆數設定到全域變數
tpeTotalCount = Convert.ToInt32(TPETotalCountLbl.Text);
//畫面上目前執行完畢筆數
TPEFinCountLbl.Text = GetTpeFinCountNow().ToString();
}
tpeProgrssBarTimer.Interval = Convert.ToInt16(Config.ProgressBarCheckTime);//每3 secs在畫面上顯示一次進度
tpeProgrssBarTimer.Tick += new System.EventHandler(tpeProgrssBarTimer_Tick);//
tpeQueryTPETimer.Interval = Convert.ToInt16(Config.SleepTime);//10 secs檢查一次是否要繼續呼叫wcf
tpeQueryTPETimer.Tick += new System.EventHandler(tpeQueryTPETimer_Tick);
tpeAvgSpeedTimer.Interval = Convert.ToInt32(Config.AverageSpeedCheckTime);//每分鐘計算一次平均執行速度
tpeAvgSpeedTimer.Tick += new System.EventHandler(queryTPEAverageSpeed_Tick);
}
然後設定按下查詢按鈕時,timer就開始執行了:GetTpeFinCountNow()會去取得目前執行成功的筆數:
//按下查詢
private void QueryTPEBtn_Click(object sender, EventArgs e)
{
TPETimerStart();
exeTPEStatusLbl.Text = "執行中";
exeTPEStatusLbl.Text = "執行中";
tpeNowCount = GetTpeFinCountNow();
tpeOldCount = tpeNowCount;
tpeNowMinuteCount = tpeNowCount;
tpeOldMinuteCount = tpeNowCount;
MessageBox.Show("已開始查詢!");
}
然後是按下停止按鈕的事件:主要就是停止timer囉
//按下停止查詢按鈕
private void stopTPEBtn_Click(object sender, EventArgs e)
{
tpeProgrssBarTimer.Stop();
tpeQueryTPETimer.Stop();
tpeAvgSpeedTimer.Stop();
exeTPEStatusLbl.Text = "停止";
MessageBox.Show("已停止查詢!");
}
然後設定畫面上執行進度如何顯示,這是透過timer的tick事件實做的:透過timer.interval的參數的設定,目前是每三秒鐘會更新一次進度條,至於那個(執行中...)的文字,純粹是弄好玩的,畫面看起來比較屌XD
//持續更新畫面上查詢的進度
private void tpeProgrssBarTimer_Tick(object sender, EventArgs e)
{
TPEFinCountLbl.Text = GetTpeFinCountNow().ToString();
int temp = DateTime.Now.Millisecond % 3;
if(temp ==0)
{
exeTPEStatusLbl.Text = "執行中。。。。。。";
}
else if (temp == 1)
{
exeTPEStatusLbl.Text = "執行中。。。。";
}
else if (temp == 2)
{
exeTPEStatusLbl.Text = "執行中。。";
}
}
然後就是重點,每三秒鐘,另一個timer就會去確認是否要繼續呼叫Wcf服務,若是呼叫太多,會塞報web service的網頁伺服器,若是塞的太少,執行效率就會很差!
因此會先透過
if (tpeNowCount != tpeTotalCount)
去檢查目前執行完畢的筆數是否等於總筆數,然後透過
if (tpeNowCount == tpeOldCount
|| tpeNowCount >= tpeOldCount + Convert.ToInt32(Config.OnceTakeCount)
- Convert.ToInt32(Config.OnceTakeCount) * Convert.ToDouble(Config.NoResponseRate))
去檢查
1.是否目前執行成功的筆數=呼叫wcf之前執行成功的筆數(tpeNowCount == tpeOldCount),此條件成立的話,就會繼續呼叫下一批wcf。舉例來說:剛剛按下查詢按鈕的時候,會成立這個條件。
2.是否 目前執行成功的筆數 >= 呼叫wcf之前的執行成功的筆數 + 每批的筆數 - web service服務可能responseLost的筆數(tpeNowCount >= tpeOldCount + Convert.ToInt32(Config.OnceTakeCount)
- Convert.ToInt32(Config.OnceTakeCount) * Convert.ToDouble(Config.NoResponseRate)),舉例來說:目前執行成功的筆數如果為1000筆(tpeNowCount ==1100),呼叫wcf之前的執行成功的筆數如果為1180筆(tpeOldCount),每批的筆數是200筆(Convert.ToInt32(Config.OnceTakeCount)),web service服務可能responseLost的筆數如果為20(Convert.ToInt32(Config.OnceTakeCount) * Convert.ToDouble(Config.NoResponseRate))(目前設定wcf的NoResponseRate為0.1來計算的話,200 x 0.1 = 20)來查詢。在完美的情況下,下次又要呼叫wcf服務的時候,那時候執行成功的筆數應該為1000+200=1200筆,但是世間上沒那麼好的事情,web service總是偶爾會給你response失敗一下,可能執行成功的筆數只有到1180筆,有20筆資料ResponseLost了, 所以1180 >= 1000 + 200 - 200 x 0.1這個條件成立了,因此就會繼續呼叫下一批兩百資料查詢wcf。
此外,在要繼續呼叫下一批wcf的情況下,會去判斷
if (tpeWCFCalling == false)
是否成立,因為tpeWCFCalling表示是否已經在呼叫wcf中了,如果已經在呼叫中(呼叫兩百多筆wcf的過程至少也要30多秒),下次timer(因為三秒後timer又會執行同樣的if判斷)又發現符合上述if然後想要呼叫wcf的時候,就會知道已經在呼叫Wcf了,就不會再重複呼叫,以免灌報web service的網頁伺服器。
private void tpeQueryTPETimer_Tick(object sender, EventArgs e)
{
tpeNowCount = GetTpeFinCountNow();//現在執行完畢的筆數
//尚未達到總筆數的時候
if (tpeNowCount != tpeTotalCount)
{
//等上次呼叫的兩百筆執行完才繼續呼叫
if (tpeNowCount == tpeOldCount
|| tpeNowCount >= tpeOldCount + Convert.ToInt32(Config.OnceTakeCount)
- Convert.ToInt32(Config.OnceTakeCount) * Convert.ToDouble(Config.NoResponseRate))
{
//上次呼叫的執行完的時候,接續著呼叫下一個兩百筆wcf
//或是只差一點就執行完畢兩百筆的時候,接著呼叫下一個兩百筆
//呼叫之前,先記錄目前執行完畢的筆數
//如果正在呼叫中,就不要一直呼叫,不然iis會過載
if (tpeWCFCalling == false)
{
tpeOldCount = GetTpeFinCountNow();
tpeWCFCalling = true;
Task.Run(() => QueryTPEByService());
}
}
else if(tpeNowCount < tpeOldCount + Convert.ToInt32(Config.OnceTakeCount))
{
//還沒執行完上次呼叫的兩百筆,什麼都不做
}
else
{
//
}
}
else
{
//已經到達總筆數
TPEFinCountLbl.Text = tpeNowCount.ToString();
exeTPEStatusLbl.Text = "已執行完畢!";
StopTpeTimer();
}
}
然後就是會在textbox裡面顯示每分鐘的平均執行速度:這部分很簡單應該不用多說。
//確認每分鐘臺北查詢速度
private void queryTPEAverageSpeed_Tick(object sender, EventArgs e)
{
using (rehouseEntities db = new rehouseEntities())
{
tpeNowMinuteCount = GetTpeFinCountNow();
string avgSpeedResult = DateTime.Now.ToString("hh:mm") + "完成筆數:" + Convert.ToString(tpeNowMinuteCount - tpeOldMinuteCount);
TPEAvgSpeedTxt.Text = TPEAvgSpeedTxt.Text + avgSpeedResult + "\r\n";
//每三分鐘檢查一次是否速度降為零
//是的話,就重送
if (DateTime.Now.Minute % 3 == 0 && tpeNowCount != tpeTotalCount)
{
if (tpeNowMinuteCount - tpeOldMinuteCount == 0)
{
tpeOldCount = tpeNowCount;
}
}
tpeOldMinuteCount = GetTpeFinCountNow();//記錄一分鐘前執行完畢的筆數
}
}
本篇大概就是這樣,只要遵守(上一批資料查詢完畢之後才去查詢下一批資料)的原則的話,就沒錯了
補充20151209:
如果不是在Winform的環境作(例如在console或是web mvc或是wcf或是webapi),timer要改成用System.Timers.Timer,幫timer設定委派事件的時候要改寫成this.tpeQueryTPETimer.Elapsed += new System.Timers.ElapsedEventHandler(tpeQueryTPETimer_Tick);就可以了。
補充20151210:
非winform的情況下,由於眼睛看不到是否還在執行,建議在iis設定應用程式集區的定期每天回收,以免timer永遠也不會停。並且設定一個資料庫變數(完整程式碼中的SLPVPSW的變數),避免正式環境(通常有多台機器做load balance)同時有多個instance同時執行。
補充關於長時間執行會自己掛掉的問題20151211:
本機的情況下,執行的時間是無限大的,但如果發行到正式機,如果是web類型的程式(例如:wcf, webapi,webform)來做大量發送的話,需要設定web.config如下:
Compilation的debug屬性必須為false, 而在httpRuntime的executionTimeout屬性設定你需要的時間長度。ps.在MVC的情況下,Timeout的設定不是這麼單純,在此就不多討論。
<configuration>
<system.web>
<!--這是以秒為單位的-->
<compilation debug="false" targetFramework="4.0" />
<httpRuntime executionTimeout="600"/>
</system.web>
</configuration>
並需要把iis的應用程式集設定閒置時間為你要的時間長度(建議新增另一個應用程式集區,不要讓其他程式跟這個長時間執行的程式來共用應用程式集):
並強烈建議設定這個應用程式集區每天回收,以防有什麼萬一你這個程式跑到天荒地老:
完整的程式碼:
WinForm:
http://saltsourcecenter.blogspot.tw/2015/11/webservicewebservice.html
WCF:(Console, web, 也都可用這個套用)
http://saltsourcecenter.blogspot.tw/2015/12/webservicewebserviceconsole.html
參考資料:
ASP.NET MVC 開發心得分享 (22):關於 executionTimeout - The Will Will Web
How to increase executionTimeout for a long-running query? - Stack overflow