作者: paul

上傳大檔到Azure Blob進行SQL的Bulk Insert

上傳大檔到Azure Blob進行SQL的Bulk Insert

Download AzCopy and Unzip to C:(for Example)

下載網址:https://docs.microsoft.com/zh-tw/azure/storage/common/storage-use-azcopy-v10

注:Blob要先開啟Share的簽章設定:Shared access signature

azcopy make "https://xxxxxxx.blob.core.windows.net/dbfile?sv=2020-02-10&ss=bfqt&srt=sco&sp=rwdlacupx&se=2021-04-26T23:26:15Z&st=2021-04-26T15:26:15Z&spr=https&sig=1asdfasdbasdfasdbqasdfasdfasdf%3D"

azcopy.exe copy "C:\test.csv" "https://xxxxxxx.blob.core.windows.net/dbfile?sv=2020-02-10&ss=bfqt&srt=sco&sp=rwdlacupx&se=2021-04-26T23:26:15Z&st=2021-04-26T15:26:15Z&spr=https&sig=1asdfasdbasdfasdbqasdfasdfasdf%3D"

In Query Editor(Sql Azure)

--資料庫密碼
CREATE MASTER KEY ENCRYPTION BY PASSWORD = '!asdkfajd;skfjasldjkfalksdjflasd@' ;

--建立權杖(use Blob SAS Token)
CREATE DATABASE SCOPED CREDENTIAL AccessAzureSecret
WITH
  IDENTITY = 'SHARED ACCESS SIGNATURE',
  -- Remove ? from the beginning of the SAS token
  SECRET = 'sv=2020-02-10&ss=bfqt&srt=sco&sp=rwdlacupx&se=2021-04-26T23:44:32Z&st=2021-04-26T15:44:32Z&spr=https&sig=1asdfasdbasdfasdbqasdfasdfasdf%3D' ;
CREATE EXTERNAL DATA SOURCE dbFile
WITH ( TYPE = BLOB_STORAGE,
LOCATION = 'https://xxxxxxx.blob.core.windows.net/dbfile'
 , CREDENTIAL= AccessAzureSecret)


BULK INSERT [dbo].[learning_records] FROM 'stage_learning_record.csv' WITH (
    DATA_SOURCE = 'dbFile',
    FIELDTERMINATOR=',',rowterminator = '\n', KEEPNULLS);

註:
csv裡面,欄位是NULL的話,要先把欄位NULL取代成空字串(不是””), 若日期長度有問題的話,就也要先取代掉

若bulk insert執行發生超過10次錯誤,就會被中斷哦

從Linux主機 使用SCP +SSH Copy檔案的相關指令

從Linux主機 使用SCP +SSH Copy檔案的相關指令

1.先將產生一個本地的ssh publickey/privatekey

#產生SSH key
ssh-keygen
#類型可以輸入rsa, 稍後就會產生id_rsa
cat id_rsa.pub
#印出id_rsa.pub內容
#複製到文字檔,再上傳到對方主機

#遠端到對方主機
#將公key append到ssh允許清單
cat id_rsa.pub >> .ssh/authorized_keys

2.本地檔案複製到對方主機

#遠端到對方主機

SCP -i ~/.ssh/id_rsa /本地檔案 username@hostname:/遠端檔案

#要注意-i指定的檔案id_rsa是私key,不是id_rsa.pub公key

就可以通了!

使用Rider Debug Azure Function專案遇到的怪異問題,ex. 找不到System.IO.Pipelines 5.0.0, 無法解析FileLoggingStatusManager

使用Rider Debug Azure Function專案遇到的怪異問題,ex. 找不到System.IO.Pipelines 5.0.0, 無法解析FileLoggingStatusManager

為什麼要特別記錄這次的案例,主要Azure Functions若順順著用,不要想什麼DI,也是挺單純,就是一個Function,像是Console一樣跑進去嘛

但是為了實踐DI設計,當然主要就是為了單元測試,把相依物件隔離,本來也沒事,UT寫的好好的,整測去Trigger容器註冊也解決了,結果實際拿Azure Function專案Debug時,

