Comment déboguer le délai d'attente de verrouillage dépassé sur MySQL?


269

Dans mes journaux d'erreurs de production, je vois parfois:

SQLSTATE [HY000]: Erreur générale: 1205 Dépassement du délai d'attente de verrouillage; essayez de redémarrer la transaction

Je sais quelle requête essaie d'accéder à la base de données à ce moment, mais existe-t-il un moyen de savoir quelle requête avait le verrou à ce moment précis?


1
Je suggère fortement à tout le monde de donner une réponse à Eirik
kommradHomer

Réponses:


261

Ce qui donne cela, c'est le mot transaction . Il est évident par l'instruction que la requête tentait de modifier au moins une ligne dans une ou plusieurs tables InnoDB.

Puisque vous connaissez la requête, toutes les tables auxquelles vous accédez sont des candidats pour être le coupable.

De là, vous devriez pouvoir exécuter SHOW ENGINE INNODB STATUS\G

Vous devriez pouvoir voir les tables affectées

Vous obtenez toutes sortes d'informations supplémentaires sur le verrouillage et les mutex.

Voici un échantillon d'un de mes clients:

mysql> show engine innodb status\G
*************************** 1. row ***************************
  Type: InnoDB
  Name:
Status:
=====================================
110514 19:44:14 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 4 seconds
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 9014315, signal count 7805377
Mutex spin waits 0, rounds 11487096053, OS waits 7756855
RW-shared spins 722142, OS waits 211221; RW-excl spins 787046, OS waits 39353
------------------------
LATEST FOREIGN KEY ERROR
------------------------
110507 21:41:35 Transaction:
TRANSACTION 0 606162814, ACTIVE 0 sec, process no 29956, OS thread id 1223895360 updating or deleting, thread declared inside InnoDB 499
mysql tables in use 1, locked 1
14 lock struct(s), heap size 3024, 8 row lock(s), undo log entries 1
MySQL thread id 3686635, query id 124164167 10.64.89.145 viget updating
DELETE FROM file WHERE file_id in ('6dbafa39-7f00-0001-51f2-412a450be5cc' )
Foreign key constraint fails for table `backoffice`.`attachment`:
,
  CONSTRAINT `attachment_ibfk_2` FOREIGN KEY (`file_id`) REFERENCES `file` (`file_id`)
Trying to delete or update in parent table, in index `PRIMARY` tuple:
DATA TUPLE: 17 fields;
 0: len 36; hex 36646261666133392d376630302d303030312d353166322d343132613435306265356363; asc 6dbafa39-7f00-0001-51f2-412a450be5cc;; 1: len 6; hex 000024214f7e; asc   $!O~;; 2: len 7; hex 000000400217bc; asc    @   ;; 3: len 2; hex 03e9; asc   ;; 4: len 2; hex 03e8; asc   ;; 5: len 36; hex 65666635323863622d376630302d303030312d336632662d353239626433653361333032; asc eff528cb-7f00-0001-3f2f-529bd3e3a302;; 6: len 40; hex 36646234376337652d376630302d303030312d353166322d3431326132346664656366352e6d7033; asc 6db47c7e-7f00-0001-51f2-412a24fdecf5.mp3;; 7: len 21; hex 416e67656c73204e6f7720436f6e666572656e6365; asc Angels Now Conference;; 8: len 34; hex 416e67656c73204e6f7720436f6e666572656e6365204a756c7920392c2032303131; asc Angels Now Conference July 9, 2011;; 9: len 1; hex 80; asc  ;; 10: len 8; hex 8000124a5262bdf4; asc    JRb  ;; 11: len 8; hex 8000124a57669dc3; asc    JWf  ;; 12: SQL NULL; 13: len 5; hex 8000012200; asc    " ;; 14: len 1; hex 80; asc  ;; 15: len 2; hex 83e8; asc   ;; 16: len 4; hex 8000000a; asc     ;;

