Error 1205 or a deadlock occurred, how to deal with it?


Εισαγωγή

Το τελευταίο διάστημα στα μαθήματα και τις παρουσιάσεις που έχω κάνει όλο και περισσότεροι έρχονται και με ρωτούν τι πρέπει να κάνουν για το deadlock καθώς όπως μου λένε έχουν όλο και περισσότερες εμφανίσεις του φαινομένου αυτού.

Πάντα απαντώ σε όλους αυτά που θα ακολουθήσουν σε αυτό το post αλλά μέχρι τώρα δεν τα είχα γράψει και νομίζω ότι πλέον είναι καιρός να γίνει αυτό.

Τι είναι το deadlock;

Κατ’ αρχήν να κάνω σαφές ότι το deadlock δεν είναι κάποιο είδος lock που έχει ο SQL Server. Είναι ένα φαινόμενο/παράδοξο το οποίο συμβαίνει εξαιτίας των locks που έχει ο SQL Server.

Για να γίνει αυτό σαφές θα δώσω ένα παράδειγμα. Έστω ότι έχω ένα transaction που ενημερώνει τον πίνακα των πελατών και στην συνέχεια τον πίνακα των παραγγελιών, και ένα άλλο το οποίο ενημερώνει τους ίδιους πίνακες αλλά με αντίστροφη σειρά δηλαδή πρώτα τον πίνακα των παραγγελιών και μετά των πίνακα των πελατών. Εάν συμπέσει αυτά τα δύο να εκτελεστούν ταυτόχρονα τότε θα έχω ένα deadlock καθώς το ένα έχει κλειδώσει το επόμενο resource που το άλλο ζητάει. Σε αυτή την περίπτωση ο SQL Server επιλέγει το deadlock victim και το τερματίζει με το error 1205 ενώ το άλλο πλέον συνεχίζει κανονικά.

Αλλά ας δούμε πρώτα μερικά θέματα αρχιτεκτονικής του SQL Server που αφορούν το πώς ο SQL Server εντοπίζει τα deadlocks αλλά και το πώς επιλέγει το deadlock victim.

Πώς ο SQL Server εντοπίζει τα deadlocks;

Ο SQL Server κάθε 5 sec κοιτάζει όλα τα ενεργά transactions για το τι locks αυτά περιμένουν και δεν έχουν ακόμα ενεργοποιηθεί. Καθώς κάνει αυτό σημειώνει αυτά που έχουν τέτοια αιτήματα locks. Στον επόμενο κύκλο ελέγχει την κατάσταση όλων ενεργοποιημένων locks με αυτά που έχει βρει από την προηγούμενη φορά και είναι στις σημειώσεις που έχει ήδη κάνει και εάν βρει ότι κάποιο από αυτά που είχε σημειώσει ότι ακόμα περιμένει να ενεργοποιηθεί τότε επαναλαμβανόμενα ελέγχει όλα τα ενεργά transactions ώστε να εντοπίσει μια κυκλική αλυσίδα από locks (a.k.a. circular chain of lock requests). Εάν εντοπίσει μια τέτοια τότε επιλέγει το deadlock victim (μπορεί να είναι περισσότερα από ένα) και τερματίζει αυτό ή αυτά με το error 1205.

Πώς ο SQL Server επιλέγει το deadlock victim;

To deadlock victim by default επιλέγεται με βάση το κόστος του transaction. Τι σημαίνει αυτό; Σημαίνει ότι σαν θύμα επιλέγεται το transaction αυτό που έχει το μικρότερο κόστος rollback, δηλαδή αυτό που έχει να κάνει τα λιγότερα πράγματα rollback. Εδώ θα πρέπει να επισημάνω ότι αυτό μπορεί να αλλάξει αν πριν το transaction ορίζουμε με την SET DEADLOCK_PRIORITY το πόσο θέλουμε το transaction αυτό να είναι θύμα.

Πώς να αποφύγω τα deadlocks;

Αφού σας είπα εν ολίγοις τον μηχανισμό, ας έρθω στην ταμπακιέρα καθώς όλοι αυτό με ρωτάτε και δεν άλλο από το πώς θα εξαφανιστούν αυτά. Η απάντηση που χρόνια δίνω αλλά και που είμαι σίγουρος ότι όλο και κάπου αλλού την έχετε διαβάσει είναι ότι 100% να τα εξαφανίσει κανείς δεν γίνεται, αλλά μπορεί να τα κάνει να εμφανίζονται σπάνια. Για να επιτύχει όμως κάποιος την σπανιότητα της εμφάνισης τους θα πρέπει να ακολουθήσει κάποιους γνωστούς κανόνες σαν ευαγγέλιο όταν φτιάχνει transactions.

Για να το πω σκληρά θα πρέπει ο developer πριν αρχίσει να γράφει transactions να έχει ορκιστεί με αίμα ότι θα τηρήσει αυτούς, αλλιώς θα παραφράσω αυτό που έχει πει ο Αλέφαντος λέγοντας καλώς τα παιδιά καλώς τα deadlocks.