除了AzureFunction剛好是Static Class,在想有哪些切入點可以註冊事件,然後不明究理的去開始debug他,結果發生了以下諸多問題

 

a.Azure 儲存體相依

b.DI容器註冊問題

c.Runtime在跑的時候其他元件相依問題

 

在此記錄一下解法!

1.Rider要先安裝azure functions core tools

image.png

Debug時,若是出現無法連到儲存體的話,若只是開啟emulator的話,也是不行

The listener for function ‘ActivateUserJob’ was unable to start. Microsoft.Azure.Storage.Common: 無法連線,因為目標電腦拒絕連線。. System.Net.Http: 無法連線,因為目標電腦拒絕連線。. System.Private.CoreLib: 無法連線,因為目標電腦拒絕連線。.

https://docs.microsoft.com/zh-tw/azure/storage/common/storage-use-emulator#how-the-storage-emulator-works
image.png

會出現 Server encountered an internal error
image.png

2.azurite將取代Azure Storage Emulator

https://docs.microsoft.com/zh-tw/azure/storage/common/storage-use-azurite#run-azurite-from-a-command-line

  • 使用 NPM 安裝和執行 Azurite
    npm install -g azurite
  • 啟用windows的azurite
    **azurite --silent --location c:\azurite --debug c:\azurite\debug.log**
  • 跑起來azurite的樣子:
    image.png

總算可以了!

3.透過DI註冊到Azure Functions

class可以註冊覆寫這個Function

public override void Configure(IFunctionsHostBuilder builder)
public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
在覆寫的時候,可以去將ServiceProvider重新指定

var configBuilder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("local.settings.json", true, true)
                .AddEnvironmentVariables();

            var builderTemp = new FunctionsHostBuilder();
            builderTemp.Services = new ServiceCollection();

            builder = builderTemp;

            var configurationRoot = configBuilder.Build();
            builder.Services.AddSingleton<IConfiguration>(provider =>
            {
                return configurationRoot;
            });


怪異問題集

1.找不到System.IO.Pipelines 5.0.0版(nuget明明有裝)
image.png
解法:
https://stackoverflow.com/questions/65382540/azure-function-app-could-not-load-system-io-pipelines-connecting-to-redis

 

2.無法解析FileLoggingStatusManager

可以想像是AzureFunction在初始化注入DI的時候無法找到相對應的類別
但我看其他專案不會有這種錯,而且我在整合測試專案時,已經塞過也可作了

但一到AzureFunction跑起來的時候就怪怪的,猜想是跟容器本身有關
因此我在Startup的時候,重新初始化ServiceCollection塞進去。
但是若要塞新的ServiceCollection,就必須實作把setter改掉!

實作IFunctionHostBuilder
image.png

Replace掉該介面的實作
image.png

SOLID重點複習-2.LSP Liskov替換原則

SOLID重點複習-2.LSP Liskov替換原則

最早由Barbara Liskov 由1988年提出:

子型態(subtype)必須要能夠替換它們的基底型態

 

最簡單的範例是:

就是在違反LSP的時候,常常也明顯違反OCP,就是在程式執行階段時,進行型別檢查,如以下

using System;
					
public class Program
{
	public struct Point
	{
		public double x;
		public double y;
	}
	
	public class Shape
	{
		public Shape(string shapeName)
		{
			ShapeName = shapeName;
		}
		
		public string ShapeName;
		
		public static void DrawShape(Shape t)
		{
			if (t.ShapeName == "circle")
			{
				((Circle)t).Draw();
			}
		    if (t.ShapeName == "square")
			{
				(t as Square).Draw();
			}
		}
	}
	
	
	
	public class Circle : Shape
	{
		public Point center;
		public double radius;
		
		public Circle(string typeName):base(typeName)
		{
		}
		
		public void Draw()
		{
			Console.WriteLine("this is circle");
		}
	}
	
	public class Square : Shape
	{
		public Point TopLeft;
		public double side;
		public Square(string typeName):base(typeName)
		{
		}
		
		public void Draw()
		{
			Console.WriteLine("this is square");
		}
	}
	
	
	
