[WebService]最佳化呼叫WebService的頻率

  • 2264
  • 0
  • 2015-12-11

最佳化呼叫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