Hay una pull request en php-src, la número 15603, que te habría dejado nombrar tu propio share persistente de curl con un ID arbitrario y mutarlo después mediante curl_share_setopt(). Fue rechazada. El reemplazo, la PR 16937, deriva el identificador de las propias opciones, devuelve un objeto inmutable y lanza un ValueError en cuanto le pides compartir cookies. Esa versión ganó la votación y se convirtió en curl_share_init_persistent() en PHP 8.5. Laurent Mn midió el resultado sobre una compilación propia de 8.5.6 y le salió un número precioso. Yo quiero defender que el número es la historia pequeña y que la pull request rechazada es la grande.
$share = curl_share_init_persistent([
CURL_LOCK_DATA_SSL_SESSION,
CURL_LOCK_DATA_CONNECT,
]);
$ch = curl_init('https://api.example.com/orders');
curl_setopt($ch, CURLOPT_SHARE, $share);
curl_exec($ch);Este es el mecanismo, porque no puedes juzgar el diseño sin él. La extensión curl mantiene una tabla hash llamada persistent_curlsh en sus globals de módulo. Esa tabla se construye en PHP_GINIT_FUNCTION y se destruye en PHP_GSHUTDOWN_FUNCTION, es decir, nace cuando arranca el worker y muere cuando muere el worker, y un ciclo normal de petición nunca la reinicia. Tu array de opciones se pliega en una máscara de cuatro bits: DNS en el bit 0, sesión SSL en el bit 1, la caché de conexiones en el bit 2, PSL en el bit 3. PHP busca esa máscara, te entrega un objeto envoltorio barato alrededor del CURLSH que encuentre, y solo construye uno nuevo si no hay coincidencia. Llamar a la función en cada petición no es una fuga, es el uso documentado.
Ahora fíjate en lo que compra ese esquema de identidad. No puedes colisionar con otra parte de tu propio código, porque dos llamantes que piden los mismos flags obtienen por definición el mismo share subyacente, y dos llamantes que piden flags distintos no pueden acabar por accidente en el mismo pool. No puedes filtrar la cookie de sesión de un visitante en la respuesta de otro, porque CURL_LOCK_DATA_COOKIE sencillamente no está en la lista aceptada junto a DNS, SSL_SESSION, CONNECT y PSL. No puedes reconfigurar un share del que otras tres peticiones ya tienen una referencia, porque el handle no admite setopt en absoluto. Cada una de esas garantías existe porque alguien en la lista de internals defendió antes la versión mutable y perdió.
El contraargumento honesto es que esto le cuesta algo a alguien. Hay equipos con una necesidad genuina de compartir un tarro de cookies entre llamadas salientes dentro de un worker, y para ellos la respuesta es un no rotundo sin puerta de escape, ni siquiera un flag de ini opcional. La inmutabilidad también significa que no puedes ajustar un share persistente de forma distinta a otro una vez creado. Si eres del tipo de equipo que lee el código C antes de desplegar, y el autor aquí hizo exactamente eso en ext/curl/share.c en lugar de fiarse de una página de manual recién escrita, la API mutable habría estado bien en tus manos. Pero las APIs de la biblioteca estándar no se escriben para ese equipo. Se escriben para el código donde alguien copia un fragmento de un blog dentro de un controlador a las 18:40 de un viernes, y en ese código un tarro de cookies compartido es un incidente de seguridad con final en forma de CVE.
Y eso me lleva a por qué desconfío de abrir con el benchmark. El resultado medido es un régimen estable en torno a un noveno de la línea base, un recorte del 88 al 89 por ciento, con el paso de handshake TLS cayendo a cero literal tras el calentamiento y una desviación estándar de 0.024 ms. Pero todo corrió sobre loopback contra un servidor TLS en Python en 127.0.0.1:8443, y el autor lo dice sin adornos. También tuvo que activar disable_nagle_algorithm para matar un artefacto de delayed ACK que había inflado una pasada anterior en 40 ms, lo que te indica con qué facilidad un microbenchmark mide la cosa equivocada. El porcentaje es una propiedad de ese montaje. Lo que generaliza es el mecanismo: la resolución DNS, la conexión TCP y la negociación TLS dejan de ocurrir en cada petición después de la primera, y en una red real eso vale muchos más milisegundos absolutos de los que el loopback puede enseñar.
Las advertencias operativas merecen el mismo cartel que la victoria. El alcance es por worker, así que cincuenta hijos de FPM significan cincuenta arranques en frío después de cada despliegue, cada uno pagado por la petición desafortunada que caiga en un proceso nuevo. La primera petición en modo persistente del test costó unos 1.9 ms, indistinguible de la línea base, exactamente lo que esperarías. Luego pm.max_requests recicla el proceso y lo vuelves a pagar, lo que convierte de la noche a la mañana un ajuste de higiene de memoria en un ajuste de rendimiento. Y CURLOPT_MAXCONNECTS cambia de significado: la caché de conexiones ahora sirve a toda la vida del worker en lugar de a una petición, así que un límite dimensionado para una sola petición te va a expulsar sockets que creías calientes. Si tu worker habla todo el día con dos o tres servicios internos, esto es casi dinero gratis. Si golpea cada vez un host distinto e impredecible, no hay nada que reutilizar ni nada que ganar.
Dos cosas menores de la misma release, conviene conocerlas antes de que alguien abra un informe de bug confuso. Comparar dos handles con === devuelve false incluso con flags idénticos, porque cada llamada acuña un objeto envoltorio nuevo alrededor del mismo recurso compartido, así que usa == o simplemente vuelve a llamar a la función y deja que la búsqueda haga su trabajo. Y curl_close() junto con curl_share_close() quedan obsoletas en 8.5, ahora que CurlHandle y CurlShareHandle se liberan solos por recolección de basura. Si estás en Symfony, el cableado es un decorador sobre http_client que empuja CURLOPT_SHARE dentro del saco extra.curl, soportado desde 6.3, y solo hace algo bajo CurlHttpClient. NativeHttpClient te ignorará con educación.
Así que esto es lo que de verdad quiero escuchar de ti. Internals sacrificó un caso de uso legítimo para hacer imposible un mal uso, y creo que acertó, pero sostengo esa postura desde la comodidad de no haber necesitado nunca un tarro de cookies compartido entre peticiones salientes. ¿Tú sí? Y algo más práctico: ¿sabes cuántos hosts distintos toca uno de tus workers a lo largo de su vida, o es un número que nadie de tu equipo ha sacado jamás de los logs?




Comentarios
Aún no hay comentarios — escribe el primero.
Inicia la conversación
Sin cuenta ni contraseña — introduce tu correo y te enviamos un enlace de acceso de un solo uso. ¿Primera vez? Todo se configura automáticamente.
Tu valoración se aplicará automáticamente al iniciar sesión.
Revisa tu bandeja de entrada
Hemos enviado un enlace de acceso a …. Ábrelo en este dispositivo — esta pestaña te conectará automáticamente.
¿No llega nada? Mira en la carpeta de spam — y marca el correo como «No es spam» para que la próxima vez llegue directo.