Utilisation de sémaphores ou de flock en mutualisé
BMPCreated with Sketch.BMPZIPCreated with Sketch.ZIPXLSCreated with Sketch.XLSTXTCreated with Sketch.TXTPPTCreated with Sketch.PPTPNGCreated with Sketch.PNGPDFCreated with Sketch.PDFJPGCreated with Sketch.JPGGIFCreated with Sketch.GIFDOCCreated with Sketch.DOC Error Created with Sketch.
question

Utilisation de sémaphores ou de flock en mutualisé

Par
LouisD
Créé le 2016-10-15 08:48:06 (edited on 2024-09-04 11:50:38) dans Hébergements Web

Bonjour,

je veux utiliser une sémaphore pour protéger une partie critique de la concurrence sur un serveur mutualisé KimSufi.
J'ai d'abord utilisé les sémaphores proposées par PHP, ça marchait bien, mais parfois (peut-être parce-qu'elles sont partagées par tous les sites du serveur mutualisé) je me retrouve avec l'erreur suivante lorsque j'appelle `sem_get()` :
> Warning : sem_get() : failed for key [mon id] : no space left on device in [mon fichier php].

J'ai donc implémenté ma propre sémaphore en utilisant un fichier et la fonction "flock" qui permet d'implémenter un verrou exclusif **bloquant** comme dans le code ci-dessous.
Le code ci-dessous est un test que je peux lancer dans deux onglets simultanément. La sémaphore devrait permettre que le code entre `semAcquire()` et `semRelease()` s'exécute atomiquement. Ainsi, la première requête devrait compter de 1 à 100, et la deuxième requête devrait attendre que la première se termine puis compter de 101 à 200. Ce test fonctionne très bien sur Wamp Server mais ne fonctionne pas sur le serveur OVH :
Visiblement, la fonction `flock` retourne `true` directement même si un verrou a déjà été demandé.. Quelqu'un sait si c'est OVH qui n'implémente pas `flock` ? Ou est-ce un problème de configuration ? Qu'est-ce que je peux faire sinon ? Merci pour votre aide ! :)


// Mes propres sémaphores puisque les sémaphores php ne marchent pas très bien en mutualisé...
function semAcquire() {
$sem = fopen("semaphore.txt", "w");
$fl = flock($sem, LOCK_EX);
echo $fl;
if($fl) {
fwrite($sem, getmypid());
} else {
die("Semaphore acquire error.");
}
return $sem;
}

function semRelease($sem) {
if(!flock($sem, LOCK_UN)) {
die("Semaphore release error.");
}
fclose($sem);
}

$sem = semAcquire();
for($j=0; $j < 100; $j++) {
for($i = 0; $i < 1000000; $i++) {

}
$n = file_get_contents("sem_test.txt");
$n++;
file_put_contents("sem_test.txt", $n);
echo "Compteur : $n
";
}
semRelease($sem);

echo "Fin!";

?>

Louis Durand


27 réponses ( Latest reply on 2020-03-28 15:21:44 Par
ElsV
)

Quelle version de PHP utilises-tu ?

Version 5.6 !

Ce qui me choque, c'est que dans la function semAcquire :
* Tu fais un **_fopen_**
* Mais tu oublie le **_fclose_**

De même dans la function semRelease je mettrais d'abord un **_fopen_**

Non fclose est appelé dans `semRelease`, car je veux garder le verrou sur le fichier jusqu'à l'appel de `semRelease` !

Ah.. Je peux essayer effectivement de mettre un fclose semAcquire, puis un fopen dans semRelease, pourquoi pas. Je vais tester ça. (Pourtant ça marche tel quel sur Wamp.)