But in child table `backoffice`.`attachment`, in index `PRIMARY`, there is a record:
PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 30; hex 36646261666133392d376630302d303030312d353166322d343132613435; asc 6dbafa39-7f00-0001-51f2-412a45;...(truncated); 1: len 30; hex 38666164663561652d376630302d303030312d326436612d636164326361; asc 8fadf5ae-7f00-0001-2d6a-cad2ca;...(truncated); 2: len 6; hex 00002297b3ff; asc   "   ;; 3: len 7; hex 80000040070110; asc    @   ;; 4: len 2; hex 0000; asc   ;; 5: len 30; hex 416e67656c73204e6f7720436f6e666572656e636520446f63756d656e74; asc Angels Now Conference Document;;

------------
TRANSACTIONS
------------
Trx id counter 0 620783814
Purge done for trx's n:o < 0 620783800 undo n:o < 0 0
History list length 35
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 0 0, not started, process no 29956, OS thread id 1192212800
MySQL thread id 5341758, query id 189708501 127.0.0.1 lwdba
show innodb status
---TRANSACTION 0 620783788, not started, process no 29956, OS thread id 1196472640
MySQL thread id 5341773, query id 189708353 10.64.89.143 viget
---TRANSACTION 0 0, not started, process no 29956, OS thread id 1223895360
MySQL thread id 5341667, query id 189706152 10.64.89.145 viget
---TRANSACTION 0 0, not started, process no 29956, OS thread id 1227888960
MySQL thread id 5341556, query id 189699857 172.16.135.63 lwdba
---TRANSACTION 0 620781112, not started, process no 29956, OS thread id 1222297920
MySQL thread id 5341511, query id 189696265 10.64.89.143 viget
---TRANSACTION 0 620783736, not started, process no 29956, OS thread id 1229752640
MySQL thread id 5339005, query id 189707998 10.64.89.144 viget
---TRANSACTION 0 620783785, not started, process no 29956, OS thread id 1198602560
MySQL thread id 5337583, query id 189708349 10.64.89.145 viget
---TRANSACTION 0 620783469, not started, process no 29956, OS thread id 1224161600
MySQL thread id 5333500, query id 189708478 10.64.89.144 viget
---TRANSACTION 0 620781240, not started, process no 29956, OS thread id 1198336320
MySQL thread id 5324256, query id 189708493 10.64.89.145 viget
---TRANSACTION 0 617458223, not started, process no 29956, OS thread id 1195141440
MySQL thread id 736, query id 175038790 Has read all relay log; waiting for the slave I/O thread to update it
--------
FILE I/O
--------
I/O thread 0 state: waiting for i/o request (insert buffer thread)
I/O thread 1 state: waiting for i/o request (log thread)
I/O thread 2 state: waiting for i/o request (read thread)
I/O thread 3 state: waiting for i/o request (write thread)
Pending normal aio reads: 0, aio writes: 0,
 ibuf aio reads: 0, log i/o's: 0, sync i/o's: 0
Pending flushes (fsync) log: 0; buffer pool: 0
519878 OS file reads, 18962880 OS file writes, 13349046 OS fsyncs
0.00 reads/s, 0 avg bytes/read, 6.25 writes/s, 4.50 fsyncs/s
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 1190, seg size 1192,
174800 inserts, 174800 merged recs, 54439 merges
Hash table size 35401603, node heap has 35160 buffer(s)
0.50 hash searches/s, 11.75 non-hash searches/s
---
LOG
---
Log sequence number 28 1235093534
Log flushed up to   28 1235093534
Last checkpoint at  28 1235091275
0 pending log writes, 0 pending chkp writes
12262564 log i/o's done, 3.25 log i/o's/second
----------------------
BUFFER POOL AND MEMORY
----------------------
Total memory allocated 18909316674; in additional pool allocated 1048576
Dictionary memory allocated 2019632
Buffer pool size   1048576
Free buffers       175763
Database pages     837653
Modified db pages  6
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages read 770138, created 108485, written 7795318
0.00 reads/s, 0.00 creates/s, 4.25 writes/s
Buffer pool hit rate 1000 / 1000
--------------
ROW OPERATIONS
--------------
0 queries inside InnoDB, 0 queries in queue
1 read views open inside InnoDB
Main thread process no. 29956, id 1185823040, state: sleeping
Number of rows inserted 6453767, updated 4602534, deleted 3638793, read 388349505551
0.25 inserts/s, 1.25 updates/s, 0.00 deletes/s, 2.75 reads/s
----------------------------
END OF INNODB MONITOR OUTPUT
============================

