View Advanced System Information on a Remote Computer
ניתן להשיג את הקוד המלא כאן
השבוע ביקרתי אצל לקוח, שהתלונן שבסביבת הSharepoint המשתמשים מרגישים איטיות.
הסביבה שעליה יושב השרת(ים) הינה סביבת SQL Cluster וגרסת SQL Server 2008 Ent.
הסביבה הינה וירטואלית. – VMware. כלומר, מספר מכונות יושבות על אותה הפלטפורמה- ESX (השרת הפיסי aka הברזלים)
בפועל VM יכול לשרת על אותו ESX(הברזל עצמו) או אחר באותה פלטפורמה – תלוי הגדרות.
בחזרה ללקוח, 2 המכונות שישבו תחת SQL Cluster.
Node1 – ישב על ESX שמכיל CPU ב 2.0Ghz
Node2 – ישב על ESX שמכיל CPU ב 3.0Ghz
Node2 היה אקטיבי במשך תקופה ארוכה, באחת מפעולות התחזוקה הוחלף הNode הפעיל לNode1. אז החלו להגיע תלונות רבות על איטיות.
לאחר זמן מה גילינו את מה שקרה.
החזרנו את הNode הפעיל לNode2 וביצענו מיגרציה(VMotion) לNode1 לESX בעל פלטפורמה זהה.
המקרה העלה חשש אצל הלקוח, שיכול להיות פוטנציאל בעייתי בעוד שרתים בארגון שמכילים SQL.
הבקשה של הלקוח הייתה למצוא דרך שאילתת T-SQL האם הCPU של הNodes זהים במקרים הבאים –
הסביבה אצל הלקוח מכילה שרתי SQL מ2005 ועד 2012 ומגוון גרסאות מערכות הפעלה Windows Server 2003 ומעלה.
קרוב לוודאי שמשימה כזאת לא הכי נכונה לביצוע מתוך SQL, אבל אהבתי את האתגר.
כמה פתרונות אפשריים עלו לי בראש:
דרך SQL עצמו בעזרת – Linked Server או SQLCMD – אך במקרה של SQL Cluster הNode המשני אינו זמין.
PowerShell – אולי זאת הדרך הנכונה, אבל נתקלתי בבעיה של מספר פלטפורמות. לגרסאות הwindows השונות קיים גרסת PS שונה וכך גם המטודות שונות זו מזו.
לדוגמא, הסקריפט שכתבתי יעבוד ברוב המכונות העדכניות. אך, מכונות בעלות מערכת הפעלה windows server 2003 ומטה, הקוד לא ייתמך – בגלל גרסאות שונות של PowerShell .
DECLARE @cmd VARCHAR(4000);
DECLARE @RemoteServer VARCHAR(4000) ;
DECLARE @output TABLE (line varchar(4000));
SELECT @RemoteServer = NodeName
FROM sys.dm_os_cluster_nodes
WHERE NodeName != @@servername
if @RemoteServer is null
SELECT @RemoteServer = replica_server_name
from sys.availability_replicas
where replica_server_name != @@servername
--set @RemoteServer 'print-srv'
SET @cmd = 'powershell.exe -noprofile -command "$servername = ''' + @RemoteServer + '''; invoke-command -computer $servername -scriptblock {[array]$wmiinfo = Get-WmiObject Win32_Processor; $cpu = ($wmiinfo[0].name); $cores = ( $wmiinfo | Select SocketDesignation | Measure-Object ).count; $NumberOfLogicalProcessors = ( $wmiinfo[0].NumberOfLogicalProcessors); $obj = New-Object Object; $obj | Add-Member Noteproperty Servername -value $servername; $obj | Add-Member Noteproperty CPU -value $cpu; $obj | Add-Member Noteproperty Cores -value $cores; $obj | Add-Member Noteproperty NumberOfLogicalProcessors -value $NumberOfLogicalProcessors; Write-Host ($obj | Format-List | Out-String);}"';
INSERT @output
EXEC xp_cmdshell @cmd;
IF exists(select top 1 1 from @output WHERE line like '%is not recognized as an internal or external command%')
begin
delete from @output;
SET @cmd = '%SYSTEMROOT%\System32\WindowsPowerShell\v1.0\powershell.exe -noprofile -command "$servername = ''' + @RemoteServer + '''; invoke-command -computer $servername -scriptblock {[array]$wmiinfo = Get-WmiObject Win32_Processor; $cpu = ($wmiinfo[0].name); $cores = ( $wmiinfo | Select SocketDesignation | Measure-Object ).count; $NumberOfLogicalProcessors = ( $wmiinfo[0].NumberOfLogicalProcessors); $obj = New-Object Object; $obj | Add-Member Noteproperty Servername -value $servername; $obj | Add-Member Noteproperty CPU -value $cpu; $obj | Add-Member Noteproperty Cores -value $cores; $obj | Add-Member Noteproperty NumberOfLogicalProcessors -value $NumberOfLogicalProcessors; Write-Host ($obj | Format-List | Out-String);}"';
INSERT @output
EXEC xp_cmdshell @cmd;
end
SELECT LEFT(line,CHARINDEX(' ',line)),RIGHT(line,CASE CHARINDEX(':',REVERSE(line)) WHEN 0 THEN 0 ELSE CHARINDEX(':',REVERSE(line))-1 END)
FROM @output
WHERE line IS NOT NULL;
Clr – מכיוון שהשרתים מפלטפורמה 2005 – 2012, מראש עלי להכין 2 סוגי מסדי נתונים שיתמכו ב2 CLR, האחד לגרסאת .Net 2.0 לתתמוך לשרתים עד 2008R2 ו.Net 3.5 לשרתים 2012 ומעלה.
ניראה לי הרפתקאה שלא כדי להיכנס בשלב זה.
פתרון יחסית קל לשימוש נין ליישם דרך הcmd
מצאתי את הכלי הבא -
כאשר אני זורק את הקבצים בשרת המתשאל(המקור) ורושם את הפקודה הבאה –
אני אוכל לדעת מה סוג הCPU של השרת אותו תשאלתי –
@" Usaage: RunRemote <\\server> Process <remote.exe> [arg1 arg2 ...]"
RunRemote \\xxx Process cmd /C wmic cpu get name
אך, הרעיון קצת יותר מורכב,
הלקוח לא מעוניין בתלות הקבצים וזיכרון המפעיל לזכור לשים את הקבצים בכל שרת.
יש מסד נתונים ניהולי והדרך היותר עדיפה היא, לשמור את הקבצים בטבלה ובהרצת פרוצדורה לזרוק אותם לנתיב מסוים – האם ניסיתם לבצע את זה פעם?
סקריפט לטעינת הקבצים-
CREATE TABLE [dbo].[Files](
[FileID] [int] IDENTITY(1,1) NOT NULL,
[FileContentType] [varchar](50) NOT NULL,
[FileName] [varchar](150) NOT NULL,
[Content] [varbinary](max) NOT NULL
) ON [PRIMARY]
insert [dbo].[Files]
Select 'dll', 'CodeProject.Remoting.Common.dll',(SELECT * FROM OPENROWSET(BULK N'D:\tmp\Release_files\CodeProject.Remoting.Common.dll', SINGLE_BLOB) AS document) document
go
insert [dbo].[Files]
Select 'dll', 'ICSharpCode.SharpZipLib.dll',(SELECT * FROM OPENROWSET(BULK N'D:\tmp\Release_files\ICSharpCode.SharpZipLib.dll', SINGLE_BLOB) AS document) document
go
insert [dbo].[Files]
Select 'exe', 'Process.exe',(SELECT * FROM OPENROWSET(BULK N'D:\tmp\Release_files\Process.exe', SINGLE_BLOB) AS document) document
go
insert [dbo].[Files]
Select 'exe', 'RunRemote.exe',(SELECT * FROM OPENROWSET(BULK N'D:\tmp\Release_files\RunRemote.exe', SINGLE_BLOB) AS document) document
go
insert [dbo].[Files]
Select 'exe', 'ServiceOnRemoteMachine.exe',(SELECT * FROM OPENROWSET(BULK N'D:\tmp\Release_files\ServiceOnRemoteMachine.exe', SINGLE_BLOB) AS document) document
go
טעינת קבצים לטבלה עברה בהצלחה.
סקריפט לשליפת הקבצים מתוך הטבלה –
דרך א - BCP
DECLARE @SQLcommand varchar(8000)
set @SQLcommand = 'bcp "SELECT Content FROM [xxx].[dbo].[Files]" queryout "D:\CodeProject.Remoting.Common.dll" -T -n -S "SQLNODE1"'
אך, התוצאה לא משביעה רצון
דרך ב – Ole Automation Procedures
sp_configure 'show advanced options', 1
GO
RECONFIGURE;
GO
sp_configure 'Ole Automation Procedures', 1
GO
RECONFIGURE;
GO
DECLARE @SQLIMG VARCHAR(MAX) ,
@DOC_PATH VARCHAR(50) = 'D:\' ,
@TIMESTAMP VARCHAR(MAX) ,
@ObjectToken INT ,
@FILENAME VARCHAR(50);
SET @TIMESTAMP = @DOC_PATH + 'CodeProject.Remoting.Common.dll';
SELECT @FILENAME = DocData
FROM [dbo].[Files];
EXEC sp_OACreate 'ADODB.Stream', @ObjectToken OUTPUT
EXEC sp_OASetProperty @ObjectToken, 'Type', 1
EXEC sp_OAMethod @ObjectToken, 'Open'
EXEC sp_OAMethod @ObjectToken, 'Write', NULL, @FILENAME --new variable here
EXEC sp_OAMethod @ObjectToken, 'SaveToFile', NULL, @TIMESTAMP, 2
EXEC sp_OAMethod @ObjectToken, 'Close'
EXEC sp_OADestroy @ObjectToken
דרך ג - CLR
פה, נעזרתי בclr שניתן להשיג מכאן –
כזכור לנו מהפסקה מעלה, תחילה לא רציתי להשתמש בclr בגלל קימפול ב2 סוגי .Net.
אך, יחסית פה המקרה הוא דיי פשוט.
ניתן לקחת את הקוד שמסופק מהמאמר, ולהכניס לקובץ טקסט בעל השם SqlFileProcessor.cs ומפה זה כבר דיי פשוט לקמפל לאיזה גרסה של .Net בדרך הבאה– בעזרת CMD
על מנת לקמפל את הקוד ל.Net 2.0 -
C:\Windows\Microsoft.NET\Framework\v2.0.50727\csc.exe /target:library /out:C:\temp\SqlFileProcessor.dll C:\temp\SqlFileProcessor.cs
על מנת לקמפל את הקוד ל.Net 3.5 -
C:\Windows\Microsoft.NET\Framework\v3.5\csc.exe /target:library /out:C:\temp\SqlFileProcessor.dll C:\temp\SqlFileProcessor.cs
מכאן הטעינה יחסית זהה –
USE <DB_Name>;
GO
ALTER DATABASE <DB_Name> SET TRUSTWORTHY ON;
GO
EXEC sp_changedbowner 'sa'
go
EXEC sp_configure 'clr enabled',1 ;
GO
RECONFIGURE
GO
CREATE ASSEMBLY SqlFileProcessor
from 'C:\SqlFileProcessor.dll'
WITH PERMISSION_SET =EXTERNAL_ACCESS;
GO
CREATE Function [Utility].[ufn_clr_GetBytesFromFile]( @FileName NVarchar(128)) RETURNs varbinary(max)
AS EXTERNAL NAME SqlFileProcessor.FileProcessor.GetBytesFromFile;
GO
CREATE Function [Utility].[ufn_clr_SaveFileToDisk]( @sFileName Nvarchar(128), @Blob varbinary(max))Returns int
AS EXTERNAL NAME SqlFileProcessor.FileProcessor.SaveFileToDisk
GO
הקוד לטובת התשאול המלא יראה כך –
DECLARE @RemoteServer VARCHAR(4000) ;
DECLARE @output TABLE (line varchar(4000));
DECLARE @RemoteProp TABLE([Server] sysname NOT NULL,Property VARCHAR(4000),[Value] varchar(4000));
DECLARE @cmdShell VARCHAR(4000);
DECLARE @PathcmdFile NVARCHAR(2048);
IF OBJECT_ID('tempdb..#Nodes') IS NOT NULL
DROP TABLE #Nodes;
CREATE TABLE #Nodes(NodeName sysname,[Type] sysname);
IF OBJECT_ID('tempdb..#RemoteServer') IS NOT NULL
DROP TABLE #RemoteServer;
CREATE TABLE #RemoteServer([Server] sysname NOT NULL,Property VARCHAR(4000),[Value] varchar(4000));
INSERT #Nodes
SELECT NodeName,'SQL Cluster' [Type]
FROM sys.dm_os_cluster_nodes;
if SERVERPROPERTY('IsHadrEnabled') = 1
EXEC('INSERT #Nodes
SELECT member_name,''AlwaysOn''
FROM sys.dm_hadr_cluster_members');
DECLARE @NoOutput TABLE (ID INT);
DECLARE @files TABLE (ID int IDENTITY, FileName varchar(100))
DECLARE cuNode CURSOR LOCAL FAST_FORWARD READ_ONLY FOR
SELECT NodeName
FROM #Nodes
OPEN cuNode
FETCH NEXT FROM cuNode INTO @RemoteServer
WHILE @@FETCH_STATUS = 0
BEGIN
DELETE FROM @output;
DELETE FROM @RemoteProp;
--By powershell
PRINT'By powershell'
SET @cmdShell = 'powershell.exe -noprofile -command "$servername = ''' + @RemoteServer + '''; invoke-command -computer $servername -scriptblock {[array]$wmiinfo = Get-WmiObject Win32_Processor; $cpu = ($wmiinfo[0].name); $cores = ( $wmiinfo | Select SocketDesignation | Measure-Object ).count; $NumberOfLogicalProcessors = ( $wmiinfo[0].NumberOfLogicalProcessors); $obj = New-Object Object; $obj | Add-Member Noteproperty CPU -value $cpu; $obj | Add-Member Noteproperty Cores -value $cores; $obj | Add-Member Noteproperty NumberOfLogicalProcessors -value $NumberOfLogicalProcessors; Write-Host ($obj | Format-List | Out-String);}"';
INSERT @output
EXEC master.sys.xp_cmdshell @cmdShell;
IF exists(select top 1 1 from @output WHERE line like '%is not recognized as an internal or external command%')
begin
delete from @output;
SET @cmdShell = '%SYSTEMROOT%\System32\WindowsPowerShell\v1.0\powershell.exe -noprofile -command "$servername = ''' + @RemoteServer + '''; invoke-command -computer $servername -scriptblock {[array]$wmiinfo = Get-WmiObject Win32_Processor; $cpu = ($wmiinfo[0].name); $cores = ( $wmiinfo | Select SocketDesignation | Measure-Object ).count; $NumberOfLogicalProcessors = ( $wmiinfo[0].NumberOfLogicalProcessors); $obj = New-Object Object; $obj | Add-Member Noteproperty CPU -value $cpu; $obj | Add-Member Noteproperty Cores -value $cores; $obj | Add-Member Noteproperty NumberOfLogicalProcessors -value $NumberOfLogicalProcessors; Write-Host ($obj | Format-List | Out-String);}"';
INSERT @output
EXEC master.sys.xp_cmdshell @cmdShell;
END
IF NOT EXISTS(SELECT TOP 1 1 FROM @output WHERE line = 'The system cannot find the path specified.')
INSERT @RemoteProp
SELECT @RemoteServer [Server],
LEFT(line,CHARINDEX(' ',line))[Property],
RIGHT(line,CASE CHARINDEX(':',REVERSE(line)) WHEN 0 THEN 0 ELSE CHARINDEX(':',REVERSE(line))-1 END)[Value]
FROM @output
WHERE line IS NOT NULL ;
--By cmdshell + clr
IF NOT EXISTS(SELECT TOP 1 1 FROM @RemoteProp)
BEGIN
PRINT 'By cmdshell + clr';
DELETE FROM @output;
SET @PathcmdFile = N'C:\tmp\RemoteFilesCmd\';
SET @cmdShell = 'dir "' + @PathcmdFile + '" /b';
EXEC master.dbo.xp_create_subdir @PathcmdFile;
INSERT INTO @files EXECUTE master.sys.xp_cmdshell @cmdShell;
INSERT @NoOutput
SELECT [Utility].[ufn_clr_SaveFileToDisk] (@PathcmdFile + PF.FileName ,Content)
FROM dbo.Files PF
LEFT JOIN @files DF ON DF.FileName = PF.[FileName]
WHERE DF.ID IS NULL;
SET @cmdShell = @PathcmdFile + 'RunRemote \\' + @RemoteServer + ' Process cmd /C wmic cpu get name'
DELETE FROM @output;
INSERT @output
EXEC master.sys.xp_cmdshell @cmdShell;
INSERT @RemoteProp
SELECT TOP 1 @RemoteServer [Server],'CPU' [Property],REPLACE(line,'Name ','') [Value]
FROM @output
WHERE line NOT IN ('
','Return value = 0','Completed: cmd /C wmic cpu get name','Name')
UNION ALL
SELECT TOP 1 @RemoteServer [Server],'Cores' [Property],CONVERT(VARCHAR(4000),COUNT(1)) [Value]
FROM @output
WHERE line NOT IN ('
','Return value = 0','Completed: cmd /C wmic cpu get name','Name');
END
IF EXISTS(SELECT TOP 1 1 FROM @RemoteProp)
BEGIN
INSERT #RemoteServer
SELECT Server ,
Property ,
Value
FROM @RemoteProp
END
DELETE FROM @RemoteProp;
FETCH NEXT FROM cuNode INTO @RemoteServer;
END
CLOSE cuNode;
DEALLOCATE cuNode;
SELECT *
FROM #RemoteServer
DROP TABLE #Nodes,#RemoteServer;
ניתן להשיג את הקוד המלא כאן