Autant je me suis trompé dans la lecture de tes fonctions. :(
Ce que tu avais fait semble correct.

Pourquoi utilises-tu **_sem_test.txt_** comme fichier compteur et non **_semaphore.txt_** ?

De plus, rien n'empêche le 2ème script lancé de démarrer en même temps et d'écrire dans le fichier **_sem_test.txt_**

Bon alors j'ai fait les tests suivants :
- Switch sur php 7.0 : pareil
- Ajout d'un fclose dans semAcquire et fopen dans semRelease : pareil (ça marche sur wamp, mais ça ne marche pas sur le serveur)
:frowning:

`semaphore.txt` est mon fichier utilisé pour la sémaphore, celui sur lequel je mets le verrou.

`sem_test.txt` est juste un fichier utilisé dans le cadre du test, ici pour faire office de mémoire partagée entre les process. Comme ça on peut voir s'il est modifié par plusieurs process en cas de concurrence.


De plus, rien n'empêche le 2ème script lancé de démarrer en même temps et d'écrire dans le fichier semtest.txt_


Si justement : la sémaphore !
Si les deux scripts sont lancés à peu près simultanément, le premier qui appelle `flock` obtiendra le verrou sur le fichier `semaphore.txt`. Ensuite, le deuxième process, lorsqu'il va appeler flock, il sera **bloqué** tant que le verrou n'est pas levé, c'est-à-dire, tant que le premier process n'aura pas appelé semRelease, à la fin des 100 tours de boucles. Là, le second process peut mettre son verrou sur flock et entamer la partie critique.


$sem = semAcquire();

Il n'y a aucune analyse du retour de cet appel.

Le processus "for($j=0; $j < 100; $j++) {" peut s'exécuter en toute liberté.

flock est **bloquant** donc le deuxième processus va rester bloqué à la ligne `$fl = flock($sem, LOCK_EX);` jusqu'à ce que le verrou soit levé !
http://php.net/manual/fr/function.flock.php

Le code fonctionne, je l'ai testé sur Wamp Server, ça marche.

En fait ma question porte davantage sur :
Est-ce que flock fonctionne chez ovh, sur un mutualisé avec l'offre KimSufi ? Si non, pourquoi ? Si oui, comment le faire marcher chez moi ! Ou comment implémenter des sémaphores autrement.. ?

En tous cas merci pour votre aide !


flock est bloquant donc le deuxième processus va rester bloqué à la ligne $fl = flock($sem, LOCK_EX); jusqu'à ce que le verrou soit levé !
http://php.net/manual/fr/function.flock.php




C'est donc ce que cela veut dire ?

Sur quel cluster es-tu ?

De plus, en mutualisé, je ne suis pas sûr que deux lancements de scripts travaillent sur le même espace.

As-tu activé le CDN ?

Personnellement, je mettrais le FLAG dans une table SQL.

Oui c'est ça ! Si on met l'option LOCK_NB (NB pour Non Blocking j'imagine, alors la fonction ne bloque pas et renvoie juste false je crois, mais c'est le fait que la fonction soit bloquante qui m'intéresse justement !)

cluster017 visiblement.

CDN ? Visiblement non, "Options CDN" affiche "Non" dans le manager d'ovh.

Mmh intéressant, comment ferais-tu pour ta solution utilisant un FLAG avec SQL ?
Note que je ne peux pas utiliser juste des transactions SQL car dans ma partie critique, j'ai des requêtes SQL et des modifications de fichier, donc le système de sémaphore était le plus simple ET efficace :)

Je suis d'accord avec ce que tu développes.

Pour le FLAG SQL :
Je créerai une table SQL avec deux champs : Id et FLAG.
* Le FLAG à 1 indiquerai un blocage d'opération pour un traitement donné.
* Le FLAG à 0 laisserai libre les opérations.

Ceci dit, j'essaierai d'abord avec un simple fichier qui contientrait ce FLAG.

Mmh oui mais comment reproduire cet effet "bloquant" où le deuxième script attend que le premier soit terminé et reprend son exécution ?
On pourrait faire un while qui vérifie l'état du flag en permanence mais ça me paraît lourd. Il me semble que l'implémentation sous-jacente à une fonction bloquante comme `flock` ou `sem_acquire` met le processus en pause et le reprend lorsque la ressource est libérée. (Du moins c'est ainsi que devraient être implémentées ce genre de fonctions)

Toujours d'accord avec toi, mais quand le **_cas NORMAL ne fonctionne pas_** on prend une rustine (Workaround, Fix). :)

Haha effectivement, je pense que ce sera ma solution si je ne trouve pas une manière d'utiliser les sémaphores natives de php ou flock, mais c'est très frustrant d'avoir DEUX solutions propres et qu'aucune ne fonctionne pour aucune raison valable.. (Les sémaphores natives, à la rigueur, je peux comprendre sur un mutualisé, mais flock ???) En tous cas j'ai rien trouvé sur le net à propos de flock qui ne fonctionne pas. Donc c'est bizarre. Et je pense être en droit d'attendre d'OVH que les fonctions php fonctionnent. Donc je vais d'abord m'entretenir un peu avec l'assistance, et sinon, j'utiliserai un flag dans un while.

Merci pour ton aide !! Si tu trouves d'autres pistes n'hésite pas à partager ça ici bien sûr :)

Malheureusement, à part les **_RUSTINES_**, je n'ai rien d'autre en rayon en ce moment. :D

Bonjour,

Je n'ai pas testé votre cas, mais vu que nous sommes sur du mutualisé, ce que je vois qu'il se passe :

- Accès au script via un host (disons : webm210), pose de votre "semaphore". Une semaphore est posée par le kernel en mémoire.
- Accès à nouveau au script via un autre host (disons: webm0300), pas de semaphore en mémoire sur ce host.

Une solution serait de jouer avec la présence ou non du fichier.

- Le fichier n'existe pas, le créer, et continuer. Ne pas oublier de supprimer le fichier une fois l'operation faite.
- Le fichier existe, on est lock, on attend ou on quitte.


Je n'ai pas testé votre cas, mais vu que nous sommes sur du mutualisé, ce que je vois qu'il se passe :

Accès au script via un host (disons : webm210), pose de votre "semaphore". Une semaphore est posée par le kernel en mémoire.
Accès à nouveau au script via un autre host (disons: webm0300), pas de semaphore en mémoire sur ce host.


C'est donc ce que je pressentais, mais que j'ai mal exprimé.

De plus, en mutualisé, je ne suis pas sûr que deux lancements de scripts travaillent sur le même espace.

Bonjour,
merci pour votre réponse. (Rah je ne pouvais pas répondre car j'avais trop posté pour mon premier jour !)
Notez que pour la sémaphore, au début ça marchait bien, et à un moment donnée, ça m'a sorti l'erreur comme quoi il n'y a plus de mémoire. J'ai essayé de trouver un autre ID qui soit libre et ça a marché à nouveau avec cet ID pour un certain temps, puis même problème.

Je veux bien mais pour votre solution, comment implémentez-vous le fait qu'on "attend" si le fichier existe ? Le fait que "flock" soit bloquant est la propriété que je recherche.

@ LouisD, comme la précisé Ludovic d'OVH, le système d'hébergement mutualisé OVH est différent du cas d'un serveur dédié qui a un environnement unique.

D'après ce que cru comprendre, il y a plusieurs serveurs "Apache" webm... qui gèrent les requêtes entrantes.
Le "flock" ne serait pas "accroché" au fichier sur le cluster xxx, mais conservé en mémoire de ce webm... .
* Si tes DEUX requêtes arrivent sur le même webm... le système de "flock" fonctionnera bien.
* Si tes DEUX requêtes arrivent sur des webm... différents le système de "flock" ne fonctionnera plus du tout, car les informations "flock" se trouvera sur l'autre webm... .

Mmh d'accord, c'est probablement ça.. Donc pas moyen de faire fonctionner flock en mutualisé de manière fiable j'imagine ?
Je vais probablement implémenter qqch moi-même qui va boucler tant que la ressource n'est pas libérée mais bon.. :/
J'aurais bien aimé savoir comment @Ludo.H aurait fait pour "attendre" ?


Le fichier existe, on est lock, on attend ou on quitte.

Bon, j'ai donc implémenté ma propre sémaphore avec un fichier comme ceci.
(Notez qu'elle ne correspond pas à la définition classique d'une sémaphore en interne car ici 0 signifie "libre", 1 signifie "locked", et 2 signifie "invalid", permettant un petit système de récupération d'erreur "à l'arrache".)

Version de base toute simple pour le principe : (il faut un fichier semaphore.txt initialisé à 0)

function semAcquire() {
for($i = 0; intval(file_get_contents("semaphore.txt")); $i++) {
usleep(8000+rand(0,2000)); // Pour éviter deux lectures simultanées de semaphore.txt
}
file_put_contents("semaphore.txt", 1);
}

function semRelease() {
file_put_contents("semaphore.txt", 0);
}

Version avec TimeOut et error recovery (dans le cas où, par erreur ou je ne sais quelle raison, la sémaphore ne serait pas relâchée).

// Mes propres sémaphores puisque les sémaphores php ne marchent pas très bien en mutualisé...
define("SEM_MAX_WAIT", 600);

function semAcquire() {
global $db;
if(intval(file_get_contents("semaphore.txt"))==2) { // Recovery
file_put_contents("semaphore.txt", 0);
$db->log("Semaphore recovery.\n", 0);
}
for($i = 0; intval(file_get_contents("semaphore.txt")) && $i usleep(8000+rand(0,2000)); // Pour éviter deux lectures simultanées de semaphore.txt
}
if($i==SEM_MAX_WAIT) {
file_put_contents("semaphore.txt", 2);
$db->log("semAcquire timeout!\n", 0);
die("Couldn't acquire semaphore.");
}
file_put_contents("semaphore.txt", 1);
}

function semRelease() {
file_put_contents("semaphore.txt", 0);
}

L'utilisation recommandée est la suivante :

semAcquire();
try {
// Partie critique
} catch(Exception $e) {
// Log le message qq part peut être utile
}
semRelease();

Ainsi, théoriquement, même si le script plante, on relâche la sémaphore, mais j'ai quand même un petit système de recovery.

Le principe de semAcquire est le suivant :
En temps normal, semaphore.txt contient 0.
- Un process vient, lit la semaphore, elle est à 0 donc il l'a met à 1 et entre la partie critique.
- Un deuxième process vient, lit la sémaphore qui est à 1 et donc boucle jusqu'à ce que : soit elle soit relâchée (remise à 0) soit jusqu'à ce que SEM_MAX_WAIT itérations soit atteint.

=> Si on timeout, on met la sémaphore à 2 et on quitte.
L'idée est que si on a timeout à cause d'un burst de requêtes simultanées, ça ne mette pas en péril la partie critique car la sémaphore étant à 2 != 0, les process en attente continuent d'attendre, et le prochain à entrer la partie critique va la remettre à 1.

Par contre si la sémaphore est "cassée" parce-qu'elle n'a jamais été relâchée :
Un premier process entre et attend jusqu'à timeout, et met la sémaphore à 2.
La prochaine requête remettra la sémaphore à 0 et tout rentre dans l'ordre.

Alors évidemment, c'est POSSIBLE de mettre à mal le système mais pour ça il faut :
Un process en cours (sem=1) et au moins un autre qui attend.
Un process qui attend timeout
Une nouvelle requête arrive AVANT que le process en cours ne termine.
=> Dans ce cas, la partie critique sera entrée alors que le premier process y est encore.

M'enfin, dans mon cas, c'est assez improbable il me semble.. (Je sens la loi de Murphy qui va me tomber dessus maintenant hihi)

Bon, voilà, en tous cas ça semble très bien fonctionner, le truc qui me dérangeait par rapport à flock ou à sem_acquire de php, est que là, les process en attente ne sont pas mis en pause, ils travaillent quand même ce qui potentiellement ralentit le process dans la partie critique et donc augmente l'effet de bottleneck dû à ma partie critique.. Mais en fait, puisque j'utilise usleep (j'y avais pas pensé au début, et en fait c'est essentiel sinon plusieurs process en attente risquent de lire en même temps que la sémaphore est relâchée et peuvent ainsi entrer simultanément la partie critique !), ça revient à les mettre en pause pour la plupart du temps au final !
Merci pour l'aide !

Astucieux l'état 2 et le time-out. :P

Cette solution ne fonctionne pas dans tous les cas.
Elle n'évite pas que un fil ouvre le fichier et lit la valeur 0 puis que un autre fil fasse la même chose avant que la valeur 1 ne soit écrite.
Voir une solution avec mkdir() ici: https://stackoverflow.com/questions/51155848/php-flock-for-read-modify-write-does-not-work