Οι κανόνες αυτοί είναι απλοί και εύκολοι στο να ακολουθηθούν και εφαρμοστούν και είμαι 100% σίγουρος ότι ακολουθούνται από έμπειρους developers που έχουν γνώση του SQL Server και δεν έχουν αφιερώσει την διεπαφή τους με τα δεδομένα μέσω ORMs. Ναι είναι σκληρό αυτό που λέω και θα ακούσω πολλά σχόλια για αυτό αλλά είναι η πραγματικότητα, απλά να τονίσω ότι ακόμα θεωρώ ότι είμαι developer αλλά από αυτούς που έχουν αποβάλει την στενοκεφαλιά που διακρίνει τους developers.

Κανόνες για την αποφυγή των deadlocks

Οι κανόνες αυτοί είναι

1. Χρησιμοποιούμε τα αντικείμενα πάντα με την ίδια σειρά σε όλα μας τα transactions

Στο παράδειγμα που παραπάνω χρησιμοποίησα για να εξηγήσω το deadlock αν σε ένα από αυτά είχαμε με την ίδια σειρά του άλλου είχαμε υλοποιήσει το transaction δεν θα είχαμε κανένα θέμα deadlocking, τουλάχιστον σε αυτά τα δύο. Δεν είναι δύσκολο να εφαρμόσουμε το κανόνα αυτό αρκεί να έχουμε αποφασίσει την σειρά και να το έχουμε επικοινωνήσει στους developers. Τόσο απλά. Αν ξέρω ότι αρκετοί θα μου φέρουν από χίλιες βρύσες νερό που λένε και στο χωριό μου και θα μου πουν ότι αυτό δεν γίνεται θα απαντήσω ότι στην πλειοψηφία των περιπτώσεων γίνεται. Ελάχιστες θα είναι αυτές που δεν μπορούμε να το κάνουμε, οπότε ας εξασφαλίσουμε την πλειοψηφία και για την μειοψηφία θα χρειαστεί να το σκεφτούμε περισσότερο.

2. Κρατάμε τα transactions μικρά και σύντομα και σε ένα batch όσο περισσότερο μπορούμε

Αν και ο πρώτος κανόνας να φαντάζει κάπως δύσκολος στο να επιτευχθεί, αυτός εδώ δεν είναι δα και πυρηνική φυσική. Είναι απλή λογική. Τόσο απλός είναι που μερικές φορές αναρωτιέμαι το γιατί δεν το ακολουθούν κάποιοι καθώς δεν κοστίζει σε τίποτα για να ακολουθηθεί. Τι πρέπει να κάνουμε για τον ακολουθήσουμε; Απλά αυτονόητα πράγματα όπως

· το να βάζουμε μέσα στα transactions που φτιάχνουμε αυτά που χρειαζόμαστε και όχι ότι να είναι π.χ. σε αρκετές περιπτώσεις κάποιοι υπολογισμοί που χρειάζονται χρόνο θα μπορούσαν να είναι εκτός αυτών, καθώς όσο λιγότερος είναι ο χρόνος που χρειάζεται αυτά να εκτελεστούν τόσο μικραίνει ο χρόνος που συντηρούνται locks από αυτά και έτσι μικραίνει και η πιθανότητα εμφάνισης του φαινομένου των deadlocks.

· το να έχουμε το transaction σε ένα batch μειώνει σημαντικά το ενδεχόμενο να έχω network roundtrips και πιθανά delays τα οποία κρατούν περισσότερο χρόνο τα locks (πόσο με τρελαίνουν τα ORMs που δεν το κάνουν σωστά αυτό δεν λέγεται).

3. Χρησιμοποιούμε όσο τον δυνατόν το μικρότερο isolation level.

Αυτό ίσως είναι ένας κανόνας που για να εφαρμοστεί απαιτεί σκέψη. Το να χρησιμοποιηθεί π.χ. read uncommitted σίγουρα δεν είναι καλό. Όμως καλό δεν είναι να γίνεται και ευρεία χρήση του serializable. Πρέπει να σκεφτούμε αρκετά καλά το τι χρειαζόμαστε και να βάλουμε στο μυαλό μας ότι το transaction που γράφουμε θα τρέξει σε ένα περιβάλλον που θα εκτελούνται και άλλα. Με βάση αυτή την πραγματικότητα, αλλά και το τι κάνει το συγκεκριμένο transaction σε συνάρτηση με την επιχειρηματική λογική θα μας βοηθήσει να επιλέξουμε το isolation level. Ναι είναι κομματάκι δύσκολο το ξέρω, το καταλαβαίνω αλλά αυτή είναι η βασική δουλειά ενός developer και όχι απλά να γράφει κώδικα για να ικανοποιήσει την ανάγκη αλλά να κάνει αυτό σε σχέση με το περιβάλλον που αυτό που γράφει θα εκτελεστεί. A, και για να μην το ξεχάσω το default στον SQL Server είναι Read Committed.

