一、背景
目前考慮到產品基於歷史原因,采用的接口webservice+webapi混合,webservice接口涉及產品winform端的業務,尤為重要,經常性出現winform端的一些性能問題,極為苦惱,想在接口端做一些性能監控以輔助分析。
二、思路
產品為單體架構,非平台級,目前能想到的方案就是結合webservice提供的擴展點來做請求攔截處理,分析記錄當前調用接口在調用時入參,調用時間點,耗時,調用來自的winform端等信息,記錄到客戶本地庫,再定時將統計信息上傳到公司服務器,過程中需考慮到寫本地庫對客戶本地庫造成的影響,已經上傳公司服務器帶來的並發。
1. 對於寫入本地庫,默認產品不寫入監控數據到本地庫即不啟用性能監控,只有當需要進行監控時才通過公司平台進行啟用,當然這里涉及到的前提是要收集到客戶的產品外網地址,結合產品提供的接口來進行啟用。
2. 考慮到接口數量目前6百多,可能要對接口涉及的業務類型進行划分,讓自己可以單獨啟用關注的業務模塊接口,或者啟動監控所有接口。
3. 數據寫入考慮到數據量對本地數據庫的沖擊,第一是寫入性能數據給接口帶來的損耗 第二 不能 影響接口的調用即考慮監控功能的異常處理 第三 數據量的增長,可考慮定時清理,保留1周的數據
4. 統計數據的上傳到公司服務器,考慮當天上傳前一天的性能統計數據以減小數據量,考慮到客戶體量,上傳時間點盡量分散且控制的客戶業務較閑時段,服務器接收考慮峰值,可采用MQ來削峰以保證服務器平穩。
三、 實現
webservice 提供的擴展點SoapExtension ,有兩種攔截方式,1種是擴展后將擴展點寫入web.config文件,但這樣的自由度不強,他會攔截所有接口,不法達到只攔截部分業務接口的目標。要達到目標就需要采用第2種方式 結合SoapExtensionAttribute 來對接口打上標簽並同時對接口分類,不廢話,上代碼:
1 public class TraceAttribute : SoapExtensionAttribute 2 { 3 TraceLevel _level = TraceLevel.NONE; 4 public TraceLevel TraceLevel// 業務類別 5 { 6 get { return _level; } 7 set { _level = value; } 8 } 9 10 11 int _priority = 1; 12 13 public override int Priority 14 { 15 get 16 { 17 return _priority; 18 } 19 set 20 { 21 _priority = value; 22 } 23 } 24 25 public override Type ExtensionType 26 { 27 get { return typeof(WSMonitorExtension); }//定義擴展soap的類型 28 } 29 }
針對TraceLevel 這里的一個小技巧就是,將其枚舉值按2的指數級賦值,這樣在接口上打標簽時可以多個業務標簽同時打,比如: TraceLevel.A | TraceLevel.B
假設 A業務類型枚舉值 2的1次方 即 00000010
B業務類型枚舉值 2的2次方 即 00000100
則 A | B = 00000010
00000100
即 00000110
此時在SoapExtension 的擴展中判斷是否需要記錄監控數據時, 就可以根據設置的監控業務類型來進行 "與" 運算來判斷是否需要記錄,比如: 設置了監控A 級別業務接口即
00000110 & 00000010 == 00000010 可以看出需要記錄監控數據,此時就把數據異步寫入本地庫中。
還要說一個的就是對參數的解析,目前采用解析成json進行存儲,這里就涉及到參數對象的轉換,盡量考慮全一點,比如 datatable,dataset,枚舉,空等情況,借用網上代碼,稍作修改,奉上:
1 public static object ConvertToObject(object obj, Type type) 2 { 3 if (type == null) return obj; 4 if (obj == null) return type.IsValueType ? Activator.CreateInstance(type) : null; 5 6 Type underlyingType = Nullable.GetUnderlyingType(type); 7 if (type.IsAssignableFrom(obj.GetType())) 8 { 9 return obj; 10 } else if ((underlyingType ?? type).IsEnum) // 如果待轉換的對象的基類型為枚舉 11 { 12 if (underlyingType != null && string.IsNullOrEmpty(obj.ToString())) // 如果目標類型為可空枚舉,並且待轉換對象為null 則直接返回null值 13 { 14 return null; 15 } 16 else 17 { 18 return Enum.Parse(underlyingType ?? type, obj.ToString()); 19 } 20 }else if (typeof(IConvertible).IsAssignableFrom(underlyingType ?? type)) // 如果目標類型的基類型實現了IConvertible,則直接轉換 21 { 22 try 23 { 24 return Convert.ChangeType(obj, underlyingType ?? type, null); 25 } 26 catch 27 { 28 return underlyingType == null ? Activator.CreateInstance(type) : null; 29 } 30 } 31 elseelse if (typeof(IConvertible).IsAssignableFrom(underlyingType ?? type)) // 如果目標類型的基類型實現了IConvertible,則直接轉換 32 { 33 try 34 { 35 return Convert.ChangeType(obj, underlyingType ?? type, null); 36 } 37 catch 38 { 39 return underlyingType == null ? Activator.CreateInstance(type) : null; 40 } 41 } 42 else { 43 TypeConverter converter = TypeDescriptor.GetConverter(type); 44 if (converter.CanConvertFrom(obj.GetType())) 45 { 46 return converter.ConvertFrom(obj); 47 } 48 ConstructorInfo constructor = type.GetConstructor(Type.EmptyTypes); 49 if (constructor != null) 50 {object o = constructor.Invoke(null); 51 PropertyInfo[] propertys = type.GetProperties(); 52 Type oldType = obj.GetType(); 53 foreach (PropertyInfo property in propertys) 54 { 55 PropertyInfo p = oldType.GetProperty(property.Name); 56 if (property.CanWrite && p != null && p.CanRead) 57 { 58 property.SetValue(o, ConvertToObject(p.GetValue(obj, null), property.PropertyType), null); 59 } 60 } 61 return o; 62 } 63 } 64 return obj; 65 }
對於 統計數據寫入公司平台服務器,第一控制 客戶端上傳的時間點盡量分散以減小並發帶來的壓力,第二 采用rabbitmq 接受數據,並寫入服務器,服務端這邊采用站點形式供查詢統計數據,接口監控開啟設置等,涉及的技術棧: .net core+webapi+elementui+vuejs+rabbitmq