	public static void Main()
	{
		//註:這邊更好是用列舉的方式指定Name, 強制約定
		Shape a = new Circle("circle");
		Shape b = new Square("square");
		
		Shape.DrawShape(a);
		Shape.DrawShape(b);
	}
}

很顯然,DrawShape違反了OCP,因為每擴充一種shape的時候,這個函式就要一併作出修正

書中提及很多人肯定這種作法是很糟糕設計,但為什麼會促使程式設計師寫出這種函式呢?

主要可能是對於多型的額外開銷(overhead)認為大得難以忍受,他沒有在shape類別中定義抽象方法,以便於抽象draw行為。

然而在現今電腦發展飛速的時代中,這種多型方法呼叫的開銷都是ns等級。其實不必要對此有過度的觀點

此例中,square類別和circle類別無法替換shape類別,就是違反了LSP, 同時又迫使DrawShape違反了OCP,因此LSP的違反也潛在地違反了OCP

 

關鍵的問題:

考慮以下程式

using System;
					
public class Program
{
	public class Rectangle
	{
		protected double width;
		protected double height;
		public virtual double Height
		{
			get
			{
				return height;
			}
			
			set
			{
				height = value;
			}
		}
	
		public virtual double Width
		{
			get
			{
				return width;
			}
			
			set
			{
				width = value;
			}
		}
		
		public double Area()
		{
			return width*height;
		}
	}
	
	public class Square :Rectangle
	{
		public override double Height
		{
			get
			{
				return base.height;
			}
			
			set
			{
				base.width = value;
				base.height = value;
			}
		}
	
		public override double Width
		{
			get
			{
				return base.width;
			}
			
			set
			{
				base.height = value;
				base.width = value;
			}
		}
	}
	
	
	public static void Main()
	{
		Rectangle r = new Square();
		r.Width = 5; 
		r.Height = 4;
		//對正方向設定5再設定4的邊界,行為意圖比較像是拉動邊的長短而己
		Console.WriteLine("square area is "+r.Area());
		
		
	}
}

雖然square跟rectangle自己都可以正常運作,這種我們稱為子類別的自相容(self-consistent)及結果是正確的,可是這個結論是錯的

跑出來的結果:

square area is 16

但一個自相容的程式未必和他所有的使用端程式相容,考慮以下例子:

Rectangle r = new Square();
    var result = CalculateArea(r);

	public static double CalculateArea(Rectangle r)
	{
		r.Width = 4;
		r.Height = 5;
		
		if (r.Area() != 20)
			throw new Exception("bad area");
		
		return r.Area();
	}

其結果反而是:

Run-time exception (line 89): bad area

 

因此關鍵的問題是,撰寫這個CalculateArea的程式,並不知道其傳進來的類別,改變長的時候會導致寬也被改變掉了

因此此函式作的假設,確因為傳入了square,導致發生了錯誤。因此square與rectangle之間的關係是違反LSP的

 

書中認為或許有人會針對這個函式所存在的問題進行爭論,認為此函式的作者不能假設長寬一定是獨立的。

但問題是此函式的作者不會同意這個論點,因為此命名以rectangle為命名作為參數,確實有一些不變性與真理足以說明。

而Rectangle其中一個不變的性質就是長寬可以獨立。此函式的作者可以斷言這個不變性(檢查)。

反而是square的作者違反了不變性。

而有 趣的是他不是違反square的不變性,而是違反了rectangle的不變性

此例告訴我們

一個模型,如果獨立來看,並不具備真正意義上的有效性,有效性只能以客戶端的程式來表現

解答LSP方針

1.OOD指出Is-A關係是從”行為”來判斷

2.基於契約的設計(DBC),在單元測試時指定契約,後面有具體例子時再討論

簡單摘要至此,足以深化LSP的觀點了,先這樣吧~

 

參考書摘-無瑕的程式碼-敏捷完整篇 物件導向原則、設計模式與C#實踐

SOLID重點複習-1.OCP開放-封閉法則

SOLID重點複習-1.OCP開放-封閉法則

提出者:Betrand Meyer在19888年提出

OCP(The Open-Closed Principle):開放-封閉原則

