Showing posts with label Automation. Show all posts
Showing posts with label Automation. Show all posts

Jan 26, 2019

Python Multithreading and automation demonstration for DBAs

A few weeks ago, I started moving my SQL Server automation scripts from Power Shell to Python to add some data analysis capabilities. Just to give some background, I am responsible for 100s of servers and collect data from remote hosts worldwide and copy at centralized location in order to process it to get useful insights like monitoring, performance analysis, capacity planning etc.

Along with re-writing scripts, the motive was also to improve the scripts as the module was connecting to each remote server in iteration, it used to take very long time before it could complete the data processing.

I decided to optimize my scripts by taking advantage of multi threading in Python while connecting to remote hosts in parallel. I also would like to point out a function from psycopg2.extras module called execute_values and it can make use of LIST that can be passed directly to function in case there are multiple rows and statements and saving you from writing loop.

Before demonstration, I would like to show facts how much time I saved using this approach in numbers comparing traditional method with Thread model:

Without thread, the process used to take 45 seconds




With thread, same process took only 761 milliseconds, a 60 times improvement




I am demonstrating one of my set of scripts that is responsible to get the list of databases from remote SQL Servers. As I am using threading in the demo, need to import module called threading, then map the function with thread object and finally start the thread.

File Name: refreshDatabases.py
Purpose: This is the main class to sync databases:

First need to import the modules required for this script:


import json
import psycopg2
import psycopg2.extras as ex
import datetime
import sys
import threading
# Following modules are manually written separately just for the purpose of reusing objects: 
import sqlRequestInJson
from pgdatabase import cursorfromconnectionpool as pgconnectionpool, Database as pgdatabase
from pgconnectionstring import connectionstring as pgconnectionstring
Next, get list of all IP addresses and ports of remote hosts to connect to.
class getdata:
def __init__(self):
self.query="select sl.ipaddress , sl.name , sl.serverid, sl.port from serverslist sl"
def return_serverlist(self):
cs = pgconnectionstring() # following line of code is using psycopg2 through a class written in different file, I am doing it to hide user name and passwords
pgdatabase.initialize(user=cs.user, password=cs.password, database=cs.database, host=cs.host)
        # Get servers list
with pgconnectionpool() as cursor:
cursor.execute(self.query)
result = cursor.fetchall()
return result

This is the main working class that would collect and save data in tables
class update:
    # empty initialize class
    def __init__(self):
        pass
    # following function retuns cursor from postgres
    def updateData(self, script):
        cs = pgconnectionstring()
        pgdatabase.initialize(user=cs.user, password=cs.password, database=cs.database, host=cs.host)
        with pgconnectionpool() as cursor:
            cursor.execute(script)
    # Following would call the fuction return_serverlist defined in the class above to get list of servers to be connected to
    def returnRows(self): #self, serverid, serverip, name):     
        gd=getdata()
        return gd.return_serverlist()
    # This is the thread function, this would create thread based on number of servers to be connected to
    def updateDatabasesThread(self, rows):
        # Empty thread list
        threadlist=[]
        for  request in rows: 
            lst=list(request['ipaddress'])
            # removing "\" from ip address as psycopg2 does not like "\", rather will provide port number for connection.
            if lst.count('\\')>0:
                lst=lst[0:request['ipaddress'].find('\\')]
                request['ipaddress']=''.join(lst)
            # The threading function takes paraters like fuction and arguments to be referencedd by each thread
            t=threading.Thread(target=self.updateDatabases, args=(request['serverid'], request['ipaddress'], request['port'],))
            # Adding thread to the list
            threadlist.append(t)
         # start the thread
        for thread in threadlist:
            try:
                thread.start()
            except:
                e="{}".format(sys.exc_info())
                print(e)
    # Following function will get database name from sql server and then would save into postgres
    def updateDatabases (self,  serverid, ipaddr, port):
        currentDT = str(datetime.datetime.utcnow())
        sqlreq=sqlRequestInJson.sqlrequests()
        # get data from SQL Server and adding result to result variable
        result=sqlreq.execsql(ipaddr, "select name from sys.databases", port)
        cs = pgconnectionstring()
        pgdatabase.initialize(user=cs.user, password=cs.password, database=cs.database, host=cs.host)
        with pgconnectionpool() as cursor:
            dblist=[]
            for row in result['result']:
                try:
                    values=(row['name'], serverid)
                    # append database name and serverid in list to be used later with execute_values
                    dblist.append(values)
                except:
                    e="{}".format(sys.exc_info())
                    print(e)
            # Following line of code will insert data into postgres, without using loop as it takes list that will be passed to insert values by function itself
            ex.execute_values(cursor,'insert into rtm.databases(databasename, serverid) values %s  on conflict(serverid, databasename) DO NOTHING', dblist)
