2014年11月27日 星期四

Performance tunning on large records insert to database

.NET   Performance issue    Multi-thread

背景


某專案中為了QC寫的程式, 資料量從2萬以下/day 暴增為 2百萬/day 在做資料庫(On my notebook) insert時當然慘不忍睹。 最後透過利用以下方式將一天資料的insert時間從 200 min 調至約 30 min 可執行完畢。
l   利用Visual Studio內建的「效能及診斷工具」掃描程式的bottleneck
l   將資料切成多份以多條執行緒送到DBbatch insert


環境


l   Windows 7 pro
l   Intel i5 2.4GHZ 2 Cores and 4 logical threads
l   16G RAM


Performance tunning


效能及診斷工具

l   使用診斷程式效能結果,發現程式在String Concat的時間很長,因為串接的資料是動態產生的,所以改用StringBuilder解決。

l   DB新增資料的部分,也是花了很多時間的兇手。 原因是採用one connection 9,000~1,0000 筆資料做Commit. 後來決定2,000Commit一次, 並且用非同步的方式送出多個batch insert指令給資料庫。


程式調效 (After)



l   主程式

//每個檔案處理間格時間(毫秒),以避免同時開太多Thread造成Out of memory
private static int FILE_HANDLE_DELAY_TIME = 1000;
//只允許Private Memory的最大可使用量,如果超過此值,會讓工作先等待Memory降下來再繼續執行
private static int MAX_MEMORY_ALLOWED_MB = 800;
//Batch insert size,每個檔案資料筆數/此設定值 = 每個檔案開幾條Thread
private static int BATCH_INSERT_SIZE = 1000;

foreach (var filePath in System.IO.Directory.GetFileSystemEntries(this._tarDirPath))
{
       //Import data asynchronous
       this.doImport(filePath);
       fileIndex++;
       //Monitor memory and delay the import task if need
       this.taskDelayWithMemoryLimit();
}

l   ?筆送出一次batch insert

private void doImport(String filePath)
{
    StreamReader sr = new StreamReader(filePath);
    var ccnsBatch = new List<CsvCCN>();
    int batchSize = BATCH_INSERT_SIZE; //?commit一次
    int batchIndex = 0; //batch size counter
try{
       while (!sr.EndOfStream)
       {
            String str = sr.ReadLine();

            //Parse data to DAO from file stream
            CsvCCN ccn = null;
            this.parseToDao(filePath, str, out ccn);

            cnsBatch.Add(ccn);
            batchIndex++;
                   
            //Run async import task when the batch size reaches BATCH_INSERT_SIZE
            if (batchIndex.Equals(batchSize)) {
                  this.runTask(ccnsBatch);
                  ccnsBatch.Clear();
batchIndex = 0; //Reset the counter
            }
            else{
                  continue;
            }
      }

      //Run async import task with the data left
      if (ccnsBatch.Count > 0)
      {
          this.runTask(ccnsBatch);
      }
      ccnsBatch.Clear();
   }
   catch (Exception) {
      throw;
   }
   finally{
      //GC
      ccnsBatch = null;
      sr.Close();
      sr.Dispose();
      //System.GC.Collect();
    }
}


l   當記憶體超過使用限制時,立即暫停工作並停止產生新的batch insert thread.

private void taskDelayWithMemoryLimit()
{
    //Get the memory usage of the process
    Process proc = Process.GetCurrentProcess();
//Task delay
    double privateMemory = 0;
    double gcTotalMemory = 0;

    do{
      Task.Delay(FILE_HANDLE_DELAY_TIME).Wait();
      privateMemory = (double)proc.PrivateMemorySize64 / 1024 / 1024;
      gcTotalMemory = (double)GC.GetTotalMemory(true) / 1024 / 1024;
      Debug.WriteLine(String.Format(
         "{0} Process private memory usage : {1}MB , Garbage collector : {2}MB",
         DateTime.Now.ToString(), privateMemory, gcTotalMemory
       ));
    } while (gcTotalMemory > MAX_MEMORY_ALLOWED_MB);
}


l   產生一條新的Threadbatch insert
注意這邊要將外部的參數先deep clone一份以確保thread safe