如果程式的區塊一旦變動,就會產生一連串的反應,導致相關使用到的模組都必須更動,那麼這個程式碼就具備bad smell

OCP建議我們應該進行重構

 

如果OCP原則應用正確的話,那麼,以後再進行”同樣”的需求變動時,就只需要增加新的程式碼,而不用再調整”已經”正常運行的程式碼

其特徵為2:

1.對於擴展是開放的(open for extension)

2.對修改是封閉的(closed for modification)

如何可以「不變動模組原始碼但改變行為呢?」,OOP讓他能做到的機制就是抽象(Abstract)

建立可以固定能”描述”可能行為的抽象體作為抽象基底類別

 

模組針對抽象體進行操作。透過抽象體的衍生,就可以擴展此模組的行為

不遵循OCP的簡單設計的程式如下,Client直接使用了ServerImplementA的類別方法。

using System;
					
public class Program
{
	public class Client
	{
		public void run()
		{
			var server = new ServerImplementA();
			server.Start();
		}
	}
	
	public class ServerImplementA
	{
		public ServerImplementA()
		{
			
		}
		public void Start()
		{
			Console.Write("ServerImplement A Running");
		}
	}
	
	
	public static void Main()
	{
		new Client().run();
	}
}

註:這種直接透過實作達成目的,雖然沒有滿足ocp的要求,不是oop的設計原則,但是並不一定是不好的哦。

因為能解決問題的程式就是好程式,若這樣的程式需求變動可能性低或目的性明確的話,或許這種程序式的程式設計風格(Procedural programming Style)反而意圖更明確好懂呢。

此例中:Client使用了Implement A的類別,這是一種既不開放又不封閉的Client

 

若遵循OCP來做設計,使用策略模式(Strategy)可以重構他

using System;
					
public class Program
{
	public class Client
	{
		public void run(string type)
		{
			ClientInterface client = null;
			if (type=="A")
			  client = new ServerImplementA();
			if (type=="B")
			  client = new ServerImplementB();
			
			if (client!=null)
				client.Start();
		}
	}
	
	
	interface ClientInterface
	{
		void Start();
	}
	
	
	public class ServerImplementA:ClientInterface
	{
		public ServerImplementA()
		{
		}
		
		public void Start()
		{
			Console.WriteLine("ServerImplement A Running");
		}
	}
	
	public class ServerImplementB:ClientInterface
	{
		public ServerImplementB()
		{
		}
		
		public void Start()
		{
			Console.WriteLine("ServerImplement B Running");
		}
	}
	
	
	public static void Main()
	{
		Client client = new Client();
		client.run("A");
		client.run("B");
	}
}

我們透過封裝Client的固定行為”Run”,拉到抽象體(策略模式的抽象載體為interface)

註:書中的案例發生了Client若使用了Server的程式時,抽象體為什麼要叫ClientInterface呢?

作者提到:抽象類別和它們的客戶的關係要比實作它們的類別關係更密切一點

 

那麼,當我們要擴充B的實作的時候,只需要針對B去”增加”實作,然後Client的使用端,不用再去改到使用行為,當然策略模式使用interface來解決抽象體

那麼簽章異動時(例如方法,參數),那麼仍然是會面對到策略模式帶來的副作用!

 

另一種樣版方法模式(Template Method)

using System;
					
public class Program
{
	public class Client
	{
		public void run(string type)
		{
			ServerInterface client = null;
			//封閉的地方
			if (type=="A")
			  client = new ServerImplementA();
			//開放的地方
			if (type=="B")
			  client = new ServerImplementB();
		}
	}
	
	
	public abstract class ServerInterface
	{
		public ServerInterface()
		{
			this.Start();
		}
		
		public abstract void Start();
	}
	
	//不變的地方
	public class ServerImplementA:ServerInterface
	{
		public ServerImplementA()
		{
		}
		
		public override void Start()
		{
			Console.WriteLine("ServerImplement A Running");
		}
	}
	
	//開放的地方
	public class ServerImplementB:ServerInterface
	{
		public ServerImplementB()
		{
		}
		