1 row in set, 1 warning (0.00 sec)

Vous devriez envisager d'augmenter la valeur du délai d'attente de verrouillage pour InnoDB en définissant la valeur innodb_lock_wait_timeout , la valeur par défaut est 50 s

mysql> show variables like 'innodb_lock_wait_timeout';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 50    |
+--------------------------+-------+
1 row in set (0.01 sec)

Vous pouvez le régler sur une valeur plus élevée en /etc/my.cnfpermanence avec cette ligne

[mysqld]
innodb_lock_wait_timeout=120

et redémarrez mysql. Si vous ne pouvez pas redémarrer mysql à ce stade, exécutez ceci:

SET GLOBAL innodb_lock_wait_timeout = 120; 

Vous pouvez également le régler pour la durée de votre session

SET innodb_lock_wait_timeout = 120; 

suivi de votre requête


5
Pour InnoDB intégré, la innodb_lock_wait_timeoutvariable ne peut être définie qu'au démarrage du serveur. Pour le plug-in InnoDB, il peut être défini au démarrage ou modifié au moment de l'exécution, et a des valeurs globales et de session.
Timo Huovinen

1
Salut @rolandomysqldba, pouvez-vous s'il vous plaît me donner des suggestions sur mon article: stackoverflow.com/questions/18267565/…
Manish Sapkal

2
J'obtiens cette erreur lorsque j'essaie d'exécuter la première requête:SQL Error (1064): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '\G' at line 1
Iulian Onofrei

1
@Pacerier A chaque redémarrage de mysqld, vous devez relancer SET GLOBAL innodb_lock_wait_timeout = 120;. Si /etc/my.cnfa l'option, innodb_lock_wait_timeoutest réglé pour vous. Tout le monde n'a pas le privilège SUPER de le changer globalement pour tout le monde ( dev.mysql.com/doc/refman/5.6/en/… )
RolandoMySQLDBA

3
@IulianOnofrei le caractère \ G est une fonction spéciale de la ligne de commande MySQL et change la façon dont la sortie est affichée. Pour les autres clients MySQL, utilisez simplement un point-virgule normal.
thenickdude

83

Comme quelqu'un l'a mentionné dans l'un des nombreux threads SO concernant ce problème: Parfois, le processus qui a verrouillé la table apparaît comme dormant dans la liste des processus! J'arrachais mes cheveux jusqu'à ce que je tue tous les fils dormants qui étaient ouverts dans la base de données en question (aucun n'était actif à l'époque). Cela a finalement déverrouillé la table et laissé la requête de mise à jour s'exécuter.

Le commentateur a dit quelque chose de semblable à «Parfois, un thread MySQL verrouille une table, puis se met en veille pendant qu'il attend que quelque chose non lié à MySQL se produise.