4. Δεν ανοίγουμε transactions που κλείνουν με επέμβαση χρηστών

Αυτό είναι κάτι το οποίο πραγματικά δεν μπορώ να καταλάβω γιατί αρκετοί δεν θέλουν να το καταλάβουν και να το εφαρμόσουν. Αν και καταλαβαίνω να το κάνουν συνάδελφοι της ηλικίας μου και μεγαλύτεροι, εντούτοις το κάνουν και νεότεροι διότι έχουν προϊστάμενους που είναι στις παραπάνω ηλικίες. Λοιπόν για να μην πλατειάζουμε κυρίες και κύριοι συνάδελφοι της ηλικία μου και πάνω με όλο το σεβασμό ΔΕΝ ΕΙΜΑΣΤΕ ΣΤΗΝ ΕΠΟΧΗ ΤΩΝ MAINFRAMES ΚΑΙ ΤΩΝ ISAM FILES, άρα δεν υπάρχει λόγος να σκεφτόμαστε όπως τότε, έχουν αλλάξει από τότε τα πράγματα. Δεν υπάρχει λόγος πλέον όταν π.χ. ανοίγουμε την καρτέλα του πελάτη να κλειδώνουμε το record του ώστε να διασφαλίσουμε ότι κανείς δεν θα πειράξει όσο το έχουμε εμείς. Αυτό είναι πλέον εξασφαλισμένο από τα σύγχρονα RDBMS. Σε αυτή την περίπτωση ανοίγουμε την καρτέλα με share locking και έτσι όλοι μπορούν να την δουν. Αν κάποιος θέλει να κάνει αλλαγές μπορεί να τις κάνει και όταν θα πατήσει το κουμπί της καταχώρησης τότε θα γίνουν τα απαραίτητα exclusive locks. Στο μεταξύ αν κάποιος άλλος προσπαθήσει να κάνει το ίδιο ταυτόχρονα θα περιμένει μέχρι ο πρώτος να τελειώσει ή εφόσον το application το επιτρέπει θα πάρει ένα μήνυμα το οποίο θα του λέει να προσπαθήσει ξανά. Αν αποφασίσει μετά από λίγο να προσπαθήσει τότε θα πάρει ένα μήνυμα λάθους που θα του λέει ότι από την ώρα που έχει διαβάσει το record μέχρι την στιγμή που προσπαθεί να αλλάξει το record αυτό έχει αλλάξει και θα πρέπει να ξαναδιαβάσει την εγγραφή και να κάνει τις αλλαγές του ξανά. Έτσι και το φαινόμενο του phantom update πιάνεται και εάν θέλουμε να μην βγαίνει αυτό μπορούμε να επιλέξουμε την λογική last win. Είναι στο χέρι μας να επιλέξουμε τι θέλουμε, αλλά σε καμία περίπτωση δεν πρέπει να κλειδώνουμε δεδομένα με το που τα διαβάζουμε. Αυτό πρέπει να σταματήσει. Ακόμα χειρότερα, γιατί υπάρχουν ακόμα χειρότερα παραδείγματα, δεν ανοίγουμε ένα transaction και λίγο πριν γίνει αυτό commit βγάζουμε ένα message box και ζητάμε από τον χρήστη να κάνει επιβεβαίωση αν αυτό θέλει να καταχωρηθεί και αυτός έχει φύγει από το γραφείο του για κάποιο λόγο. Σε αυτή όπως και την πρώτη περίπτωση έχουμε «ρίξει» locks που δεν χρειάζονται και το κακό είναι ότι αυτά μένουν ενεργά για αρκετή ώρα με αποτέλεσμα άλλες διαδικασίες να περιμένουν αυτά να τελειώσουν και εμείς να έχουμε βάλει το σπόρο για να γεννηθεί ένα ωραιότατο deadlock.

Επίλογος

Η αντιμετώπιση των deadlocks είναι βασική ευθύνη των developers. Οι DBAs που δεν έχουν εμπλακεί στον σχεδιασμό και την υλοποίηση της database είναι σχεδόν αδύνατον να δώσουν λύση στο φαινόμενο αυτό. Οι περιπτώσεις που οι DBAs μπορούν να δώσουν λύσεις είναι να είναι έμπειροι και με αρκετά καλή γνώση του SQL Server και της T-SQL, να γνωρίσουν άριστα το business logic και τις διαδικασίες που αποτυπώνονται στην database μέσω των transactions που γίνονται σε αυτή και αυτά να είναι γραμμένα μέσα σε stored procedures, καθώς αν είναι μέσα από τα applications και δεν έχουν στα χέρια τους τον source code για να επέμβουν. Σε όλες τις άλλες περιπτώσεις πρέπει να κυνηγήσουν τους developers.