		public override void Start()
		{
			Console.WriteLine("ServerImplement B Running");
		}
	}
	
	
	public static void Main()
	{
		//不變的地方
		Client client = new Client();
		
		//開放的地方
		client.run("A");
		client.run("B");
	}
}

透過抽象類別的實作,其實針對使用端來說,差不多差不多,都是為了抽象化,讓Client使用上足以一致性

以我抽象類別實作的好處是你可以將部分共同的實作封裝在抽象類別中!讓重覆的行為更被集中!以上面的例子,就是將Start的 行為抽到Constructor去

而抽象體使用介面或是抽象類別來達成哪個好?以結果論則是視使用上是否足夠抽象來決定

 

這兩個模式是滿足OCP最常用的方法

 

參考書摘-無瑕的程式碼-敏捷完整篇 物件導向原則、設計模式與C#實踐

 

.net throw ex 與 throw 的不同之處(重新理解)

.net throw ex 與 throw 的不同之處(重新理解)

前幾天保哥在.net社群提示一個人的詢問 throw ex跟throw差在哪,他寫了類似以下範例的程式,但沒有明說差別
仔細想想,我似乎也沒特別想過這個問題
以往例外處理的議題就是不要不處理就往最外面丟,或是不要都不做事把錯誤隱藏
但往外丟本身,我習慣確實是throw,而不是throw一個變數,通常我要丟變數的時候,要不就是重新包成有意義的new exception,例如把function的意義多做描述。
否則就是不會new一個似是而非的錯誤”訊息”。
實際查了一下,stackflow網站上有人提出來:
throw ex resets the stack trace (so your errors would appear to originate from HandleException)
throw doesn’t – the original offender would be preserved.
結果我實際寫了一個winform看了一下差別,一開始我還把這兩個例子都直接寫在”同”一個function裡,結果想說,沒有不見呀…
結果原來是在同一個function裡面看不出來,因為不算是跨stack

