Monthly Archives: July 2020

Kisah Quick Fix Aplikasi Web PHP 5

Belum lama ini saya diminta tolong memperbaiki aplikasi lama dalam PHP 5. Aplikasi ini sudah lama dan masih dipakai sampai development aplikasi baru selesai. Aplikasi ditulis dalam PHP dengan framework Code Igniter dengan database MySQL. Deskripsi masalahnya begini: di aplikasi ini setiap hari ada satu halaman yang semakin lambat sampai suatu hari error, tidak bisa diakses lagi.

Pesan popup itu muncul dari komponen datatables.net di browser. Setelah diselidiki: penyebabnya adalah error di sisi server, tepatnya lagi ternyata out of memory di sisi server.

Allowed memory size of 134217728 bytes exhausted (tried to allocate 7077931 bytes) /var/www/html/application/XXX.php

Aplikasi ini meload data dari database menjadi satu file JSON berisi beberapa belas ribu baris. Dari hasil membaca dokumentasi komponen datatables ini, seharusnya mudah membuat paging dengan pemrosesan di sisi server cukup dengan menambah OFFSET dan LIMIT pada query SQL. Tapi ternyata tidak mudah di aplikasi ini.

Kira-kira aplikasinya begini:

$q = "select puluhan_kolom from table where belasan_filter;";
$data=array()
foreach ($rows as $row){
    //isi loop for ini ratusan baris 
    //ada beberapa puluh query berdasarkan nilai $row
    //dalam kondisi tertentu data baru ditambahkan ke $data
}
echo json_encode($data);


Jadi untuk tahu berapa jumlah row-nya perlu menjalankan banyak query, bukan hasil count dari query utama. Andaikan tahu jumlah row-nya pun, tetap akan sulit untuk pergi ke halaman-N karena harus memfilter lagi dari awal. Dari berbagai kondisi di dalam loopnya sepertinya banyak hal ad-hoc ditambahkan, atau mungkin juga designnya salah dari awal.

Lalu setelah saya pikirkan lagi: Jika bisa paging pun, fitur lain harus diubah. Datatables menyediakan fitur filtering dan search built in. Jika kita meload data per page, search dan filter ini harus diimplementasikan ulang dengan kode baru di sisi server.

Jadi apa yang bisa saya lakukan? permintaan tolong ini di hari kerja, jadi saya cuma bisa melakukan ini:

  • menaikkan memory limit PHP
  • menambah ukuran cache mysql
  • menginstall PHP opcode cache (APC)

Beberapa hal lain yang tidak saya lakukan:

  • menginstall PHP 7, karena distribusi Linuxnya terlalu tua. Ribet jika harus menginstall manual
  • mengubah kode program karena terlalu rumit

Programnya bisa berjalan lagi walau seringkali butuh beberapa menit mendownload JSON sekitar 5.5 MB. Dengan koneksi di indonesia, jika bisa stabil 100 kilobyte/second maka butuh 55 detik untuk mengirim datanya. Itu di luar waktu query, karena transfer data baru bisa dimulai setelah semua data siap. Dari cerita yang saya dapat: Koneksi 100 kilobyte/second (kira-kira 1 Mbit/s) sebenarnya tidak sulit dicapai tapi ketika sambil sharing screen di meeting Zoom, kecepatannya bisa sangat tidak stabil. Belum lagi jika dua orang mengakses halaman yang sama untuk didiskusikan.

Mungkin ada yang heran: memory limit kan 128 MB dan ukuran JSON 5.5MB itu sebenarnya sangat kecil, kenapa bisa out of memory? Struktur internal PHP (terutama PHP versi sebelum 7) sangat tidak efisien, ukuran memori yang dipakai bisa belasan kali lipat dari tipe data dasar di bahasa lain (pernah saya tuliskan di artikel tahun 2009). File JSON itu hanya bentuk akhir saja, array yang menyimpan data sebelum jadi JSON memakai memori besar dan setiap query juga memakan memori.

Quick Fix

Di akhir pekan, saya ubah kodenya seminimal mungkin agar cukup cepat. Pertama yang terlihat jelas adalah mengkompres data. Saya sudah menguji bahwa data JSON hasil aplikasi jika dikompres ukurannya menjadi kurang dari 700kb. Dari kecepatan download saja: data 700 kb bisa didownload dalam 7 detik jika koneksinya 100 kilobyte/second vs 55 detik data aslinya.

Di PHP, kompresi bisa dilakukan dengan:

    ob_start("ob_gzhandler", 16*1024);
    //outputkan data seperti biasa dengan echo
    @ob_end_flush();

Dengan modifikasi kecil ini hasilnya: data bisa diakses kurang dari 7 detik setelah dihasilkan. Masih ada masalah lain: harus menunggu dulu sampai semua query selesai baru bisa dikompresi. Langkah berikutnya adalah mengoutputkan JSON per elemen array. Jadi loopnya diganti kira-kira menjadi seperti ini:

ob_start("ob_gzhandler", 16*1024);
echo '{"data":[';
$first = false;
foreach (/*row*/) {
        if ($data_ok) {
              if ($first)   $first = false; else echo ",";
              echo json_encode($current_data);
        }
}
echo "]}";
@ob_end_flush();

Dengan ini sambil melakukan query, data sudah bisa mulai dikirim ke browser. Aplikasi juga menjadi lebih cepat karena tidak menyimpan data di memori, dan tidak perlu menghasilkan string ukuran besar dengan JSON.

Dengan dua quickfix tersebut plus yang sudah dilakukan sebelumnya (menambah limit memori, memakai cache APC, dan menaikkan limit cache mysql), aplikasi bisa diakses kurang dari 10 detik. Menurut saya ini masih lambat, seharusnya dengan design yang baik, aplikasinya bisa instan menampilkan halaman pertama, dan setiap halaman berikutnya bisa meminta request baru. Tentunya akan lebih banyak kode yang perlu ditulis baik di sisi client maupun server.

Pelajaran

Sebagai catatan: posting ini bukan kritik terhadap PHP. Dalam bahasa manapun kesalahan seperti ini bisa terjadi. Tapi faktor kerakusan memori PHP dan lambatnya PHP versi lama membuat bugnya lebih cepat muncul.

Jika Anda adalah seorang client yang meminta dibuatkan aplikasi: perhatikan ukuran data. Selain menguji bahwa aplikasi sudah berjalan, ujilah juga aplikasi dengan jumlah data yang besarnya sesuai dengan pemakaian. Misalnya ingin membuat aplikasi untuk sekolah offline, jumlah murid mungkin bisa ratusan, atau ribuan sampai beberapa tahun ke depan. Dalam kasus ini jumlah murid tapi tidak akan sampai level jutaan.

Ini juga cerita secara umum: membuat aplikasi sederhana memang tidak sulit, tapi membuat aplikasi yang scalable akan butuh waktu lebih lama dan/atau kode yang lebih banyak. Cerita ini hanya sekedar scaling dari ratusan data (sangat cepat ketika didemokan) sampai ke belasan ribu data (aplikasi sangat lambat lalu mati), belum sampai ke level jutaan atau ratusan juta data yang butuh perencanaan yang lebih matang dan implementasi yang lebih sulit.