Après avoir réexaminé le show engine innodb statusjournal (une fois que j'ai retrouvé le client responsable du verrou), j'ai remarqué que le fil coincé en question était répertorié tout en bas de la liste des transactions, sous les requêtes actives qui étaient sur le point de générer une erreur. à cause du verrou gelé:

------------------
---TRANSACTION 2744943820, ACTIVE 1154 sec(!!)
2 lock struct(s), heap size 376, 2 row lock(s), undo log entries 1
MySQL thread id 276558, OS thread handle 0x7f93762e7710, query id 59264109 [ip] [database] cleaning up
Trx read view will not see trx with id >= 2744943821, sees < 2744943821

(vous ne savez pas si le message "Trx read view" est lié au verrou gelé, mais contrairement aux autres transactions actives, celle-ci ne s'affiche pas avec la requête qui a été émise et prétend à la place que la transaction est en train de "nettoyer", mais a plusieurs verrous de ligne)

La morale de l'histoire est qu'une transaction peut être active même si le thread est en sommeil.


2
Je ne peux pas dire que vous m'avez sauvé la vie, mais vous vous êtes sûr de la paix. En lisant votre réponse, j'ai trouvé un fil de discussion effrayant qui est actif pendant 3260 secondes et ne s'affiche nulle part. après l'avoir tué, tous mes problèmes ont été résolus!
kommradHomer

C'était mon problème. Une transaction en sommeil avec un temps de 20 000 secondes qui empêchait le travail différé dans une application Rails de fonctionner correctement. Merci @Eirik
bigtex777

Avez-vous une idée de la raison pour laquelle une transaction en sommeil n'est pas tuée de toute façon? Par exemple, existe-t-il un délai d'expiration que vous pouvez définir pour qu'une transaction se termine?
patrickdavey

1
Autres commandes qui pourraient être utiles dans votre recherche de transactions de verrouillage: show processlist;pour afficher une liste exhaustive des processus en cours d'exécution, ce qui est bien car c'est une version condensée de show engine innodb status\g. De plus, si votre base de données se trouve sur une instance Amazon RDS, vous pouvez utiliser CALL mysql.rds_kill(<thread_id>);pour tuer les threads. Il a des autorisations plus élevées, je pense, car cela m'a permis de tuer plus de processus que plain kill <thread_id>;- notez que ceux-ci devraient être exécutés dans MySQL CLI
nickang

1
N'importe qui a une source pour cela - peut-être une page de documentation indiquant que les verrous sont placés avant la phase COMMIT? Je n'ai rien trouvé, malgré le fait d'avoir vu ce problème exact et qu'il a été résolu en tuant le fil endormi qui tenait des serrures.
Erin Schoonover

42

En raison de la popularité de MySQL, il n'est pas étonnant que le délai d'attente de verrouillage ait été dépassé; essayez de redémarrer la transaction exception de attire tellement l'attention sur SO.

Plus vous avez de conflits, plus les risques de blocages sont élevés, ce qu'un moteur de base de données résoudra en expirant l'une des transactions bloquées. En outre, les transactions de longue durée qui ont modifié (par exemple UPDATEou DELETE) un grand nombre d'entrées (qui prennent des verrous pour éviter les anomalies d'écriture incorrecte, comme expliqué dans la persistance Java haute performance livre de ) sont plus susceptibles de générer des conflits avec d'autres transactions.

Bien qu'InnoDB MVCC, vous pouvez toujours demander des verrous explicites à l'aide de la FOR UPDATEclause . Cependant, contrairement à d'autres bases de données populaires (Oracle, MSSQL, PostgreSQL, DB2), MySQL utilise REPEATABLE_READcomme niveau d'isolement par défaut .

Désormais, les verrous que vous avez acquis (en modifiant des lignes ou en utilisant un verrouillage explicite) sont conservés pendant la durée de la transaction en cours d'exécution. Si vous voulez une bonne explication de la différence entre REPEATABLE_READet READ COMMITTEDen ce qui concerne le verrouillage, veuillez lire cet article Percona .

Dans REPEATABLE READ, chaque verrou acquis pendant une transaction est conservé pendant la durée de la transaction.

Dans READ COMMITTED, les verrous qui ne correspondaient pas à l'analyse sont libérés une fois la DÉCLARATION terminée.

...

Cela signifie que dans READ COMMITTED, les autres transactions sont libres de mettre à jour des lignes qu'elles n'auraient pas pu mettre à jour (dans REPEATABLE READ) une fois l'instruction UPDATE terminée.

Par conséquent: plus le niveau d'isolement est restrictif ( REPEATABLE_READ,SERIALIZABLE ) est plus les chances de blocage sont grandes. Ce n'est pas un problème "en soi", c'est un compromis.

Vous pouvez obtenir de très bons résultats avec READ_COMMITED, car vous avez besoin d'une prévention des mises à jour perdues au niveau de l'application lorsque vous utilisez des transactions logiques qui s'étendent sur plusieurs requêtes HTTP. L' approche de verrouillage optimiste cible les mises à jour perdues qui peuvent se produire même si vous utilisez le SERIALIZABLEniveau d'isolement tout en réduisant le conflit de verrouillage en vous permettant d'utiliser READ_COMMITED.


4
Le délai d'attente du verrouillage n'est-il pas différent du blocage? Par exemple, si un thread détient un verrou pendant 60 secondes pour des raisons légitimes, le délai d'attente du verrouillage peut se produire. N'est-il pas vrai que s'il y a vraiment un blocage MySQL détectera cela et tuera une transaction instantanément et ce n'est pas lié au délai d'attente du verrouillage?
ColinM

1
Vous avez raison. La base de données détecte le blocage mort après le délai d'expiration et tue un processus en attente, donc une transaction gagne tandis que l'autre échoue. Mais plus vous maintenez un verrou, moins l'application est évolutive. Même si vous ne rencontrez pas de verrous mortels, vous augmenterez toujours la partie sérialisable du comportement d'exécution de votre application.
Vlad Mihalcea

19

Pour mémoire, l'exception de délai d'attente de verrouillage se produit également lorsqu'il y a un blocage et que MySQL ne peut pas le détecter, il expire donc simplement. Une autre raison pourrait être une requête extrêmement longue, qui est cependant plus facile à résoudre / réparer, et je ne décrirai pas ce cas ici.

MySQL est généralement capable de gérer les blocages s'ils sont construits "correctement" en deux transactions. MySQL tue / annule ensuite la seule transaction qui possède moins de verrous (est moins importante car elle affectera moins de lignes) et laisse l'autre terminer.

Supposons maintenant qu'il existe deux processus A et B et 3 transactions:

Process A Transaction 1: Locks X
Process B Transaction 2: Locks Y
Process A Transaction 3: Needs Y => Waits for Y
Process B Transaction 2: Needs X => Waits for X
Process A Transaction 1: Waits for Transaction 3 to finish

(see the last two paragraph below to specify the terms in more detail)

=> deadlock 

C'est une configuration très malheureuse car MySQL ne peut pas voir qu'il y a un blocage (réparti sur 3 transactions). Donc ce que MySQL fait est ... rien! Il attend juste, car il ne sait pas quoi faire. Il attend que le premier verrou acquis dépasse le délai d'expiration (Traiter une transaction 1: verrouille X), puis cela débloquera le verrou X, ce qui déverrouille la transaction 2, etc.

L'art est de savoir ce qui (quelle requête) provoque le premier verrou (Lock X). Vous pourrez voir facilement ( show engine innodb status) que la transaction 3 attend la transaction 2, mais vous ne verrez pas quelle transaction la transaction 2 attend (transaction 1). MySQL n'imprimera aucun verrou ou requête associé à la transaction 1. Le seul indice sera que tout en bas de la liste des transactions (dushow engine innodb status impression), vous verrez la transaction 1 apparemment ne rien faire (mais en fait, attendre que la transaction 3 soit terminée). terminer).

