Fet per continuar funcionant: alta disponibilitat per a un WordPress Multisite de 15.000 usuaris
Vam traslladar un WordPress Multisite de 15.000 usuaris i amb molta escriptura des d’un servei de WP gestionat a un stack d’alta disponibilitat a AWS que operem de cap a cap. El servei gestionat era més barat, i encara ho és, però executava la plataforma com un únic entorn: quan una peça fallava, el lloc queia. A AWS cada capa s’executa en almenys dues zones de disponibilitat, de manera que perdre un servidor, la instància principal de la base de dades o una zona sencera ja no deixa la plataforma fora de línia.
| Servei de WP gestionat | Alta disponibilitat a AWS | |
|---|---|---|
| Topologia | Entorn únic | Cada capa en dues o més zones de disponibilitat |
| Fallada del servidor web | Caiguda fins a la recuperació | Substituït automàticament, sense downtime |
| Fallada de la base de dades | Caiguda fins a la restauració | Es promou una rèplica, breu pausa |
| Fallada d’una zona | Caiguda | L’altra zona continua servint |
| Pegats | Finestres de manteniment | Progressius, sense finestra de manteniment |
| Pics de trànsit | Limitats pel pla | Auto Scaling i rèpliques de lectura |
| Cost mensual | Més baix | Més alt |
Context
La plataforma és un WordPress Multisite en subdirectoris amb uns 15.000 usuaris repartits en uns 40 subllocs. Té molta escriptura: la major part del trànsit són persones autenticades que publiquen, editen i pugen fitxers, així que la memòria cau de pàgina completa ajuda poc i gairebé totes les peticions d’administració arriben a PHP i a la base de dades.
El servei de WP gestionat la feia funcionar bé dia a dia, i pel preu era difícil de superar. El problema era la forma de la configuració: un sol entorn, sense failover automàtic. Un servidor caigut, un problema a la base de dades o la caiguda d’una zona volien dir que el lloc quedava fora de servei fins que el proveïdor el restaurava, i aplicar pegats volia dir finestres de manteniment.
Per a una plataforma on milers de persones publiquen cada dia, cada hora de caiguda és feina perduda, confiança perduda i una cua de suport plena. Vam decidir que la resiliència valia la pena pagar-la, i que la volíem tenir a les nostres mans: un únic stack d’alta disponibilitat, definit a Terraform, que operem i podem explicar de cap a cap.
Restriccions
- Cap punt únic de fallada. Cada capa ha de sobreviure a la pèrdua d’una instància o d’una zona de disponibilitat sense que hi hagi d’intervenir ningú.
- Cap aturada planificada després de la migració. Els desplegaments, els pegats del sistema operatiu i el manteniment de la base de dades es fan mentre el lloc està servint.
- Cap pèrdua de dades. Cada entrada, usuari i fitxer pujat de cada sublloc havia d’arribar intacte, inclòs un historial de mitjans de fa anys.
- Una migració curta i planificada. Una sola finestra de manteniment de quatre hores un dissabte a la nit, amb una via de rollback cap al servei de WP gestionat fins que el DNS s’estabilitzés.
- Sense SSH ni claus de llarga durada als hosts. Tot l’accés per shell passa per SSM Session Manager i queda registrat.
- Tot a Terraform. Si no és al codi, no es pot reconstruir en una altra zona.
- Canvis mínims al mateix WordPress. L’aplicació només va incorporar el que la infraestructura necessitava: WP Offload Media, un drop-in de memòria cau d’objectes per a Redis i un drop-in de base de dades per separar lectures i escriptures.
Què vol dir aquí alta disponibilitat
L’alta disponibilitat no és una sola funcionalitat. És la resposta a una llista de fallades concretes, així que primer vam escriure aquesta llista i vam dissenyar cada capa en funció d’ella.
En un entorn únic, qualsevol fallada d’un servidor o de la base de dades es converteix en una caiguda, per bo que sigui el proveïdor. La fallada d’una zona és poc habitual però real, i fa caure de cop tot el que hi ha en aquella zona. Tot el que hi ha a l’arquitectura següent existeix per convertir aquestes files en «sense downtime».
Arquitectura
Uns servidors web sense estat se situen entre un edge compartit i un estat compartit, i cada capa amb estat manté una segona còpia en una altra zona de disponibilitat. Qualsevol instància pot morir, i la següent arrenca des del mateix launch template, munta el mateix codi i parla amb les mateixes dades.
- Edge: CloudFront, que no té cap zona única que es pugui perdre, amb dos orígens: l’ALB per a les pàgines dinàmiques i wp-admin, i S3 per als mitjans descarregats. Una CloudFront Function reescriu els camins del Multisite a l’entrada.
- Balancejador de càrrega: un ALB que abasta diverses zones de disponibilitat, comprova la salut de cada instància i està protegit per AWS WAF.
- Capa web: un Auto Scaling group d’instàncies Apache + PHP repartides entre zones. Una instància que no supera la comprovació de salut se substitueix automàticament. Sense key pair ni port 22: l’accés per shell és només per SSM Session Manager.
- Codi: el nucli de WordPress, els temes i els plugins són a EFS, que emmagatzema les dades entre zones i es munta a través d’un access point, de manera que totes les instàncies serveixen la mateixa versió.
- Fitxers pujats: WP Offload Media escriu els mitjans nous directament a S3, així que cap fitxer pujat no viu en una instància que pugui desaparèixer.
- Base de dades: Aurora MySQL amb l’escriptor i els lectors en zones diferents, darrere de RDS Proxy. Si l’escriptor falla, es promou un lector.
- Memòria cau d’objectes: ElastiCache for Redis amb una rèplica en una segona zona i failover automàtic.
- Tot l’anterior: Terraform, des de la VPC fins al codi de la CloudFront Function.
Decisions clau i compromisos
Cada decisió següent o bé elimina un punt únic de fallada o bé manté la plataforma utilitzable mentre se substitueix una peça que ha fallat.
Instàncies sense estat: el codi a EFS, els mitjans a S3
Una instància que guarda alguna cosa única és un punt únic de fallada, per moltes còpies que se n’executin. Per això a les instàncies no hi viu res únic: el codi ve d’EFS, els fitxers pujats van a S3, la memòria cau d’objectes és a Redis i les dades a Aurora. Una instància és només un launch template més un muntatge, i això és el que permet a l’Auto Scaling group substituir-ne una en minuts sense que ningú hagi de decidir si importava.
- Compromís: el codi es llegeix per la xarxa. Que PHP llegeixi milers de fitxers des d’EFS és més lent que llegir-los del disc local. OPcache n’absorbeix la major part, amb les comprovacions de timestamp relaxades perquè PHP no pregunti a EFS a cada petició si cada fitxer ha canviat; els desplegaments buiden la memòria cau.
Separació de lectures i escriptures amb RDS Proxy i Aurora
WordPress assumeix una sola base de dades. Vam mantenir l’escriptor per a les escriptures i vam moure les lectures a les rèpliques d’Aurora, fent servir el drop-in LudicrousDB per encaminar cada consulta: les escriptures a l’endpoint per defecte de RDS Proxy i les lectures al seu endpoint de només lectura, que les reparteix entre els lectors.
El proxy es guanya el lloc per partida doble, i la primera raó és la disponibilitat. Durant un failover de l’escriptor manté les connexions dels clients mentre es promou un lector, de manera que PHP veu una pausa curta en lloc d’una ràfega d’errors de base de dades. En el dia a dia, a més, agrupa connexions: PHP n’obre una per petició, així que cada escalat horitzontal multiplicava les connexions a Aurora, i el proxy ho absorbeix.
- Compromís: retard de les rèpliques. Un usuari que desa una entrada i recarrega immediatament no pot llegir una rèplica desactualitzada. Quan una petició ja ha escrit, el drop-in envia la resta de lectures d’aquella petició a l’escriptor.
- Compromís: cost i pinning. RDS Proxy es factura per vCPU de la base de dades, i algunes sentències a nivell de sessió fixen un client a una sola connexió, cosa que redueix l’agrupament. Per això la mètrica de pinning és al dashboard.
Les tres decisions següents actuen sobre la mateixa petició, una rere l’altra, mentre travessa l’edge.
Reescriptura d’URI amb una CloudFront Function per al Multisite
En un Multisite en subdirectoris, cada sublloc serveix els mateixos fitxers del nucli sota el seu propi prefix: /site-a/wp-includes/js/jquery.js i /site-b/wp-includes/js/jquery.js són el mateix fitxer. Si no s’hi fa res, CloudFront en desa a la memòria cau una còpia per sublloc i els seus cache behaviours basats en camins no les poden fer coincidir.
Una CloudFront Function a la viewer request elimina el prefix del sublloc dels camins dels recursos del nucli abans de consultar la memòria cau. Ara un sol objecte a la memòria cau serveix tots els subllocs, i els behaviours estàtics coincideixen sense problemes. Vam triar una CloudFront Function en lloc de Lambda@Edge perquè això és una reescriptura de cadenes: s’executa en menys d’un mil·lisegon a l’edge, costa una fracció i no necessita cap desplegament regional.
També ajuda a la disponibilitat. Cada petició servida des de la memòria cau de l’edge no arriba mai a l’origen, així que un pic de trànsit sobre recursos estàtics recau en CloudFront i no en PHP.
Llista d’accés a l’ALB amb WAF
L’ALB ha de ser públic perquè CloudFront hi pugui arribar, i això vol dir que qualsevol podria saltar-se la CDN i atacar-lo directament. Un web ACL de WAF a l’ALB bloqueja per defecte i només permet les peticions que porten la capçalera secreta d’origen de CloudFront, més un petit IP set per fer proves directes des de l’oficina.
L’alternativa era un security group que fes referència a la managed prefix list de CloudFront. Aquesta llista compta com a desenes de regles contra la quota del security group, i no pot expressar «aquesta capçalera o aquesta IP». WAF sí que pot, i registra cada bloqueig, de manera que una petició rebutjada és fàcil d’explicar.
mod_remoteip per obtenir la IP real del client
Darrere de CloudFront i l’ALB, Apache veu l’adreça privada de l’ALB com a client. Això trenca els logs d’accés, la limitació d’intents d’inici de sessió, les comprovacions d’spam als comentaris i qualsevol altra cosa que depengui de la IP.
mod_remoteip llegeix X-Forwarded-For i el recorre enrere a través dels proxies en què confiem: el rang de la VPC per a l’ALB i els rangs publicats de CloudFront. La primera adreça que no és de confiança és el client real. És important confiar només en proxies coneguts; confiar cegament en la capçalera permetria a qualsevol falsificar la seva IP.
Què costa l’alta disponibilitat
L’alta disponibilitat a AWS costa més cada mes que el servei de WP gestionat, i ho sabíem des del principi. El preu del servei gestionat cobria un sol entorn; el nostre també paga una redundància que la majoria de dies està inactiva, que és exactament la seva feina.
La despesa addicional ve de quatre llocs:
- Segones còpies de tot. Capacitat web, un lector d’Aurora i una rèplica de Redis en una segona zona, dimensionats perquè una sola zona pugui suportar tota la càrrega.
- Peces gestionades. RDS Proxy, EFS, WAF i el balancejador de càrrega afegeixen cadascun una línia que un sol servidor no té.
- Xarxa entre zones. El trànsit entre zones es factura, i també un NAT gateway a cada zona.
- El nostre temps. Els pegats, les actualitzacions i les guàrdies ara són cosa nostra i no del proveïdor.
Ho vam justificar comparant el sobrecost mensual amb el cost d’una sola caiguda greu. Per a una plataforma que milers de persones fan servir per publicar cada dia, una caiguda de diverses hores costa més en feina perduda, càrrega de suport i confiança que la diferència d’allotjament, i l’alta disponibilitat converteix aquestes caigudes en no-incidents. L’Auto Scaling manté el sobrecost a ratlla executant menys instàncies fora de les hores punta.
Què va fallar
Cap d’aquests problemes no va fer caure el lloc. Tots van ser casos en què la nova plataforma va fer exactament el que se li havia dit, i el que se li havia dit estava incomplet.
Un cron que es va aturar sense fer soroll
El wp-config.php va arribar amb DISABLE_WP_CRON a true. Al servei de WP gestionat això era correcte, perquè el proveïdor executava el cron de WordPress des d’un planificador del sistema. Aquell planificador no va venir amb nosaltres, així que després de la migració res no executava els esdeveniments programats: ni entrades programades, ni correus a la cua, ni neteja. No es registrava cap error, perquè no fallava res; simplement no passava.
Solució: una única tasca programada que executa amb WP-CLI els esdeveniments pendents de cada sublloc. Una tasca, no un crontab per instància, o un Auto Scaling group de N instàncies executa el cron N vegades. Lliçó: cal migrar el comportament, no només la configuració. Un flag que desactiva alguna cosa dona per fet que una altra cosa la substitueix.
Els access points d’EFS passen per sobre de root
Un access point d’EFS imposa el seu propi usuari POSIX a cada operació que hi passa, també a les de root. Els nostres passos de desplegament s’executaven com a root i esperaven tenir-ne els poders; a l’access point, root es convertia en l’usuari del servidor web. chown fallava amb errors de permisos i els fitxers nous no tenien els propietaris que demanaven els scripts.
Solució: tractar l’access point com el model de propietat. El seu usuari i grup són els del servidor web, el seu directori arrel es crea amb aquests propietaris i el desplegament ja no intenta canviar la propietat a EFS. Lliçó: un access point és una substitució d’identitat, no només un camí de muntatge.
require_secure_transport d’Aurora i MYSQLI_CLIENT_SSL
Vam imposar TLS a Aurora amb require_secure_transport = ON. El client mysql des d’un shell es connectava sense problemes, perquè per defecte prefereix TLS. WordPress no: mysqli no demana TLS si no se li diu, així que Aurora rebutjava totes les connexions per transport insegur.
Solució: define( 'MYSQL_CLIENT_FLAGS', MYSQLI_CLIENT_SSL ); al wp-config.php. Lliçó: una prova correcta des del shell demostra que el shell funciona. Aquesta idea torna a sortir més avall.
El .htaccess de lloc únic i el bucle de redireccions
El .htaccess que va venir amb el codi era el predeterminat de WordPress per a un lloc únic. Un Multisite en subdirectoris necessita regles de reescriptura addicionals que tornin a apuntar /site-a/wp-admin/ i companyia cap als fitxers reals del nucli. Sense elles, les URL d’administració dels subllocs rebotaven entre WordPress i Apache fins que el navegador es rendia.
Solució: el bloc de reescriptura del Multisite en subdirectoris, que ara es guarda al repositori i es desplega amb el codi. Lliçó: cal comparar cada fitxer de configuració amb el tipus d’instal·lació, no només amb el servidor antic.
El shell no és PHP: la fallada de pujades del site 16
Les pujades fallaven en un sublloc, el site 16. Totes les comprovacions des d’un shell d’SSM passaven: els camins existien, el bucket acceptava un objecte de prova i les credencials es resolien. La pujada continuava fallant, perquè el shell comprovava els camins que esperàvem, i PHP feia servir el camí que li donava WordPress. El site 16 encara tenia una opció upload_path de feia anys, amb un camí absolut al servidor del servei de WP gestionat. WP Offload Media escriu cada fitxer localment abans de copiar-lo a S3, així que PHP intentava escriure en un directori que no existia a les noves instàncies.
Solució: eliminar les opcions obsoletes upload_path i upload_url_path amb WP-CLI, i després revisar la taula d’opcions de cada sublloc a la recerca de camins absoluts de l’antic allotjament. Dos subllocs més tenien la mateixa deixalla i es van arreglar abans que ningú se n’adonés. Lliçó: cal reproduir el problema dins del runtime. Executa la prova com l’usuari del servidor web, a través de PHP i amb la configuració de PHP, abans de fiar-te del resultat. Després dels problemes amb el cron, TLS i les pujades, «funciona des de PHP?» es va convertir en la primera pregunta de cada investigació.
Resultats
Des de la migració, el març del 2026, la plataforma ha superat totes les fallades que hem vist o provocat sense cap caiguda d’infraestructura.
- Disponibilitat: 99,98 % d’uptime des de la migració. L’únic incident que va arribar als usuaris va ser curt i no va venir d’un component que fallés.
- Fallades d’instàncies: l’Auto Scaling group ha substituït diverses vegades instàncies no saludables, sense errors visibles per als usuaris i sense avisar ningú de guàrdia.
- Simulacres de failover: vam terminar instàncies web, vam forçar failovers d’Aurora i vam treure una zona de l’ALB en moments de poc trànsit. Els failovers d’Aurora van aturar les escriptures uns 20 o 30 segons, cosa que RDS Proxy va convertir en peticions més lentes en lloc d’errors.
- Manteniment: cap aturada planificada des de la migració. Els pegats del sistema operatiu es despleguen amb instance refresh, i les actualitzacions menors d’Aurora passen per un failover.
- Rendiment de l’administració: el p95 de wp-admin va baixar d’uns 2,4 s a uns 650 ms als cinc subllocs amb més activitat, un efecte secundari de moure les lectures a les rèpliques i els objectes a Redis.
- Cost: més alt que el del servei de WP gestionat, tal com estava previst i com s’explica més amunt.
Els guanys que no caben en un número importen igual. Una fallada a les 3 de la matinada ara és una línia de log al matí en lloc d’una trucada. Cada canvi arriba com un pla de Terraform revisat, ningú no té cap clau SSH i, quan alguna cosa es degrada, podem veure quina capa n’és la responsable.
Què faria diferent
- Programar simulacres de failover des del primer dia. Vam provar el failover abans de la migració i després només de tant en tant. Un simulacre mensual que mati una instància i faci failover de la base de dades mantindria la confiança al dia i detectaria el drift aviat.
- Reduir abans la diferència de cost. Un Compute Savings Plan i capacitat reservada d’Aurora per a la base sempre activa, més instàncies Graviton per a la capa web, haurien reduït el sobrecost de l’alta disponibilitat des del primer mes.
- Fer inventari del que l’antic allotjament feia de franc. La fallada del cron va venir d’un comportament que el servei de WP gestionat oferia sense que aparegués mai al nostre codi. Una llista escrita de «què fa l’allotjament per nosaltres» (planificadors, regles de reescriptura, límits de PHP) l’hauria fet aflorar a staging.
- Fer privat l’ALB amb els VPC origins de CloudFront. Ara CloudFront pot arribar directament a un ALB intern. Això elimina l’endpoint públic i, amb ell, la major part del motiu de la llista d’accés de WAF.
- Smoke tests des de dins de PHP, per sublloc. Una comprovació posterior al desplegament que pugi un fitxer i programi un esdeveniment a cada sublloc hauria detectat els problemes del cron i del site 16 en minuts.
