Verbesserung von Performance und Monitoring

Neue Performance-Metriken in `EDMIService` hinzugefügt, inkl. Felder für Job-Dauer, langsame Queries und Timestamps. `ServiceStatusResponse` um diese Metriken erweitert.
`DatatableJob` optimiert: `<DisallowConcurrentExecution>` hinzugefügt, Logging für langsame Queries (>10s) und Jobs (>30s) verbessert, sowie Gesamt-Dauer-Messung integriert.
`Scheduler` nutzt nun dedizierte MSSQL-Instanz zur Entkopplung von Datenbankoperationen. Thread-Pool-Größe reduziert und gestaffeltes Starten von INIT-Jobs eingeführt.
Connection-Strings in `WindowsService` angepasst: `ApplicationName` spezifisch gesetzt und `MaxPoolSize` erhöht. Allgemeine Logging-Verbesserungen für bessere Skalierbarkeit und Stabilität.
This commit is contained in:
Developer01
2026-10-13 12:04:13 +02:00
parent 205e0ad302
commit 8220852e56
6 changed files with 102 additions and 52 deletions

View File

@@ -1,20 +1,13 @@
Option Explicit On
Imports DigitalData.Modules.Database
Imports DigitalData.Modules.Language.Utils
Imports DigitalData.Modules.Logging
Imports Quartz
Public Class DatatableJob
<DisallowConcurrentExecution> Public Class DatatableJob
Implements IJob
Private Const COMMAND_SQL_TIMEOUT = 240
Private _MSSQL As MSSQLServer
Public Function Execute(context As IJobExecutionContext) As Task Implements IJob.Execute
Dim oJobData = context.MergedJobDataMap
Dim oLogConfig As LogConfig = oJobData.Item("LogConfig")
@@ -26,7 +19,8 @@ Public Class DatatableJob
Dim oResult As New JobResult()
oLogger.Info("Running Datatable Job [{0}]/[{1}]", context.JobDetail.Key, oCronJobTitle)
Dim oJobStopWatch As New Stopwatch() ' ← Gesamt-Messung für den gesamten Job
oJobStopWatch.Start()
Try
Dim oMSSQL As MSSQLServer = oJobData.Item("MSSQL")
Dim oCronSQL As String = $"SELECT * FROM TBAPPSERV_CRON_DETAIL WITH (NOLOCK) WHERE CRON_ID = {oCronJobId}"
@@ -46,16 +40,23 @@ Public Class DatatableJob
oLogger.Debug("Datatable Name: {0}", oDatatableName)
oLogger.Debug("Connection Id: {0}", oConnectionId)
oStopWatch.Start()
oStopWatch.Restart()
Dim oConnectionString = oMSSQL.Get_ConnectionStringforID(oConnectionId)
Dim oDecryptedConnectionString = MSSQLServer.DecryptConnectionString(oConnectionString)
Dim oConnectionString As String = oMSSQL.Get_ConnectionStringforID(oConnectionId)
Dim oTable = oMSSQL.GetDatatableWithConnection(oSQL, oDecryptedConnectionString, COMMAND_SQL_TIMEOUT)
Dim oTable = oMSSQL.GetDatatableWithConnection(oSQL, oConnectionString, COMMAND_SQL_TIMEOUT)
oTable.TableName = oDatatableName
oStopWatch.Stop()
oLogger.Debug("[Time] Getting Data From DB: {0}ms", oStopWatch.ElapsedMilliseconds)
Dim oQueryMs = oStopWatch.ElapsedMilliseconds
' ✅ Schwellwert-Warnung: Query dauert länger als 10s
If oQueryMs > 10000 Then
oLogger.Warn("⚠ SLOW QUERY [{0}] | Datatable=[{1}] | Duration=[{2}ms] | SQL=[{3}]",
oTitle, oDatatableName, oQueryMs, oSQL.Substring(0, Math.Min(200, oSQL.Length)))
Else
oLogger.Debug("[Time] Getting Data From DB: {0}ms", oQueryMs)
End If
oStopWatch.Restart()
Dim oView As DataView = Nothing
@@ -107,7 +108,15 @@ Public Class DatatableJob
' Das Ergebnis speichern
context.Result = oResult
oLogger.Info("✔✔ Fetched [{0}] Datatables ✔✔", oResult.Tables.Count)
oJobStopWatch.Stop()
' ✅ Gesamt-Performance-Log pro Job-Ausführung
Dim oTotalMs = oJobStopWatch.ElapsedMilliseconds
If oTotalMs > 30000 Then
oLogger.Warn("⚠ SLOW JOB [{0}] | Tables=[{1}] | TotalDuration=[{2}ms]",
oCronJobTitle, oResult.Tables.Count, oTotalMs)
Else
oLogger.Info("✔✔ Fetched [{0}] Datatables in [{1}ms] ✔✔", oResult.Tables.Count, oTotalMs)
End If
Catch ex As Exception
oLogger.Error(ex)

View File