La technique pour savoir quelle requête SQL entraîne l'octroi du verrou (Lock X) pour une transaction en attente est décrite ici Tracking MySQL query history in long running transactions

Si vous vous demandez quel est le processus et la transaction exactement dans l'exemple. Le processus est un processus PHP. La transaction est une transaction telle que définie par innodb-trx-table . Dans mon cas, j'avais deux processus PHP, dans chacun j'ai commencé une transaction manuellement. La partie intéressante était que même si j'ai commencé une transaction dans un processus, MySQL utilisait en interne deux transactions distinctes (je n'ai aucune idée pourquoi, peut-être que certains développeurs MySQL peuvent l'expliquer).

MySQL gère ses propres transactions en interne et a décidé (dans mon cas) d'utiliser deux transactions pour gérer toutes les requêtes SQL provenant du processus PHP (Process A). La déclaration selon laquelle la transaction 1 attend la fin de la transaction 3 est une chose interne à MySQL. MySQL "savait" que la Transaction 1 et la Transaction 3 étaient en fait instanciées dans le cadre d'une demande de "transaction" (du Processus A). Maintenant, la "transaction" entière a été bloquée car la transaction 3 (une sous-partie de "transaction") a été bloquée. Parce que "transaction" n'a pas pu terminer la transaction 1 (également une sous-partie de la "transaction") a également été marquée comme non terminée. C'est ce que je voulais dire par "Transaction 1 attend la fin de la Transaction 3".


14