# finally put all together
# creating object from class
upd= update()
# return data from function
rows=upd.returnRows()
#Start thread
print("Started {}" .format(str(datetime.datetime.utcnow())))
upd.updateDatabasesThread(rows)
print("Ended {}" .format(str(datetime.datetime.utcnow())))
         
As mentioned above about writing classes separately, Python is an Object Oriented scripting language, thus I created multiple classes in separate files to re-use the code and these are the supporting python class files to be used in the script above:

File Name: pgconnectionstring.py:
Purpose: save credentials in separate class and use these as properties in connection strings wherever needed


class connectionstring:
    def __init__(self):
        self.user="user"
        self.password="password"
        self.database="dbname"
        self.host="ip\hostname with port"

File Name: pgdatabase.py
Purpose: This class returns the data from Postgres in Data Dictionary format:


from psycopg2 import pool
from psycopg2 import extras
import psycopg2
class Database:
    connection_pool=None;
    @classmethod
    def initialize(cls,**kwargs):
        Database.connection_pool=psycopg2.connect(**kwargs);
    @classmethod
    def get_connection(cls):
        #return cls.__connectionpool.getconn();
        return cls.connection_pool;

class cursorfromconnectionpool:
 
    def __init__(self):
        self.connection = None
 
    def __enter__(self):
        self.connection=Database.connection_pool
     
        self.cursor = self.connection.cursor(cursor_factory=psycopg2.extras.DictCursor)
        return self.cursor
 
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_val is not None:
            self.connection.rollback();
        else:
            self.cursor.close()
            self.connection.commit()
 
         
File Name: sqlRequestInJson.py
Purpose: This class returns the data from SQL in Data Dictionary format:


import pymssql
import sys
class sqlrequests:
    def execsql(self, server, script, port):
     
        try:
            connection=pymssql.connect(user='user', password='xxxx', database='mydb', host=server, as_dict=True,login_timeout=30, timeout=30, port=port)
            connection.autocommit(True)
            with connection.cursor() as cursor:
                cursor.execute(script)
                result=cursor.fetchall()
                connection.close()
                # instead of returning direct data, I am returning json data with result in result key and message in message key, this will be useful to read the exception from calling class
                return {'result':result,'message':'nothing'}
        except:
            e="{}".format(sys.exc_info())
            e=e.replace("'","*")
            # as metioned in comment above, following line of code will return exception to calling class instead of simply crashing
            return {'result':'exception','message':e}
This demonstration involves multiple database servers including Microsoft SQL Server and Postgres. In order to execute this, you will have to create one table in one of the Postgres server where data will be stored. The table in Postgres database should have at least table with this spec: select sl.ipaddress , sl.name , sl.serverid, sl.port from serverslist sl

Dec 4, 2018

Background Intelligent Transfer Service - BITS

BITS or Background Intelligent Transfer Service has been available for many years with Windows Servers, it is generally considered only as a background service for Web Servers, Application, etc., though it can be used by System Administrator, IT Administrators or DBAs too. As a Database Professional I find it very useful thus I would like to share a demo of this service. The purpose of BITS is simple, transfer files between source and destination with fault tolerance capabilities.
Making it easy to use, BITS cmdlets are available with PowerShell too. The purpose of BITS as written on Microsoft Documentation gives more insight on its capabilities:
Background Intelligent Transfer Service (BITS) is used by programmers and system administrators to download files from or upload files to HTTP web servers and SMB file shares. BITS will take the cost of the transfer into consideration, as well as the network usage so that the user's foreground work has as little impact as possible. BITS also handles network interruptions, pausing and automatically resuming transfers, even after a reboot. BITS includes PowerShell cmdlets for creating and managing transfers as well as the BitsAdmin command-line utility.
Here is quick guide on using BITS with PowerShell:

Importing Module in PowerShell
  • This will be a good practice to run PowerShell with elevated rights (Right Click and Run as administrator) or run in same security context each time, otherwise there may be inconsistencies as some of the resources may not be available.
  • Once Powershell Opens, execute Import-Module BitsTransfer
  • You should not get any message or prompt if Import was successful
image
  • This demonstration has been tested with PowerShell Version 4 and 5. You may verify the version by executing $psversiontable.psversion from PS prompt.
Pre-Requisites before starting transfer
  • Obviously you would need to know the source and destination locations and file to be transfer. Also make sure both locations are accessible from PS prompt.
  • BITS provides two types of transfer modes, Synchronous and Asynchronous.
    • In Asynchronous mode BITS job will be registered and pointer will be returned to prompt, job will continue in background by service.
    • In Synchronous mode, BITS job will be registered and pointer will not be returned to prompt unless transfer completes.
