Le cœur d'OpenResty : cosocket
API7.ai
October 28, 2022
Aujourd'hui, nous allons apprendre la technologie centrale d'OpenResty : le cosocket.
Nous l'avons mentionné plusieurs fois dans les articles précédents, le cosocket est la base des diverses bibliothèques non bloquantes lua-resty-*
. Sans cosocket, les développeurs ne peuvent pas utiliser Lua pour se connecter rapidement à des services web externes.
Dans les versions antérieures d'OpenResty, si vous vouliez interagir avec des services comme Redis et memcached, vous deviez utiliser les modules C redis2-nginx-module
, redis-nginx-module
et memc-nginx-module
. Ces modules sont toujours disponibles dans la distribution d'OpenResty.
Cependant, avec l'ajout de la fonctionnalité cosocket, les modules C ont été remplacés par lua-resty-redis
et lua-resty-memcached
. Personne n'utilise plus les modules C pour se connecter à des services externes.
Qu'est-ce que le cosocket ?
Alors, qu'est-ce exactement que le cosocket ? Le cosocket est un terme propre à OpenResty. Le nom cosocket est composé de coroutine + socket
.
Le cosocket nécessite le support de la fonctionnalité de concurrence de Lua et le mécanisme d'événement fondamental dans NGINX, qui se combinent pour permettre des I/O réseau non bloquants. Le cosocket supporte également TCP, UDP et Unix Domain Socket.
L'implémentation interne ressemble au diagramme suivant si nous appelons une fonction liée au cosocket dans OpenResty.
J'ai également utilisé ce diagramme dans l'article précédent sur les principes et concepts de base d'OpenResty. Comme vous pouvez le voir dans le diagramme, pour chaque opération réseau déclenchée par le script Lua de l'utilisateur, il y aura à la fois le yield
et le resume
de la coroutine.
Lorsqu'il rencontre une I/O réseau, il enregistre l'événement réseau dans la liste des écouteurs NGINX et transfère le contrôle (yield) à NGINX. Lorsqu'un événement NGINX atteint la condition de déclenchement, il réveille la coroutine pour continuer le traitement (resume).
Le processus ci-dessus est le plan qu'OpenResty utilise pour encapsuler les opérations de connexion, d'envoi, de réception, etc., qui constituent les API cosocket que nous voyons aujourd'hui. Je vais utiliser l'API pour gérer TCP comme exemple. L'interface pour contrôler UDP et Unix Domain sockets est la même que celle de TCP.
Introduction aux API et commandes cosocket
Les API cosocket liées à TCP peuvent être divisées en catégories suivantes.
- Création d'objets :
ngx.socket.tcp
. - Définition du délai d'attente :
tcpsock:settimeout
ettcpsock:settimeouts
. - Établissement de la connexion :
tcpsock:connect
. - Envoi de données :
tcpsock:send
. - Réception de données :
tcpsock:receive
,tcpsock:receiveany
, ettcpsock:receiveuntil
. - Pool de connexions :
tcpsock:setkeepalive
. - Fermeture de la connexion :
tcpsock:close
.
Nous devons également porter une attention particulière aux contextes dans lesquels ces API peuvent être utilisées.
rewrite_by_lua*, access_by_lua*, content_by_lua*, ngx.timer.*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*_
Un autre point que je veux également souligner est qu'il existe de nombreux environnements indisponibles en raison de diverses limitations dans le noyau NGINX. Par exemple, l'API cosocket est indisponible dans set_by_lua*
, log_by_lua*
, header_filter_by_lua*
, et body_filter_by_lua*
. Elle est indisponible dans init_by_lua*
et init_worker_by_lua*
pour le moment, mais le noyau NGINX ne restreint pas ces deux phases, le support pour lesquelles peut être ajouté plus tard.
Il y a huit commandes NGINX commençant par lua_socket_
liées à ces API. Jetons-y un coup d'œil rapidement.
lua_socket_connect_timeout
: délai de connexion, par défaut 60 secondes.lua_socket_send_timeout
: délai d'envoi, par défaut 60 secondes.lua_socket_send_lowat
: seuil d'envoi (low water), par défaut 0.lua_socket_read_timeout
: délai de lecture, par défaut 60 secondes.lua_socket_buffer_size
: taille du tampon pour la lecture des données, par défaut 4k/8k.lua_socket_pool_size
: taille du pool de connexions, par défaut 30.lua_socket_keepalive_timeout
: temps d'inactivité de l'objet cosocket du pool de connexions, par défaut 60 secondes.lua_socket_log_errors
: indique si les erreurs cosocket doivent être enregistrées lorsqu'elles se produisent, par défauton
.
Ici, vous pouvez également voir que certaines commandes ont la même fonctionnalité que l'API, comme la définition du délai d'attente et la taille du pool de connexions. Cependant, s'il y a un conflit entre les deux, l'API a une priorité plus élevée que les commandes et remplacera la valeur définie par l'ordre. Donc, en général, nous recommandons d'utiliser les API pour faire les réglages, ce qui est également plus flexible.
Ensuite, regardons un exemple concret pour comprendre comment utiliser ces API cosocket. La fonction du code suivant est simple, qui envoie une requête TCP à un site web et imprime le contenu retourné :
$ resty -e 'local sock = ngx.socket.tcp()
sock:settimeout(1000) -- délai d'attente d'une seconde
local ok, err = sock:connect("api7.ai", 80)
if not ok then
ngx.say("échec de la connexion: ", err)
return
end
local req_data = "GET / HTTP/1.1\r\nHost: api7.ai\r\n\r\n"
local bytes, err = sock:send(req_data)
if err then
ngx.say("échec de l'envoi: ", err)
return
end
local data, err, partial = sock:receive()
if err then
ngx.say("échec de la réception: ", err)
return
end
sock:close()
ngx.say("la réponse est: ", data)'
Analysons ce code en détail.
- Tout d'abord, créez un objet TCP cosocket nommé
sock
en utilisantngx.socket.tcp()
. - Ensuite, utilisez
settimeout()
pour définir le délai d'attente à 1 seconde. Notez que le délai d'attente ici ne différencie pas entre la connexion et la réception ; c'est un réglage uniforme. - Ensuite, utilisez l'API
connect()
pour vous connecter au port 80 du site web spécifié et quittez en cas d'échec. - Si la connexion est réussie, utilisez
send()
pour envoyer les données construites et quittez en cas d'échec. - Si l'envoi des données est réussi, utilisez
receive()
pour recevoir les données du site web. Ici, le paramètre par défaut dereceive()
est*l
, ce qui signifie que seule la première ligne de données est retournée. Si le paramètre est défini sur*a
, il reçoit les données jusqu'à ce que la connexion soit fermée. - Enfin, appelez
close()
pour fermer activement la connexion socket.
Comme vous pouvez le voir, utiliser les API cosocket pour faire de la communication réseau est simple en quelques étapes. Faisons quelques ajustements pour explorer l'exemple plus en profondeur.
1. Définir le délai d'attente pour chacune des trois actions : connexion socket, envoi et lecture.
Le settimeout()
que nous avons utilisé pour définir le délai d'attente à une seule valeur. Pour définir le délai d'attente séparément, vous devez utiliser la fonction settimeouts()
, comme suit.
sock:settimeouts(1000, 2000, 3000)
Les paramètres de settimeouts
sont en millisecondes. Cette ligne de code indique un délai de connexion de 1
seconde, un délai d'envoi de 2
secondes et un délai de lecture de 3
secondes.
Dans OpenResty et les bibliothèques lua-resty, la plupart des paramètres des API liées au temps sont en millisecondes. Mais il y a des exceptions auxquelles vous devez faire attention lors de leur appel.
2. Recevoir le contenu de la taille spécifiée.
Comme je l'ai dit, l'API receive()
peut recevoir une ligne de données ou recevoir des données en continu. Cependant, si vous voulez uniquement recevoir des données de 10K de taille, comment devez-vous le configurer ?
C'est là que receiveany()
intervient. Il est conçu pour répondre à ce besoin, alors regardez la ligne de code suivante.
local data, err, partial = sock:receiveany(10240)
Ce code signifie que seulement jusqu'à 10K de données seront reçues.
Bien sûr, une autre exigence générale des utilisateurs pour receive()
est de continuer à récupérer des données jusqu'à ce qu'elle rencontre la chaîne spécifiée.
Le receiveuntil()
est conçu pour résoudre ce genre de problème. Au lieu de retourner une chaîne comme receive()
et receiveany()
, il retournera un itérateur. De cette façon, vous pouvez l'appeler en boucle pour lire les données correspondantes par segments et retourner nil lorsque la lecture est terminée. Voici un exemple.
local reader = sock:receiveuntil("\r\n")
while true do
local data, err, partial = reader(4)
if not data then
if err then
ngx.say("échec de la lecture du flux de données: ", err)
break
end
ngx.say("lecture terminée")
break
end
ngx.say("morceau lu: [", data, "]")
end
Le receiveuntil
retourne les données avant \r\n
et en lit quatre octets à la fois via l'itérateur.
3. Au lieu de fermer directement le socket, le mettre dans le pool de connexions.
Comme nous le savons, sans pool de connexions, une nouvelle connexion doit être créée, ce qui entraîne la création d'objets cosocket à chaque fois qu'une requête arrive et leur destruction fréquente, entraînant une perte de performance inutile.
Pour éviter ce problème, après avoir fini d'utiliser un cosocket, vous pouvez appeler setkeepalive()
pour le mettre dans le pool de connexions, comme suit.
local ok, err = sock:setkeepalive(2 * 1000, 100)
if not ok then
ngx.say("échec de la définition du réutilisable: ", err)
end
Ce code définit le temps d'inactivité de la connexion à 2
secondes et la taille du pool de connexions à 100
, de sorte que lorsque la fonction connect()
est appelée, l'objet cosocket sera d'abord récupéré du pool de connexions.
Cependant, il y a deux choses dont nous devons être conscients lors de l'utilisation du pool de connexions.
- Premièrement, vous ne pouvez pas mettre une connexion erronée dans le pool de connexions. Sinon, la prochaine fois que vous l'utiliserez, elle échouera à envoyer et recevoir des données. C'est l'une des raisons pour lesquelles nous devons déterminer si chaque appel d'API est réussi ou non.
- Deuxièmement, nous devons déterminer le nombre de connexions. Le pool de connexions est au niveau
Worker
, et chaque Worker a son propre pool de connexions. Si vous avez 10Worker
s et que la taille du pool de connexions est définie à30
, cela fait 300 connexions pour le service backend.
Résumé
Pour résumer, nous avons appris les concepts de base, les commandes associées et les API du cosocket. Un exemple pratique nous a familiarisés avec l'utilisation des API liées à TCP. L'utilisation d'UDP et d'Unix Domain Socket est similaire à celle de TCP. Vous pouvez facilement gérer toutes ces questions après avoir compris ce que nous avons appris aujourd'hui.
Nous savons que le cosocket est relativement facile à utiliser, et nous pouvons nous connecter à divers services externes en l'utilisant bien.
Enfin, nous pouvons réfléchir à deux questions.
La première question, dans l'exemple d'aujourd'hui, tcpsock:send
envoie une chaîne ; que faire si nous devons envoyer une table composée de chaînes ?
La deuxième question, comme vous pouvez le voir, le cosocket ne peut pas être utilisé dans de nombreuses étapes, alors pouvez-vous penser à des moyens de le contourner ?
N'hésitez pas à laisser un commentaire et à le partager avec moi. N'hésitez pas à partager cet article avec vos collègues et amis afin que nous puissions communiquer et progresser ensemble.