Le gros problème avec cette exception est que ce n'est généralement pas reproductible dans un environnement de test et nous ne sommes pas là pour exécuter le statut du moteur innodb quand cela se produit sur prod. Donc, dans l'un des projets, j'ai mis le code ci-dessous dans un bloc catch pour cette exception. Cela m'a aidé à saisir l'état du moteur lorsque l'exception s'est produite. Cela a beaucoup aidé.

Statement st = con.createStatement();
ResultSet rs =  st.executeQuery("SHOW ENGINE INNODB STATUS");
while(rs.next()){
    log.info(rs.getString(1));
    log.info(rs.getString(2));
    log.info(rs.getString(3));
}

11

Jetez un œil à la page de manuel de l' pt-deadlock-loggerutilitaire :

brew install percona-toolkit
pt-deadlock-logger --ask-pass server_name

Il extrait des informations de ce qui engine innodb statusprécède et peut également être utilisé pour créer un daemonqui s'exécute toutes les 30 secondes.


3
cet outil fait maintenant partie de la boîte à outils Percona
Brad Mace

Les délais d'attente de verrouillage ne sont pas les mêmes que les interblocages, en particulier innodb n'affiche aucune information à leur sujet car ils ne sont pas détectés, donc je ne pense pas que pt-deadlock-logger soit une aide.
Jay Paroline


11

Extrapolant de la réponse de Rolando ci-dessus, ce sont eux qui bloquent votre requête:

---TRANSACTION 0 620783788, not started, process no 29956, OS thread id 1196472640
MySQL thread id 5341773, query id 189708353 10.64.89.143 viget

Si vous devez exécuter votre requête et ne pouvez pas attendre que les autres s'exécutent, tuez-les en utilisant l'ID de thread MySQL:

kill 5341773 <replace with your thread id>

(depuis mysql, pas le shell, évidemment)

Vous devez trouver les ID de thread à partir de:

show engine innodb status\G

et déterminez lequel est celui qui bloque la base de données.


1
Comment savez-vous que c'est 5341773? Je ne vois pas ce qui le distingue des autres.
Wodin

Non ce n'est probablement pas ce threadID, c'était un exemple. Vous devez trouver les ID de thread à partir de la commande «show engine innodb status \ G» et déterminer lequel est celui qui bloque la base de données.
Ellert van Koperen

1
Merci. En d'autres termes, il n'y a aucun moyen de dire lequel c'est sans par exemple les tuer un par un?
Wodin

Dans la liste des transactions, vous pouvez voir lesquelles sont en cours d'exécution et pour combien de temps. Donc pas besoin de les tuer un par un, cette liste vous donne généralement une assez bonne idée de ce qui se passe.
Ellert van Koperen

10

Voici ce que j'ai finalement dû faire pour déterminer quelle «autre requête» a causé le problème de délai d'attente de verrouillage. Dans le code d'application, nous suivons tous les appels de base de données en attente sur un thread distinct dédié à cette tâche. Si un appel DB prend plus de N secondes (pour nous, c'est 30 secondes), nous enregistrons:

-- Pending InnoDB transactions
SELECT * FROM information_schema.innodb_trx ORDER BY trx_started; 

-- Optionally, log what transaction holds what locks
SELECT * FROM information_schema.innodb_locks;

Avec ce qui précède, nous avons pu identifier les requêtes simultanées qui ont verrouillé les lignes à l'origine de l'impasse. Dans mon cas, c'étaient des déclarations commeINSERT ... SELECT qui, contrairement aux SELECT simples, verrouillent les lignes sous-jacentes. Vous pouvez ensuite réorganiser le code ou utiliser une isolation de transaction différente comme la lecture non validée.

Bonne chance!


9

Vous pouvez utiliser:

show full processlist

qui listera toutes les connexions dans MySQL et l'état actuel de la connexion ainsi que la requête en cours d'exécution. Il existe également une variante plus courte show processlist;qui affiche la requête tronquée ainsi que les statistiques de connexion.



-2

Activez MySQL general.log (disque) et utilisez mysql_analyse_general_log.pl pour extraire les transactions de longue durée, par exemple avec:

--min-duration = votre valeur innodb_lock_wait_timeout

Désactivez general.log après cela.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.