Limitation
  • The only major limitation with BITS is that it can only be run if user’s session is active, i.e. BITS commands cannot be scheduled by schedulers such as SQL Agent, Task Scheduler, etc., with only exception that user can disconnect the session without logging off from the Windows Session and the process will continue. If this restriction is removed then this will become my favorite file transfer utility.
Cmdlets
  • To start\initiate transfer: start-BitsTransfer –Source [sourcepath] –Destination [DestinationLocation] –Asynchronous [If not specified then it becomes Synchronous]
  • To pause transfer: suspend-BitsTransfer 
  • To resume transfer: resume-BitsTransfer
  • To modify existing transfer: set-BitsTransfer
  • To get existing transfer jobs: get-BitsTransfer
  • To stop\terminate transfer: remove-BitsTransfer
  • To complete the transfer job: complete-BitsTransfer
Demonstration
  • To demonstrate, I will transfer a 800 GB file using BITS as shown in the image below and will use some of the commonly used parameters as well as will get more useful details out of the job.
image

  • The source and destination are “C:\demo\source\SSMS-Setup-ENU.exe” and “\\swarns\demo\destination\” respectively

Starting Transfer in Asynchronous mode
  • import-module bitstransfer
  • start-bitsTransfer -Source "C:\demo\source\SSMS-Setup-ENU.exe" –Destination “\\swarns\demo\destination” –Asynchronous
image
  • The image above shows the job has been registered and current job status as Connecting

Suspending the job
  • I have intentionally suspended this job so that I can read properties of this job before copy finishes, to do that the cmdlet is suspend-bitstransfer
  • To suspend you need to provide jobid as a parameter in two different ways, first is using PIPE and other is using variable. I am going to use PIPE and first need jobid
    • get-bitstransfer | select jobid
image
    • get-BitsTransfer -jobid 1c6eaea9-efb5-4e1c-bae9-64b79b5d8433 | suspend-BitsTransfer

Reading BITS properties in easily readable format
  • I have intentionally suspended this job so that I can read properties of this job before copy finishes, to do that the cmdlet is suspend-bitstransfer
  • get-BitsTransfer -jobid 1c6eaea9-efb5-4e1c-bae9-64b79b5d8433 | select *
image
  • The image above shows basic output of command and this can be formatted to get more usable
  • I have used the following scriptIf you want to test the following script, please write the following scriptlet in single line, I have intentionally divided the scriptlet at each Pipe to make it readable
    • Get-BitsTransfer -jobid  1c6eaea9-efb5-4e1c-bae9-64b79b5d8433 
    • | SELECT *,  @{name='Xferred_percent';expr={$_.BytesTransferred/$_.BytesTotal*100}}, @{name='Minutes'; expr={((Get-Date)-$_.creationtime).TotalMinutes}}
    • |SELECT @{name='BytesPerMinute';expr={$_.BytesTransferred/$_.Minutes}},  @{name='EtaInMinutes';expr={$_.Minutes/$_.BytesTransferred*($_.BytesTotal-$_.BytesTransferred)}}, *
    • | SELECT @{name='CompletionEstimate';expr={(Get-Date).AddMinutes($_.EtaInMinutes)}}, Xferred_percent, Minutes, BytesPerMinute, JobState | Format-Table
image
  • The script above may sound little bit complex, but let me simplify it below:
    • A PIPE  “|” is used to pass the value to next cmdlet or expression, to define an expression, a hashtable “@{}” is used with name and expr tags. Calculations can be performed in expr tag. Next PIPE can access the expr by refering to name defined in hashtable.
image
Last Step
  • To resume the suspended job, the cmdlet will be get-BitsTransfer -jobid 1c6eaea9-efb5-4e1c-bae9-64b79b5d8433  | resume-BitsTransfer –Asynchronous #Optional
  • Once transfer completes, if you started job with Asynchronous file transfer, this would require you to manually complete the job by issuing get-BitsTransfer -jobid 1c6eaea9-efb5-4e1c-bae9-64b79b5d8433  | remove-BitsTransfer
image

Finally
This service is robust, runs very well, I never got into any issues after transferring bunch of files. I have used robocopy too for many transfers, but could not read transfer statistics asynchronously. At the same time the inability to use BITS if user did not login, reduces its usage for many. I wish if there was no such restriction. Thanks for reading this article, please leave your comments if you find it useful.








Optimizing Indexes with Execution Plans

Contact Me

Name

Email *

Message *