Draf emailnya sudah sampai kalimat terakhir. Isinya kabar yang paling tidak ingin saya kirim ke siapa pun sesudah sebuah pemulihan darurat: database situs kelihatannya dikembalikan ke backup lama, dan pekerjaan dua bulan terakhir tidak ada lagi. Saya sudah menyiapkan angkanya supaya kalimat itu tidak terdengar seperti tebakan panik.
post 1.301 terbaru dua bulan lalu
episode 505 terbaru dua bulan laluSemuanya saya hitung dari luar, lewat HTTP, tanpa menyentuh servernya sama sekali. Arsip publik, endpoint koleksi REST, sitemap. Tiga sumber yang berbeda, dan ketiganya bercerita hal yang sama persis. Selisihnya pun masuk akal: bukan angka acak, bukan setengah tabel kosong, tapi potongan bersih di satu titik waktu, persis seperti bentuk database yang di-restore dari dump lama.
Yang menahan tangan saya bukan kecurigaan. Justru sebaliknya. Saya ingin emailnya lebih sulit dibantah, jadi saya mau satu angka yang dikutip langsung dari database, bukan dari halaman depan. Untuk itu saya harus masuk ke server dan membuka wp-config.php. Dan untuk membuka wp-config.php, saya harus tahu dulu instalasi yang mana.
Ternyata Apache melayani direktori yang lain
Di server itu ada dua direktori bersaudara di /var/www/html, sisa migrasi beberapa bulan sebelumnya. Yang satu instalasi produksi, yang satu lagi versi lama pra-migrasi yang seharusnya sudah pensiun dan cuma tinggal sebagai arsip. Saya buka konfigurasi vhost-nya lebih dulu, hanya untuk memastikan saya mengetik path yang benar.
apache2ctl -S
grep -R "DocumentRoot" /etc/apache2/sites-enabled/sites-available/situs.example.conf: DocumentRoot /var/www/html/situs.example
sites-available/situs.example-le-ssl.conf: DocumentRoot /var/www/html/situs.exampleRunbook yang saya tulis sebulan sebelumnya mencatat nilai yang berbeda, /var/www/html/prod.situs.example. Kedua berkas vhost berubah, termasuk varian -le-ssl yang melayani lalu lintas HTTPS, jadi seluruh trafik publik mendarat di direktori yang salah, bukan sebagian.
Saat itu penjelasannya masih terasa ringan. Situs menyajikan berkas tema yang lama, pikir saya, tinggal balikkan DocumentRoot-nya dan konten produksi muncul lagi. Yang belum saya sadari, direktori pensiun itu bukan cuma menyimpan tema dan plugin versi lama. Dia menyimpan basis data sendiri.
Prefix tabel yang sama, database yang berbeda
Cara paling cepat membedakan dua instalasi WordPress yang tinggal di host database yang sama biasanya lewat $table_prefix. Instalasi yang dipasang di waktu berbeda hampir selalu dapat prefix berbeda, apalagi kalau installer-nya mengacak. Di sini prefiksnya memang acak, delapan karakter yang jelas bukan wp_ bawaan, dan justru itu yang membuat saya lengah. Saya sempat menganggapnya sebagai sidik jari yang cukup.
for d in /var/www/html/prod.situs.example /var/www/html/situs.example; do
echo "== $d"
grep -E "DB_NAME|DB_HOST|table_prefix" "$d/wp-config.php"
done== /var/www/html/prod.situs.example
define( 'DB_NAME', 'prod_situs' );
define( 'DB_HOST', 'db-internal.example:3306' );
$table_prefix = 'wp_a7k2m9x4_';
== /var/www/html/situs.example
define( 'DB_NAME', 'situs' );
define( 'DB_HOST', 'db-internal.example:3306' );
$table_prefix = 'wp_a7k2m9x4_';Host database sama. Prefix tabel sama persis, sampai ke karakter terakhir. Yang berbeda cuma DB_NAME. Migrasi itu rupanya membawa serta prefix lamanya, dan yang dipisahkan cuma nama basis datanya.
Akibatnya cukup jahat. Kalau kamu memeriksa prefix untuk memastikan sedang berdiri di instalasi yang benar, pemeriksaannya akan lulus di kedua direktori. Kamu akan yakin sudah berada di tempat yang tepat, padahal kamu sedang membaca database yang berbeda dengan isi yang berbeda. Di banyak server, membandingkan prefix memang cukup. Di server ini, satu-satunya pembeda yang berarti adalah DB_NAME.
Dan begitu itu jelas, angka-angka di draf email saya kehilangan seluruh maknanya. Setiap hitungan yang saya ambil dari halaman depan bukan mengukur proyek saya. Dia mengukur situs pensiun yang kebetulan sedang dilayani Apache, lengkap dengan basis datanya sendiri yang memang berhenti terisi dua bulan lalu. Angka 1.301 dan 505 itu benar. Cuma milik situs yang salah.
Mengukur ke database yang benar
Pengukuran ulangnya tidak lewat HTTP sama sekali. Saya sambungkan ke prod_situs dan hitung langsung, per post type, jumlah baris berikut tanggal terbarunya.
SELECT post_type,
COUNT(*) AS jumlah,
MAX(post_date) AS terbaru
FROM wp_a7k2m9x4_posts
WHERE post_status = 'publish'
GROUP BY post_type;Hasilnya 1.328 post, 77 halaman, dan 519 episode, dengan tanggal terbaru di ketiganya jatuh di bulan berjalan, bukan dua bulan lalu. Satu query lagi ke tabel lead milik plugin custom-nya menghitung 1.867 baris. Tidak ada satu pun yang hilang. Yang hilang cuma akses publik ke sana.
Perhatikan bahwa hitungan produksi malah lebih besar dari hitungan HTTP di setiap kategori, 1.328 lawan 1.301, 519 lawan 505. Selisih yang kecil seperti itu tidak pernah cukup untuk membunyikan alarm siapa pun. Kalau selisihnya ribuan, orang akan curiga alat ukurnya rusak. Karena selisihnya cuma puluhan, otak saya langsung membacanya sebagai kehilangan yang wajar dan menyedihkan, bukan sebagai bukti bahwa saya sedang mengukur benda yang lain.
Sitemap posisinya bahkan lebih lemah lagi sebagai bukti, dan saya sempat menghitungnya juga. Sitemap adalah berkas hasil cache. Dia bisa lebih tua dari database mana pun di server itu, termasuk yang sudah pensiun, dan tetap disajikan dengan yakin.
Mengembalikan DocumentRoot, dan kenapa urutannya penting
Perbaikannya sendiri satu baris di dua berkas vhost: kembalikan DocumentRoot ke docroot produksi, lalu reload Apache. Suntingan itu butuh root dan saya tidak memegangnya, jadi bagian eksekusinya bukan milik saya. Yang jadi tugas saya adalah memastikan urutannya benar sebelum ada yang menyentuhnya.
Dan urutannya memang penting, karena docroot produksi punya kerusakannya sendiri di lapisan bootstrap, cerita terpisah yang tidak ada hubungannya dengan vhost. Selama kerusakan itu masih ada, memindahkan DocumentRoot kembali hanya menukar satu gejala dengan gejala yang lebih buruk: dari situs yang menyajikan konten lama menjadi situs yang tidak menyajikan apa-apa. Bootstrap-nya dibereskan dulu dan dibuktikan merender halaman utuh, baru vhost-nya dipindahkan.
Cara membuktikannya juga tanpa Apache, supaya tidak berputar di lingkaran yang sama. Saya jalankan render dari dalam docroot produksi lewat PHP di baris perintah dan periksa keluarannya: ukuran byte yang wajar, judul yang benar, direktori tema yang benar, dan satu penutup dokumen. Kalau keempatnya keluar seperti seharusnya, bootstrap-nya sehat, dan memindahkan vhost aman dilakukan.
Yang berubah dalam cara saya mengukur
Pelajarannya bukan soal Apache, dan bukan juga soal prefix tabel. Pengukuran lewat HTTP menjawab pertanyaan "apa yang server balas hari ini", bukan "apa isi proyek saya". Selama dua pertanyaan itu punya jawaban yang sama, perbedaannya tidak pernah terasa. Begitu ada kemungkinan situs dilayani dari direktori yang tidak kamu duga, dua pertanyaan itu berpisah, dan setiap angka yang kamu tarik dari halaman depan mulai mengukur benda yang salah dengan sangat meyakinkan.
Jadi sekarang, dalam insiden apa pun yang menyentuh konfigurasi server, hal pertama yang saya lakukan bukan menghitung apa-apa. Saya tetapkan dulu direktori mana yang benar-benar dilayani, lalu DB_NAME mana yang dibaca direktori itu. Dua fakta itu murah didapat, keduanya read-only, dan tanpa keduanya setiap angka berikutnya cuma dugaan yang kebetulan berbentuk data.
Draf email itu tidak pernah saya kirim. Saya simpan sebentar sebelum menghapusnya, sebagai pengingat bahwa kesalahan yang paling mahal bukan yang bikin layar penuh error. Yang paling mahal adalah kesalahan yang keluar dalam bentuk angka rapi, konsisten di tiga sumber, dan siap ditempel di email.