private void runTask(List<CsvCCN> ccnsBatch)
{
    List<CsvCCN> ccnsBatchCopy = null;
using (var dp = new Domain.DeepCopy.DeepCopyObjList<CsvCCN>())
    {
        ccnsBatchCopy = dp.Clone(ccnsBatch);
    }
Task doTask = new Task(
       () => asyncImport(ccnsBatchCopy));

doTask.ContinueWith(x => {
     
      Console.WriteLine(String.Format("Import Thread {0} 結束。", doTask.Id));
      ccnsBatchCopy.Clear();
      ccnsBatchCopy = null;

    });

    Console.WriteLine(String.Format("Import Thread {0} 開始 ...", doTask.Id));
    doTask.Start();
}


執行紀錄

l   監看Memory Usage

2014/11/27 下午 07:28:35 Process private memory usage : 20.6875MB , Garbage collector : 0.804416656494141MB
2014/11/27 下午 07:28:36 Process private memory usage : 20.6875MB , Garbage collector : 0.804409027099609MB
2014/11/27 下午 07:28:36 Process private memory usage : 20.6875MB , Garbage collector : 0.804416656494141M
2014/11/27 下午 07:28:37 Process private memory usage : 20.6875MB , Garbage collector : 0.804416656494141MB

l   Running threads






Reference





2014年11月19日 星期三

[Entity Framework 6] Code First (1)

.NET   Entity Framework   Code First

背景

How to implement Code first in Entity framework.

環境

l   Windows 7 pro
l   IIS Express 7.5
l   Visual Studio 2012 (C#)
l   MS Sql Server 2008 R2
l   Entity Framework 5

步驟



l   設定Web.Config的資料庫Connection String
l   建立基底欄位的類別:BaseEntity
l   實作資料表 Customers,Reservations,Rooms 設定相關屬性(Pkey, Foreign Key …)
l   建立繼承DbContext資料庫Context : HotelDbContext
l   實作第一次建立資料庫之後的Initial Load程式碼



實作


Database connection

l   設定Web.Config的資料庫Connection String

<connectionStrings>
<
add name="HotelDbContext" 
connectionString="Data Source=localhost; Initial Catalog=RoomReserve; Integrated Security=True;
MultipleActiveResultSets=True
"
providerName="System.Data.SqlClient" />
</
connectionStrings>


以連到本機的Sql Express為例。
設定的連線名稱:HotelDbContext在步驟2.會使用到。




Model (DAO)

l   BaseEntity
建立基底欄位的類別:BaseEntity 以及設定Property的預設值
public class BaseEntity
{
        ///
 <summary>
        ///
 建立日期
        
/// </summary>
        public DateTime CreateOn { get; set; }
        ///
 <summary>
        ///
 
更新日期
        
/// </summary>
        public DateTime UpdateOn { get; set; }

        public
 BaseEntity()
        {

         
CreateOn = DateTime.Now;
           UpdateOn = 
DateTime.Now;
         }
}

l   Customer

public class Customer : BaseEntity
{
   [
Key]
   [
Column(Order = 1)]
   [
Display(Name = "身分字號")]
   
public String RegId { get; set; }

   [
Column(Order = 2)]
   [
Display(Name = "姓名")]
   
public String Name {get; set;}
   
   [
Column(Order = 3)]
   [
Display(Name = "電話號碼")]
   
public String Phone { get; set; }
   
   
public virtual  ICollection<Reservation> Reservations { get; set; }  //備註
}

備註: 表示一對多關係,一個Customer可以有多筆Reservation

l   Room

public class Room : BaseEntity
{
  [
Key]
  [
Required]
  [
Column(Order=1)]
  [
Display(Name="房號")]
  
public String RoomId { get; set; }

  [
Column(Order = 2)]
  [
Display(Name = "價格")]
  
public int Price { get; set; }

  
public virtual ICollection<Reservation> Reservations { get; set; } //備註
}

備註: 表示一對多關係,一個Room可以對應到多筆Reservation

l   Reservation

public class Reservation: BaseEntity
{
  
[Key] //當欄位型別為int且設定為主索引鍵,則EF會將其自動編號(流水號)
  
 [Column(Order = 1)]
   
public int Id { get; set; }

   [
Column(Order = 2)]
   [
Display(Name = "房號")]
   
public String RoomId { get; set; }
   
   [
Column(Order = 3)]
   [
Display(Name = "身分字號")]
   
public String RegId { get; set; }
  
   [
Column(Order = 4)]
   [
Display(Name = "人數")]
   
public int People { get; set; }
  
   [
Column(Order = 5)]
   [
Display(Name = "訂房時間")]
   
public DateTime ReservationOn { get; set; }
   
   [
Column(Order = 6)]
   [
Display(Name = "入住時間")]
   
public DateTime CheckInOn { get; set; }
  
  
//備註一
   [
ForeignKey("RoomId")]
   
public virtual Room Room { get; set; }
   
//備註二
   [
ForeignKey("RegId")]
   
public virtual Customer Customer { get; set; }
}

備註一:指定RoomIdForeign Key,且Reference Room對應的表格。
備註二:指定RegIdForeign Key,且Reference Customer對應的表格。

DbContext

l   建立繼承DbContext的資料庫Context : HotelDbContext
public class HotelDbContext : DbContext
{
public HotelDbContext()
             : 
base("name=HotelDbContext")  //指定對應的Connection String名稱 
    { }
    //宣告資料模型: Customers (Customer類別組成)
    public
 DbSet<Customer> Customers { getset; }
//宣告資料模型: Reservations (Reservation類別組成)
public DbSet<Reservation> Reservations { getset; }
//宣告資料模型: Rooms (Room物件組成)
public DbSet<Room> Rooms { getset; }

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    
try
    {
        ///
設定Customer 一對多 Reservations
        modelBuilder.Entity<
Customer>().HasMany(
m => m.Reservations).WithRequired(m => m.Customer);

       ///
設定Room 一對多 Reservations
       modelBuilder.Entity<
Room>().HasMany(
m => m.Reservations).WithRequired(m => m.Room);
    }
    
catch (Exception ex) { }
}
}