@@ -1,4 +1,5 @@
Imports System.Collections.Specialized
Imports System.Data.SqlClient
Imports DigitalData.Modules.Database
Imports DigitalData.Modules.Logging
Imports Quartz
@@ -10,14 +11,14 @@ Public Class Scheduler
Inherits BaseClass
Private ReadOnly _Factory As StdSchedulerFactory
Private ReadOnly _MSSQL As MSSQLServer
Private ReadOnly _SchedulerMSSQL As MSSQLServer ' ✅ Dedizierte Connection für alle Scheduler-Operationen
Private _Scheduler As IScheduler
Private ReadOnly _JobListener As JobListener
Private ReadOnly _GlobalState As GlobalState ' ✅ NEU
Private ReadOnly _GlobalState As GlobalState
Private ReadOnly _Props = New NameValueCollection From {
{"quartz.serializer.type", "binary"},
{"quartz.threadPool.maxConcurrency", 3}
{"quartz.threadPool.maxConcurrency", 2} ' ← von 3 auf 2
}
Private Const JOB_GROUP As String = "DatatableJobs"
@@ -26,8 +27,17 @@ Public Class Scheduler
MyBase.New(pLogConfig)
_Factory = New StdSchedulerFactory(_Props)
_MSSQL = pMSSQL_Config
_JobListener = New JobListener(pLogConfig, _MSSQL, pTableStore)
' ✅ Separater Connection-Pool ausschließlich für Scheduler-Jobs
' Application Name weicht ab → ADO.NET legt eigenständigen Pool an
' → Scheduler-Queries blockieren nie die WCF-Verbindungen des Service
Dim oBuilder As New SqlConnectionStringBuilder(pMSSQL_Config.CurrentConnectionString)
oBuilder.ApplicationName = "EDMIService-Scheduler"
oBuilder.MaxPoolSize = 2 ' ← NEU: begrenzt SQL Server-Last unter Volllast
_SchedulerMSSQL = New MSSQLServer(pLogConfig, oBuilder.ToString())
Logger.Info("Scheduler: dedicated MSSQLServer instance created | AppName=[{0}]", oBuilder.ApplicationName)
_JobListener = New JobListener(pLogConfig, _SchedulerMSSQL, pTableStore)
_GlobalState = pGlobalState
_GlobalState.JobListener = _JobListener
@@ -37,14 +47,11 @@ Public Class Scheduler
End Sub
Public Async Sub Start()
' Get new scheduler
_Scheduler = Await _Factory.GetScheduler()
' configure it
_Scheduler.ListenerManager.AddJobListener(_JobListener,
GroupMatcher(Of JobKey).GroupEquals(JOB_GROUP))
' start it
Await _Scheduler.Start()
Dim oCronjobs As DataTable = Await GetCronJobs()
@@ -66,10 +73,13 @@ Public Class Scheduler
Dim oJobData As New JobDataMap From {
{"LogConfig", LogConfig},
{"MSSQL", _MSSQL},
{"MSSQL", _SchedulerMSSQL},
{"CronJobId", oGuid},
{"CronJobTitle", oTitle}
}
' INIT-Job: gestaffelt starten – 15s Abstand statt 10s
' Bei maxConcurrency=2 laufen max. 2 Jobs gleichzeitig
Dim oInitDelaySeconds As Integer = oCronjobs.Rows.IndexOf(oRow) * 15
Dim oIdentity As String = $"CRON-JOB-{oGuid}"
@@ -83,15 +93,16 @@ Public Class Scheduler
UsingJobData(oJobData).
Build()
Await _Scheduler.ScheduleJob(oJob, oCronTrigger)
Dim oNextFireTime As DateTimeOffset? = Await _Scheduler.ScheduleJob(oJob, oCronTrigger)
Logger.Info("Cron Job [{0}] -> next scheduled run: {1}", oTitle,
If(oNextFireTime.HasValue, oNextFireTime.Value.LocalDateTime.ToString("yyyy-MM-dd HH:mm:ss"), "unknown"))
' Also create variant of the job that runs immediately, once
Dim oInitIdentity As String = $"CRON-JOB-INIT-{oGuid}"
oInitTrigger = TriggerBuilder.Create().
WithIdentity(oInitIdentity & "-TRIGGER", JOB_GROUP).
StartNow().
StartAt(DateTimeOffset.Now.AddSeconds(oInitDelaySeconds)). ' ✅ war: StartNow()
Build()
oInitJob = JobBuilder.Create(Of DatatableJob)().
@@ -112,12 +123,11 @@ Public Class Scheduler
Logger.Warn("⚠ Unexpected Error while setting up scheduler: " & ex.Message)
End Try
End Sub
Public Async Function GetCronJobs() As Task(Of DataTable)
Try
Dim oSQL As String = "SELECT * FROM TBAPPSERV_CRON_JOB WITH (NOLOCK) WHERE ACTIVE = 1"
Dim oDatatable As DataTable = Await _MSSQL.GetDatatableAsync(oSQL)
Return oDatatable
Return Await _SchedulerMSSQL.GetDatatableAsync(oSQL) ' ✅ dedizierte Instanz
Catch ex As Exception
Return Nothing
End Try
@@ -126,9 +136,7 @@ Public Class Scheduler
Public Async Function GetCronJobDetails(CronJobId As Integer) As Task(Of DataTable)
Try
Dim oSQL As String = $"SELECT * FROM TBAPPSERV_CRON_DETAIL WITH (NOLOCK) WHERE CRON_ID = {CronJobId}"
Dim oDatatable As DataTable = Await _MSSQL.GetDatatableAsync(oSQL)
Return oDatatable
Return Await _SchedulerMSSQL.GetDatatableAsync(oSQL) ' ✅ dedizierte Instanz
Catch ex As Exception
Return Nothing
End Try
@@ -137,4 +145,4 @@ Public Class Scheduler
Public Async Sub [Stop]()
Await _Scheduler.Shutdown()
End Sub
End Class
End Class