以下是看的出來的例子..

 //for test args = new[] {"appreset"};
            try
            {
                try
                {
                    test1();
                }
                catch (Exception b)
                {
                    throw b;
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }


            try
            {
                try
                {
                    test2();
                }
                catch (Exception ex)
                {
                    throw;
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }



   private static void test2()
        {
            try
            {
                throw new Exception("OK", new Exception("b"));
            }
            catch (Exception)
            {
                throw;
            }
        }

        private static void test1()
        {
            try
            {
                throw new Exception("OK", new Exception("b"));
            }
            catch (Exception a)
            {
                throw a;
            }
        }

定期搬移目錄到指定日期目錄Bat範例

定期搬移目錄到指定日期目錄Bat範例

實驗worked!!

記錄一下

set sourcePath=D:\A
set targetPath=D:\B

for /f "skip=1 delims=" %%x in ('wmic os get localdatetime') do if not defined X set X=%%x
set yyyyMMdd=%X:~0,8%

mkdir D:\B\%yyyyMMdd%

xcopy /F /Q /E /S %sourcePath% %targetPath%\%yyyyMMdd%

del /F /q "D:\A\*.*"
FOR /D %%p IN ("%sourcePath%\*.*") DO rmdir "%%p" /s /q

SQL Server 誤刪資料庫的資料怎麼辦

SQL Server 誤刪資料庫的資料怎麼辦

今天同事忘了下where條件

不小心誤更新了四千多筆的資料表

一時間協助查詢解決方法,畢竟不是dba

操作實務不足,也不敢馬上給指令

深怕把資料庫搞壞,連現行資料都弄不回來

 

事後,經過比較各個blog,MSDN,還有跟同事的討論後,實證驗正過以下做法可行,足以做為SOP

也同時花了點時間理解原理

 

發現原來我認知的誤區就是我以為資料庫靠transaction log就可以rollback回去過去時間點(減法)

但其實關鍵點反而是用加法在還原時間點加回來交易紀錄,見上圖所示,我寫在黑板記錄一下

看參考部落格時還想說為啥要先restore 資料..後來才想通

因此語法真的誤刪(人人似乎都有這個黑歷史)的話,別緊張,Follow以下SOP,應該可以救的回來,前提是資料庫是在完整模式下

 

範例資料:

USE [DBName]
GO
/****** Object:  Table [dbo].[Test]    Script Date: 2020/4/28 上午 08:54:31 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Test](
	[a] [nvarchar](50) NULL,
	[b] [nvarchar](50) NULL
) ON [PRIMARY]
GO
INSERT [dbo].[Test] ([a], [b]) VALUES (N'1', N'2')
GO
INSERT [dbo].[Test] ([a], [b]) VALUES (N'4', N'3')
GO
INSERT [dbo].[Test] ([a], [b]) VALUES (N'5', N'6')
GO
INSERT [dbo].[Test] ([a], [b]) VALUES (N'8', N'7')
GO
INSERT [dbo].[Test] ([a], [b]) VALUES (N'9', N'10')
GO
INSERT [dbo].[Test] ([a], [b]) VALUES (N'11', N'12')
GO

原本的資料

不小心誤刪了或漏下了where條件

 

 

 

 

 

 

這個時候,不要緊張,先找到上一次完整備份的時間點

以我的範例是8:55分

Step 0.網站停機或下線 (注意,千萬不能第一時間又做了一次完整備份)

避免寫入更多資料,複原時會遺失更多資料

Step1.切換單人模式並重新離線上線(確保資料庫是無人連線)

ALTER DATABASE DBName
SET SINGLE_USER
WITH ROLLBACK IMMEDIATE;

 

Step 1.進行完整的交易記錄備份 (ex.9:00分當下)

這時,你的DB應該會是在還原模式下:

 

Step2.還原上一次的資料備份檔案(記得要比欲複原的時間點更早,ex:AM 08:55那一版本的Bak)

注意:還原計畫只能包含資料,不能勾選到剛剛的交易記錄

還原完整備份的bak時,復原狀態要選擇Restore With No Recovery

 

 

Step3.還原交易記錄,並指定還原時間點(ex:8:56分)

 

還原成功後,再進行將資料庫切回多人模式

ALTER DATABASE DBName
SET Multi_User

檢查資料,已經回復了,可喜可賀

 

 

想法有誤的話,歡迎大家回饋指教

 

 

參考blog:

https://kknews.cc/zh-tw/code/q4lj5x8.html

https://blog.miniasp.com/post/2010/04/21/SQL-Server-Full-Differential-Transaction-Backup

C# 工作平行程式庫的例外處理機制

C# 工作平行程式庫的例外處理機制

最近2個月又重回C#懷抱了,昨晚追查一個底層ThreadPool Exception,然後會造成w3wp的Exception, 會造成集區重啟的異常

後來發現有一個非同步呼叫射後不理的部分,並沒有合理的包裝例外,然後又會造成.net framework底層無法Handle的例外

Legacy Code的用法是改一半的非同步呼叫方法

function method1()
{
       method2();
}

async Task<ResultObj> function method2()
{
       //略
}

後來查詢多方說法後(StackOverFlow網友建議),建議這種射後不理的整合寫法,可以採用TPL(工作型非同步執行庫)

正規TPL的非同步方法在Handle的時候如下:

其例外處理範例,因為Task的例外會拋出來的是AggregateException方法。因此可以例用以下方法來走訪所有的Exception

   public static void Main()
   {
      var task1 = Task.Run( () => { throw new CustomException("This exception is expected!"); } );

      try
      {
          task1.Wait();
      }
      catch (AggregateException ae)
      {
          foreach (var e in ae.InnerExceptions) {
              // Handle the custom exception.
              if (e is CustomException) {
                  Console.WriteLine(e.Message);
              }
              // Rethrow any other exception.
              else {
                  throw;
              }
          }
      }
   }

 

後續改寫一版後,能明確抓到射後不理的Exception,也不會造成集區崩潰就變成以下的版本了

Task listenerTask = Task.Run(() => proxy.callService<ResultObject>(inputObj)).ContinueWith( 
	t => { logException(t, proxy.ServiceName); }, TaskContinuationOptions.OnlyOnFaulted);

private static void logException(Task<BaseAddOutput> t, string serviceName)
{
	var message = string.Empty;
    if (t.Exception != null)
       message = t.Exception.Message;
    if (t.Exception.InnerException != null)
       message = t.Exception.InnerException.Message;
    Debug.Log(ConfigUtil.GetBoolean("Debug"), "system call " + serviceName + " error, msg:" + message);
}

 

當然,若不是要射後不理的話,建議還是要讓function async / wait化,來確保執行流程順序中的工作都有被完成

 

MS Docs參考

https://docs.microsoft.com/zh-tw/dotnet/standard/parallel-programming/task-based-asynchronous-programming

關於實作秒殺壓力測試的架構驗證

關於實作秒殺壓力測試的架構驗證

之前為了一個案子,實作了一個POC秒殺的情境,透過python、nginx、lucust、docker來模擬,但那時候必竟是架構新手,對於POC驗證的環節

只想的到以下的方式,以下提供參考…若有盲點或不足之處,歡迎指導…

 

一  測試內容

二  測試方法

三  測試方法

四  測試目標

五  測試環境

5-1 壓力測試部署圖

5-2 壓測用戶端與服務端主機規格

六  系統部署

6-1 系統部署架構圖

6-2相關系統容器配置表

七  性能測試結果與分析

7-1 測試步驟與結果摘要

7-2 劇情1 線上用戶30,000,開放10,000個號碼牌

7-3 劇情2線上用戶30,000,開放30,000個號碼牌

7-4 結果與發現

 

 

一  測試內容

本次測試是針對搶票系統進行壓力測試,在秒殺產品的場景中,需求遠遠大於供給,因此考慮巨大流量的情況下,我們在設計交易的接口中,加入了號碼牌機制,從入場、選位到結帳三關卡皆透過號碼牌的控制來監控與管理入場選擇產品的人流。並逐步縮小對於後端API的請求,達到集中運算資源與降低關鍵接口負載壓力的目的。這次我們主要驗證的功能包含了領取號碼牌選位結帳的功能。

 

二  測試方法

本次採用了python開源的分散式壓力測試工具Locust.io,這個工具的特性是可以快速佈署壓測用戶端的集群,並透過統一的Master發動壓力測試,並具備Dashboard,提供效能統計表與分析圖型,近期Amazon Web Service廠商已經善用其可分散式佈署的特性,透過259台用戶端,將壓力測試提升到每秒超過10萬次的後端請求的等級,請參考相關AWS官方連結

 

這次測試採用容器技術動態建立Locust測試集群,並透過Http協議發出GET請求,伺服器端主要透過Nginx+Tornado建立Load Balance與非阻塞式的Web Server,並結合容器技術動態擴充WebServer群,包含主要關鍵流程API,包含取得號碼牌請求、模擬選位請求、模擬結帳請求。關鍵流程之間,透過Redis快取技術協調並模擬用戶狀態(包含入場前、入場選位前、選位後、結帳前),最終透過MongoDB儲存持久化交易結果。

 

三  測試場景

測試場景包含了1取得號碼牌API,其主要目的是第一關控制流量,但也因為要面對第一線最多的請求流量,因此需要的資源最多。其2為模擬選位API,因應用戶取得號碼牌後的狀態變動,我們透過快取保存用戶狀態,以模擬用戶端行為。用戶在具備號碼牌的狀態下,呼叫模擬選位API,會有自動配位的邏輯處理,亂數決定客戶購買1~4張的張數,並動態查詢仍有空位的連續座位區域並隨機決定。此時會透過鎖定機制,保存在快取內,並非同步創建訂單到NoSQL的儲存體中,其狀態為未付款(Pay Status=1),並將此用戶加入到待付款的佇列中。最終為模擬結帳API,此API將會隨機取得佇列中的排隊付款用戶,並同步更新NoSQL的付款狀態(Pay Status=2),此時會標記其用戶已付款完成。

本次測試樣本將模擬銷售單一場次、6區域,依2回合進行測試,分別開放token限制為1萬、3萬,分別模擬請求流量的變化。

四  測試目標

測試獲取單一物理主機的伺服器在反應時間3秒內,其單位承載量RPS為多少,模擬併發用戶為多少。

 

採用號碼牌分流搶票流程下,在上述系統效能需求下,最終全數票券銷售完成到結帳狀態(模擬完成金流後,寫回訂單資料押付款完成的狀態)需要多久的時間。

 

五 測試環境

  5-1 壓力測試部署圖

  5-2 壓測用戶端與服務端主機規格

環境 機器型號 作業系統 硬體cpu 硬體mem
用戶端 Linux虛擬機 CentOS 7 4核 8G
服務端 Linux虛擬機 CentOS 7 4核 8G

 

六  系統部署

6-1 系統部署架構圖

6-2相關系統容器配置表

容器環境 佈署應用程式 數量# 備註
用戶端主機 Locust Master 1 統一發動測試端與測試結果Dashboard
Locust Slave 10 接受發動測試指令,執行測試腳本
服務端主機 Nginx 3 用於3個關鍵流程的Reverse Proxy分流關卡
Tornado(Token API) 5 主要面對最大流量領取號碼牌請求
Tornado(Reserve API) 3 面對已過濾擁有號碼牌的用戶進行選位模擬
Tornado(Admin API) 1 初始化資料預熱

Dashboard監控資訊

開放TokenLimit接口

MongoDb(NoSQL) 1 主要儲存最終訂單持久化與結帳後模擬金流回填狀態。
Redis Cache Server 1 主要處理關鍵流程間狀態控管

 

 

七  性能測試結果與分析

7-1 測試步驟與結果摘要

步驟一.Dashboard 初始化

步驟二.Locust配置與啟動

步驟三.抽樣觀測Server狀態與銷售狀態

步驟四 銷售完畢後,並待所有結帳佇列消耗完成後,搜集Client端數據

 

7-2 劇情1  模擬線上30,000用戶,開放10,000個號碼牌,銷售總票數為10,000張

7-2-1 Locust壓測用戶端統計資訊

開放10,000個號碼牌,銷售總票數為10,000張
模擬線上用戶數 30,000
用戶平均爬升數/秒 2,000
銷售總花費時數 1分40秒
有效訂單數(已付款,已進入數據庫) 3,971
總請求數(擷至全售完) 117,678
總平均RPS(Request per Second) 1,176.8
取號碼牌成功請求總數(HttpStatus=200) 32,268
模擬選位成功請求總數(HttpStatus=200) 10,903
模擬結帳成功請求總數(HttpStatus=200) 7,610
服務器狀態 全數存活

 

 

 

7-2-2 Locust壓測期間趨勢圖

7-2-3容器正常服務

 

7-3 劇情2  模擬線上30,000用戶,開放20,000個號碼牌,銷售總票數為10,000張

7-3-1 Locust壓測用戶端統計資訊

開放20,000個號碼牌,銷售總票數為10,000張
模擬用戶數 30,000
用戶平均爬升數/秒 2,000
銷售總花費時數 2分17秒
有效訂單數(已付款,已進入數據庫) 3,962
總請求數(擷至全售完) 156,029
總平均RPS(Request per Second) 1,138.9
取號碼牌成功請求總數(HttpStatus=200) 36,606
模擬選位成功請求總數(HttpStatus=200) 14,776
模擬結帳成功請求總數(HttpStatus=200) 6,984
服務器狀態 全數存活

 

 

7-2-2 Locust壓測期間趨勢圖

7-2-3容器正常服務

 

7-4 結果與發現

從相同的壓測用戶參數,並透過號碼牌來控制流程API的流量,我們發現號碼牌發放的控制,可以有效提升服務效率,銷售的時間更短。同時,這次銷售10,000張票在我們配置的架構下(Nginx與Tornado),單台主機可以支撐爆量請求而不會造成應用程式崩潰與異常銷售,仍然可以將其票券全數售完,並正常寫入資料庫。而本次透過容器建置所有的系統與API,說明未來管理上雲或是自建機房(IDC)可以在即短的時間內佈署。同時具備易用與易於擴充的特性。而切分微服務的概念,除了在系統佈署的配置上可以更彈性的運用,且未來更有機會隨時提升到Serverless的層次,透過雲端全託管的服務,可以有效降低維運成本。

 

WP Facebook Auto Publish Powered By : XYZScripts.com