l   Initial Load

public class HotelDbInitial : 
   
CreateDatabaseIfNotExists<HotelDbContext>
{
        
protected override void Seed(HotelDbContext context)
        {

         
try
            
{
                context.Rooms.Add(
new Room() { RoomId = "100", Price = 10000 });
                context.Rooms.Add(
new Room() { RoomId = "101", Price = 9999 });
                context.Rooms.Add(
new Room() { RoomId = "102", Price = 9998 });

                 context.Customers.Add(
new Customer() { 
                    RegId=
"R22222222", 
                    Name = 
"JB", 
                    Phone = 
"09331111111" });
                 context.Customers.Add(
new Customer() { 
                     RegId=
"A22222222", 
                     Name = 
"Lily", 
                     Phone = 
"0911111111" });
                 context.SaveChanges();
            }
            
catch (Exception ex) { }
         }
}


然後在Global.asax 裡面的  Application_Start() 加入以下程式碼,即可在建立資料庫後做Initial Load

protected void Application_Start()
{
     //…
     //
註冊資料庫Initial
    
Database.SetInitializer(new HotelDbInitial());
}


備註

l   手動指定Table Name
如果我們要手動指定,只要在Model 加上以下attribute … 
[Table("MyRoom")]
public class Room : BaseEntity
{ … }

資料庫裡面的表格名稱就會建立成 MyRoom
建議同時一起修改資料庫Contex裡面的資料模型名稱:

//宣告資料模型: MyRoom (Room物件組成)
public DbSet<Room> Rooms { get; set; }     原來是設定成預設名稱 Rooms
public DbSet<Room> MyRoom { get; set; } 

這樣在程式裡面操作資料模型時,才不會因為名稱和資料庫表格名稱不同,而造成混淆。


l   宣告Property 在資料庫中為Identity欄位

[Key]
[Column(Order = 1)]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }

如果是Int (int , Int32 …)欄位且指定為Primary key,則EF會自動將其設置為Identity

l   指定Model欄位在資料庫的型態

[Required]
[Column(TypeName = "NVARCHAR")]
[MaxLength(100)]
public String Name { get; set; }


l   EF Code First中定義資料模型的欄位常用Attribute



延伸閱讀