Panduan operasional

Panduan Tuning Server SIMGos

Cara menyetel Apache, PHP-FPM, MySQL, dan Tomcat agar SIMGos berjalan cepat dan stabil — ditulis untuk admin IT rumah sakit dan klinik, tanpa istilah yang membingungkan.

Panduan ini gratis dan boleh Anda kerjakan sendiri. Kalau tidak sempat, kami bisa menanganinya — remote.

Panduan Tuning Server SIMGos

Untuk siapa: admin/IT rumah sakit & klinik yang mengelola server SIMGos, tanpa harus jadi ahli database atau sysadmin.

Isi: cara menyetel 4 layanan yang menentukan cepat-lambatnya SIMGos:

LayananPerannya di SIMGos
Apache (httpd)Pintu depan. Menerima semua permintaan dari browser petugas.
PHP-FPMDapur. Menjalankan kode SIMGos (webservice).
MySQL (mysqld)Gudang data. Semua data pasien, tindakan, obat, tagihan.
TomcatTukang cetak. Membuat PDF laporan/berkas via JasperReports.

Semua angka dan "jebakan" di panduan ini berasal dari kasus nyata di server yang kita tangani (4 rumah sakit dan 4 klinik pengguna SIMGos di Indonesia — nama fasilitas disamarkan). Bagian Jebakan di akhir adalah yang paling berharga — semuanya pernah bikin sistem down.

💡 Tidak ingin menghitung manual? Gunakan Kalkulator Tuning Server — masukkan spesifikasi server Anda, dapatkan seluruh file konfigurasi siap salin. Semua perhitungan berjalan di browser Anda.


Butuh angkanya sekarang juga?

Semua layanan dalam satu server — cari baris RAM Anda, salin angkanya, lalu buka bagian 10.2 untuk file config lengkapnya.

RAM / vCPUPengguna bersamaanApache<br>ServerLimit / MaxRequestWorkersPHP-FPM<br>pm.max_childrenMySQL<br>buffer_pool / max_connectionsTomcat<br>-Xmx
8 GB / 4 vCPU< 254 / 100252G / 80512m
16 GB / 4 vCPU< 506 / 150505G / 1001G
32 GB / 8 vCPU< 1008 / 20010010G / 2002G
64 GB / 8–16 vCPU< 25016 / 40025024G / 3004G
96–128 GB / 8–16 vCPU250+32 / 80060048G / 7006G

Kalau layanan dipisah ke server sendiri:

Peran serverRAMAngka kuncinyaDetail
Server aplikasi (httpd+php-fpm+redis)8 / 16 / 32 GBApache 200/400/800 · fpm 150/350/700 · Redis wajib10.4
Server database (mysqld saja)16 / 32 / 64 / 128 GBbuffer_pool 10G/20G/40G/80G · max_conn = Σ max_children semua server app + 5010.5
Server laporan (tomcat+jasper)8 / 16 / 24 GBTomcat Xmx 4G/8G/12G · maxThreads 25/40/50 · request_terminate_timeout 600s10.6

Tiga aturan yang mengikat semuanya:

Apache MaxRequestWorkers  ≈ pengguna bersamaan × 2   (dan HARUS = ServerLimit × 25)
PHP-FPM pm.max_children   ≈ MaxRequestWorkers × 0,5
MySQL max_connections     = pm.max_children + 50

Daftar Isi

  1. Kamus istilah (bahasa awam)
  2. Prinsip dasar sebelum menyetel apa pun
  3. Rantai kapasitas: urutan yang benar
  4. Ukur dulu, baru setel
  5. Tuning Apache (httpd)
  6. Tuning PHP-FPM
  7. Tuning MySQL
  8. Tuning Tomcat (server laporan)
  9. Sesi Redis — obat lemot paling ampuh
  10. Resep siap pakai (per ukuran server & per peran)
  11. Cara menerapkan dengan aman
  12. Verifikasi setelah tuning
  13. Kesalahan yang sering terjadi
  14. Diagnosa cepat: gejala → penyebab
  15. Lampiran: perintah harian

1. Kamus istilah (bahasa awam)

Baca sekali, lalu boleh dilewati.

Umum

IstilahArtinya, sederhananya
Proses / worker / childSatu "pegawai" yang mengerjakan satu permintaan. Kalau semua pegawai sibuk, permintaan baru harus antre.
ThreadSama seperti pegawai, tapi lebih ringan — beberapa thread berbagi satu meja (proses).
ConcurrencyBerapa banyak permintaan yang ditangani pada saat yang sama. Bukan total per hari.
Antrean (queue)Permintaan yang menunggu giliran karena semua pegawai sibuk. Ini yang dirasakan user sebagai "lemot".
TimeoutBatas waktu menunggu. Lewat itu, dibatalkan.
CacheSalinan sementara yang disimpan supaya tidak perlu ambil ulang dari sumber lambat.
Daemon / serviceProgram yang jalan terus di latar belakang (httpd, php-fpm, mysqld, tomcat).
Restart vs ReloadReload = ganti setelan tanpa memutus pekerjaan yang sedang jalan (aman jam kerja). Restart = matikan lalu nyalakan; semua yang sedang jalan terputus (lakukan di jam sepi).
Drop-in configFile setelan tambahan terpisah (mis. /etc/my.cnf.d/simgos.cnf) yang menimpa setelan bawaan, tanpa mengubah file asli. Lebih aman karena update paket tidak menghapusnya.

Memori

IstilahArtinya, sederhananya
RAMMeja kerja server. Cepat tapi terbatas.
SwapMeja cadangan di harddisk. Sangat lambat. Kalau server mulai pakai swap banyak, artinya RAM kurang.
RSSAngka memori yang muncul di top. Menipu — memori yang dipakai bersama-sama dihitung berkali-kali.
USSMemori yang benar-benar milik sendiri satu proses. Ini yang dipakai untuk menghitung "kalau saya tambah 1 pegawai lagi, butuh RAM berapa?"
PSSDi antara RSS dan USS (memori bersama dibagi rata).
OOM KillerSatpam kernel Linux. Kalau RAM habis, dia mematikan paksa proses terbesar — biasanya MySQL. Server "mati sendiri" tanpa jejak error aplikasi.

Aturan emas: hitung kebutuhan RAM pakai USS, jangan RSS. Kita pernah salah hitung 4× lipat di RS-2 gara-gara ini.

Database

IstilahArtinya, sederhananya
InnoDBMesin penyimpan data MySQL yang dipakai SIMGos.
Buffer poolRuang di RAM tempat MySQL menyimpan data yang sering dibaca. Setelan MySQL paling berpengaruh. Kalau kekecilan, MySQL bolak-balik baca harddisk = lambat.
ConnectionSatu saluran telepon antara PHP dan MySQL. Kalau semua saluran terpakai → error Too many connections.
IndexDaftar isi buku. Tanpa index, MySQL membaca seluruh tabel dari awal.
Slow query logCatatan query yang lama. Alat diagnosa utama.
Stored procedureProgram kecil yang tersimpan di dalam database. SIMGos memakainya banyak untuk laporan (mis. CetakMR2).
TriggerAksi otomatis yang jalan setiap kali data disimpan/diubah. SIMGos + SATUSEHAT punya puluhan. Beban tersembunyi.
Table lock / FLUSH TABLESPerintah "semua tutup dulu, saya mau kunci sebentar". Kalau ada query yang jalan berjam-jam, perintah ini macet menunggu — dan backup gagal.

Java / Tomcat

IstilahArtinya, sederhananya
JVMMesin yang menjalankan program Java (Tomcat).
HeapJatah RAM untuk JVM. -Xms = jatah awal, -Xmx = jatah maksimal.
GC (Garbage Collector)Petugas kebersihan JVM, membuang data yang tak terpakai.
SerialGCPetugas kebersihan satu orang. Kalau sampah menumpuk, dia sibuk terus dan seluruh aplikasi berhenti. Jangan dipakai untuk beban laporan.
G1GCPetugas kebersihan satu tim, bekerja paralel. Ini yang harus dipakai.
OOM HeapJatah RAM JVM habis. Tomcat seolah "hang" padahal sedang tercekik GC.
Governor (Jasper)Rem darurat JasperReports: batalkan laporan kalau halaman/waktunya keterlaluan.

PHP

IstilahArtinya, sederhananya
OpcacheMenyimpan hasil "terjemahan" kode PHP supaya tidak diterjemahkan ulang tiap permintaan. Wajib nyala.
SessionKartu identitas login user. Disimpan di file atau di Redis.
File lock (flock)Kunci file. Kalau session disimpan sebagai file, setiap permintaan mengunci file itu — permintaan lain dari user yang sama harus antre satu-satu. Penyebab lemot nomor 1 di SIMGos.
RedisPenyimpan data super cepat di RAM. Dipakai sebagai pengganti file session.
pm (process manager)Cara PHP-FPM mengatur pegawainya: static, dynamic, atau ondemand.

2. Prinsip dasar sebelum menyetel apa pun

  1. Backup dulu, selalu. Sebelum menyentuh file config apa pun:

    cp -a /etc/my.cnf /etc/my.cnf.bak-$(date +%Y%m%d-%H%M%S)
    

    Kalau tidak ada backup, jangan mulai.

  2. Ubah satu hal, ukur, lanjut. Kalau mengubah 5 hal sekaligus lalu server bermasalah, Anda tidak akan tahu yang mana penyebabnya.

  3. Angka besar ≠ lebih cepat. Ini kesalahan paling sering. pm.max_children = 2156 bawaan installer bukan setelan bagus — itulah yang membuat Klinik-C reboot 7× dalam 19 hari. Batas yang terlalu besar tidak menambah kecepatan, hanya memperdalam antrean sampai RAM habis.

  4. Server lemot biasanya bukan karena kurang tenaga. Dari semua kasus kita, penyebab tersering berurutan:

    • kunci session file (flock) — bukan CPU, bukan DB
    • satu laporan berat yang tak terkendali
    • trigger/stored procedure yang jalan diam-diam
    • baru terakhir: kapasitas benar-benar kurang
  5. Jangan menebak, ukur. Bagian 4 menjelaskan caranya.

  6. Catat apa yang Anda ubah, kapan, dan kenapa. Simpan di file di server, mis. /root/tuning-log.txt.


3. Rantai kapasitas: urutan yang benar

Ini konsep terpenting di seluruh panduan. Keempat layanan adalah rantai — permintaan mengalir dari kiri ke kanan:

Browser  →  Apache  →  PHP-FPM  →  MySQL
                          ↓
                       Tomcat (khusus cetak laporan)

Aturannya:

Setiap tahap tidak boleh lebih kecil dari tahap sebelumnya, dan tidak ada gunanya lebih besar dari tahap sebelumnya.

Terjemahan praktisnya:

Apache MaxRequestWorkers   ≥  PHP-FPM pm.max_children
PHP-FPM pm.max_children    <  MySQL max_connections   (sisakan cadangan ±20%)

Kenapa penting:

  • Kalau pm.max_children lebih besar dari MaxRequestWorkers → sia-sia. Apache secara fisik tidak akan pernah mengirim permintaan sebanyak itu. Di RS-2 nilainya 3184 padahal Apache hanya bisa 800 — 2384 sisanya cuma angka di kertas.
  • Kalau max_connections MySQL lebih kecil dari pm.max_children → Anda hanya memindahkan masalah. Dulunya user melihat "lemot", sekarang user melihat error Too many connections. Di RS-2: 800 pegawai PHP vs 500 saluran MySQL.
  • Titik pengereman yang benar adalah Apache, bukan MySQL. Menahan antrean di pintu depan jauh lebih baik daripada membiarkan 800 permintaan menyerbu database.

Batas terakhir yang tidak bisa dilanggar: jumlah CPU. Server 4 vCPU tidak akan mengerjakan 800 hal sekaligus. Di atas beberapa ratus permintaan bersamaan, menambah pegawai hanya memperdalam antrean — throughput tidak naik.


4. Ukur dulu, baru setel

4.1 Spesifikasi server

nproc                    # jumlah CPU
free -h                  # RAM total & terpakai
df -h /                  # sisa disk
uptime                   # load average

Membaca load average: angka pertama ≈ rata-rata antrean 1 menit terakhir. Sehat kalau di bawah jumlah CPU. Server 4 CPU dengan load 3.5 = sibuk tapi sehat; load 20 = kewalahan.

4.2 Beban nyata (bukan tebakan)

Puncak permintaan per menit dari log Apache:

awk '{print $4}' /var/log/httpd/access_log | cut -d: -f1-3 | sort | uniq -c | sort -rn | head -5

Di RS-2 hasilnya 2036 permintaan pada menit tersibuk ≈ 105 permintaan/detik.

Jumlah pegawai PHP yang benar-benar terpakai:

pgrep -c php-fpm

⚠️ Ambil sampel di jam sibuk (09:00–12:00), bukan tengah malam. Kesalahan kita di RS-2: mengukur 70 pegawai jam 01:00 lalu mengira itu representatif.

Cara benar — sampler otomatis. Pasang cron ini seminggu, lalu baca hasilnya:

cat > /etc/cron.d/fpm-sampler <<'EOF'
* * * * * root echo "$(date +\%F\ \%H:\%M) $(pgrep -c php-fpm) $(pgrep -c httpd)" >> /var/log/fpm-peak.log
EOF

Baca puncaknya:

sort -k3 -rn /var/log/fpm-peak.log | head -20

Apakah PHP-FPM pernah kehabisan pegawai:

grep "reached pm.max_children" /var/log/php-fpm/error.log

Ada barisnya = batas terlalu kecil, naikkan. Kosong = batas cukup atau batasnya memang mustahil tercapai (lihat rantai kapasitas).

4.3 Berat sebenarnya satu pegawai PHP (USS, bukan RSS)

for p in $(pgrep php-fpm | head -20); do
  awk '/^Rss:/{r=$2} /^Pss:/{p=$2} /^Private/{u+=$2} END{print r, p, u}' /proc/$p/smaps_rollup 2>/dev/null
done | awk '{r+=$1;p+=$2;u+=$3;n++} END{printf "rata2 RSS %d MB | PSS %d MB | USS %d MB (n=%d)\n", r/n/1024, p/n/1024, u/n/1024, n}'

Hasil nyata di RS-2: RSS 60 MB, PSS 16 MB, USS 15 MB. Pakai angka USS.

  • Salah (pakai RSS): 800 pegawai × 60 MB = 48 GB → panik, kesimpulan "RAM tidak cukup"
  • Benar (pakai USS): 800 pegawai × 15 MB = 12 GB → aman

4.4 Ukuran database

mysql -uroot -e "
SELECT ROUND(SUM(data_length+index_length)/1024/1024/1024,1) AS total_GB
FROM information_schema.tables;"

Angka ini menentukan innodb_buffer_pool_size.

4.5 Rumus anggaran RAM

RAM total  =  OS (1–2 GB)
            + MySQL       (buffer_pool × 1,3)
            + PHP-FPM     (max_children × USS)
            + Tomcat      (Xmx + 1 GB)
            + Apache      (± 0,5 GB)
            + Redis       (maxmemory)
            + cadangan 15%

Kalau totalnya melebihi RAM fisik → kecilkan buffer pool atau max_children, jangan berharap "biasanya tidak semua terpakai". Server Klinik-C mati justru karena harapan itu.


5. Tuning Apache (httpd)

Perannya: pintu depan + pengerem. Di sinilah antrean harus ditahan.

File: buat drop-in /etc/httpd/conf.d/mpm-event.conf (jangan edit httpd.conf bawaan).

5.1 Pastikan memakai MPM event

httpd -V | grep "Server MPM"

Harus event. Kalau prefork, tiap koneksi memakan satu proses penuh — boros. Ubah di /etc/httpd/conf.modules.d/00-mpm.conf: aktifkan mod_mpm_event.so, nonaktifkan mod_mpm_prefork.so.

5.2 Rumus utama

MaxRequestWorkers = ServerLimit × ThreadsPerChild

Ini bukan saran — ini hukum. Apache menghitungnya sendiri. Kalau Anda menulis MaxRequestWorkers lebih besar dari hasil perkalian itu, Apache diam-diam menurunkannya dan menulis peringatan AH00515 di error log setiap kali start.

ThreadsPerChild biarkan 25 (nilai standar, jangan diutak-atik). Jadi cukup atur ServerLimit:

Kapasitas yang diinginkanServerLimitMaxRequestWorkers
1004100
1506150
2008200
40016400
80032800
1200481200

Berapa yang saya butuhkan? Ambil puncak permintaan/detik dari [4.2], kalikan waktu rata-rata satu permintaan (detik), lalu kali 3 sebagai cadangan. Contoh RS-2: 105 req/detik × 0,02 detik × 3 ≈ 7. Nilai 800 sudah 8× lipat dari puncak terukur — sangat longgar. Tidak perlu dinaikkan.

5.3 Konfigurasi

# /etc/httpd/conf.d/mpm-event.conf
<IfModule mpm_event_module>
    StartServers             2
    ServerLimit              8
    ThreadsPerChild          25
    MaxRequestWorkers        200      # HARUS = ServerLimit × ThreadsPerChild
    MinSpareThreads          25
    MaxSpareThreads          75
    MaxConnectionsPerChild   10000
</IfModule>

Timeout               60
KeepAlive             On
KeepAliveTimeout      5
MaxKeepAliveRequests  100

Penjelasan tiap baris:

SetelanArtinyaCatatan
StartServersJumlah proses saat start2 cukup, Apache menambah sendiri
ServerLimitBatas keras jumlah prosesMenaikkan ini butuh RESTART penuh, reload tidak cukup
ThreadsPerChildPegawai per prosesBiarkan 25
MaxRequestWorkersTotal permintaan bersamaanIni "kapasitas" Apache
MaxConnectionsPerChildProses didaur ulang setelah N koneksiMencegah kebocoran memori menumpuk
KeepAliveTimeout 5Berapa lama koneksi menganggur dipertahankanJangan 15 detik ke atas — koneksi menganggur memakan slot pegawai
Timeout 60Batas menunggu permintaan lambat

5.4 Timeout proxy untuk laporan

Kalau server laporan terpisah, Apache mem-proxy ke sana. Timeout proxy harus selaras dengan request_terminate_timeout PHP-FPM di server laporan, kalau tidak koneksi menggantung dan menghabiskan pegawai:

# /etc/httpd/conf.d/php.conf — server laporan
ProxySet timeout=600

⚠️ Kecuali endpoint pembayaran/BPJS. Di RS-1, pembayaran-tagihan sengaja dibiarkan timeout=7200 karena operasi BPJS/INACBG memang lama. Memotongnya = transaksi tagihan gagal. Jangan pukul rata semua endpoint.

5.5 Rotasi log — jangan sampai terlewat

⚠️ Aturan bawaan /etc/logrotate.d/httpd hanya mencakup /var/log/httpd/*log. Kalau vhost Anda menulis log ke tempat lain (mis. /var/www/html/log/), log itu tidak akan pernah dirotasi. Di RS-2 requests.log tumbuh sampai 258 GB selama 2 tahun.

Cek dulu:

grep -rE "CustomLog|ErrorLog" /etc/httpd/conf.d/*.conf

Kalau ada path di luar /var/log/httpd/, buat aturan sendiri:

# /etc/logrotate.d/simgos-vhost
/var/www/html/log/*.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    dateext
    create 0644 simgos simgos
    sharedscripts
    postrotate
        /usr/bin/systemctl reload httpd > /dev/null 2>&1 || true
    endscript
}

Rotasi pertama harus dipaksa (logrotate menganggap file baru "sudah dirotasi hari ini"):

logrotate -f /etc/logrotate.d/simgos-vhost

Kalau log sudah terlanjur raksasa: jangan pakai copytruncate (menyalin 258 GB). Pakai rename + reload seperti di atas, tunggu Apache melepas file lama (ls -l /proc/<pid>/fd | grep <namafile-lama> sampai kosong), baru kompres. Kompresi di server itu terbatas I/O bukan CPU — pakai ionice -c 2 -n 7 (bukan -c 3 yang malah melambat jadi 5 MB/detik).

5.6 Terapkan

httpd -t                    # WAJIB: cek sintaks dulu
systemctl reload httpd      # ganti setelan tanpa memutus user

Khusus perubahan ServerLimit: perlu systemctl restart httpd (lakukan di jam sepi).


6. Tuning PHP-FPM

Perannya: dapur. Ini tempat setelan paling sering salah.

File: /etc/php-fpm.d/www.conf, /etc/php.ini, /etc/php.d/10-opcache.ini

6.1 Pilih mode process manager

ModeCara kerjaPakai kapan
staticSemua pegawai dibuat di awal, tetapServer besar khusus, beban rata
dynamicJaga sejumlah pegawai siaga, tambah saat ramaiBeban naik-turun sedang
ondemandBuat pegawai hanya saat ada permintaan, bubarkan setelah menganggurRekomendasi untuk SIMGos

Kenapa ondemand: beban SIMGos sangat berpola — ramai 08:00–13:00, sepi malam. ondemand menghemat RAM saat sepi. Kerugiannya hanya sedikit lambat di permintaan pertama, tidak terasa.

⚠️ Di mode ondemand, setelan pm.start_servers, pm.min_spare_servers, pm.max_spare_servers diabaikan sepenuhnya. Boleh dibiarkan ada, tidak berbahaya, tapi jangan berharap berpengaruh.

6.2 Menentukan pm.max_children

Ambil nilai terkecil dari tiga batas ini:

1. Apache MaxRequestWorkers             ← biasanya ini yang menentukan
2. (RAM tersedia untuk PHP) ÷ USS per pegawai
3. jumlah CPU × 25   (batas kewarasan)

Contoh RS-2 (4 vCPU, 96 GB, Apache 800):

  • Batas 1: 800
  • Batas 2: RAM lega, ~800 × 15 MB = 12 GB → tidak mengikat
  • Batas 3: 4 × 25 = 100 (tapi terbukti aman lebih tinggi karena banyak waktu habis menunggu I/O)
  • Dipilih: 800 — disamakan persis dengan Apache.

Contoh klinik kecil (3 vCPU, 10 GB, Apache 100):

  • Batas 1: 100
  • Batas 2: RAM setelah MySQL 5 GB + Tomcat 0,5 GB + OS 1 GB → sisa ~3 GB ÷ 20 MB = 150
  • Dipilih: 20 — sengaja sangat konservatif; hanya 10–15 pasien/hari, dan RAM lebih baik diberikan ke buffer pool MySQL.

6.3 Konfigurasi pool

; /etc/php-fpm.d/www.conf

pm = ondemand
pm.max_children = 100
pm.process_idle_timeout = 30s
pm.max_requests = 500

; endpoint pemantauan (dipakai bagian verifikasi)
pm.status_path = /php-fpm-status
ping.path = /php-fpm-ping

; catat permintaan yang lebih lama dari 10 detik — alat diagnosa utama
request_slowlog_timeout = 10s
slowlog = /var/log/php-fpm/www-slow.log
SetelanArtinya
pm.max_childrenBatas pegawai bersamaan
pm.process_idle_timeoutPegawai menganggur dibubarkan setelah sekian detik
pm.max_requestsPegawai didaur ulang tiap N permintaan (mencegah kebocoran memori)
request_slowlog_timeoutPermintaan >N detik dicatat lengkap dengan baris kode penyebabnya

⚠️ Jangan ada titik koma di akhir nilai. pm.process_idle_timeout = 30s; (dengan ;) pernah lolos di RS-2 — PHP-FPM menganggapnya awal komentar.

6.4 request_terminate_timeout — hati-hati

Setelan ini membunuh permintaan yang lewat batas waktu.

  • Di server laporan: pakai (600s). Laporan yang tak terkendali harus dibunuh.
  • Di server aplikasi utama: JANGAN pasang global. Operasi BPJS/INACBG/tagihan memang bisa lama; membunuhnya = transaksi gagal. Di RS-1 ini sengaja tidak dipasang.

Kalau perlu, buat pool terpisah untuk laporan/pembayaran dengan timeout berbeda (RS-3 sudah begini: pool www, report, pembayaran).

6.5 php.ini

memory_limit         = 256M    ; per permintaan, bukan total
max_execution_time   = 60
post_max_size        = 32M
upload_max_filesize  = 32M
max_input_vars       = 5000    ; form SIMGos panjang, bawaan 1000 kadang kurang
realpath_cache_size  = 4M      ; cache lokasi file — signifikan untuk Laminas
realpath_cache_ttl   = 600

6.6 Opcache — wajib

Tanpa opcache, PHP menerjemahkan ulang ribuan file SIMGos di setiap permintaan. Ini beda kecepatan 2–3×.

; /etc/php.d/10-opcache.ini
zend_extension=opcache
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=192      ; MB — SIMGos besar, 128 sering kurang
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000 ; SIMGos punya ribuan file
opcache.validate_timestamps=1       ; deteksi file berubah (butuh untuk deploy)
opcache.revalidate_freq=2           ; cek tiap 2 detik
opcache.save_comments=1             ; WAJIB 1 — Laminas/Doctrine membaca anotasi
opcache.fast_shutdown=1

⚠️ opcache.save_comments=0 akan merusak Laminas. Jangan "mengoptimalkan" baris ini. ⚠️ Kalau opcache.validate_timestamps=0 (lebih cepat), setiap deploy harus systemctl reload php-fpm. Untuk SIMGos yang sering di-patch, biarkan 1.

6.7 Terapkan

php-fpm -t                  # WAJIB: cek sintaks
systemctl reload php-fpm    # aman di jam kerja

7. Tuning MySQL

Perannya: gudang data. Setelan paling berdampak, dan paling berisiko kalau salah.

File: buat drop-in /etc/my.cnf.d/simgos.cnf.

7.1 Cek dulu: apakah drop-in dibaca?

MySQL versi resmi (bukan bawaan distro) tidak selalu punya !includedir. Kalau tidak ada, file drop-in Anda diabaikan diam-diam.

grep -n "includedir" /etc/my.cnf

Kalau kosong, tambahkan di baris paling akhir /etc/my.cnf:

!includedir /etc/my.cnf.d/

7.2 Cek duplikat: baris terakhir yang menang

grep -n "max_connections\|innodb_buffer_pool_size" /etc/my.cnf /etc/my.cnf.d/*.cnf

Di RS-2, /etc/my.cnf punya dua baris max_connections: baris 20 = 2048 dan baris 50 = 500. Yang berlaku adalah yang terakhir (500); baris 20 sudah mati sejak lama tanpa ada yang sadar. Kalau ada duplikat, komentari yang lama.

7.3 innodb_buffer_pool_size — setelan paling penting

Cara memilih:

KondisiNilai
MySQL sendirian di server60–70% RAM
MySQL bareng httpd+php-fpm+tomcat30–40% RAM
Ukuran DB lebih kecil dari ituukuran DB × 1,2 sudah cukup

Jangan berlebihan. RS-2 punya buffer pool 48 GB untuk DB berukuran 25 GB — hanya 11% halaman terpakai. Tidak merugikan (alokasi bersifat malas/lazy), tapi mubazir.

Tambahan:

innodb_buffer_pool_instances = 4     ; 1 instance per 1–2 GB pool, maks 8

Cek efektivitasnya (target ≥ 99%):

mysql -uroot -e "
SELECT ROUND(100 - (SELECT VARIABLE_VALUE FROM performance_schema.global_status
  WHERE VARIABLE_NAME='Innodb_buffer_pool_reads')
  / (SELECT VARIABLE_VALUE FROM performance_schema.global_status
  WHERE VARIABLE_NAME='Innodb_buffer_pool_read_requests') * 100, 2) AS hit_rate_persen;"

Kalau di bawah 95% → pool kurang besar. Kalau 99,9% → sudah cukup, jangan ditambah.

7.4 max_connections

max_connections ≥ pm.max_children + 50

Cadangan 50 untuk Tomcat, cron (SATUSEHAT, BPJS, aplicares), backup, dan sesi admin.

⚠️ Menaikkan max_connections wajib diikuti menaikkan batas file:

mkdir -p /etc/systemd/system/mysqld.service.d
cat > /etc/systemd/system/mysqld.service.d/limits.conf <<'EOF'
[Service]
LimitNOFILE=32000
EOF
systemctl daemon-reload

MySQL butuh sekitar max_connections + table_open_cache × 2 file terbuka. Bawaan sering hanya 10.000 — kurang untuk max_connections = 1000. Berlaku setelah restart mysqld berikutnya.

Menaikkan max_connections sendiri bisa tanpa restart:

mysql -uroot -e "SET GLOBAL max_connections = 1000;"

Lalu tetap tulis di simgos.cnf supaya bertahan setelah restart.

7.5 Konfigurasi lengkap

# /etc/my.cnf.d/simgos.cnf
[mysqld]

# === Memori ===
innodb_buffer_pool_size        = 6G
innodb_buffer_pool_instances   = 6
innodb_redo_log_capacity       = 1G      # MySQL 8.0.30+; versi lama: innodb_log_file_size
innodb_log_buffer_size         = 64M

# === Disk I/O ===
innodb_flush_method            = O_DIRECT
innodb_flush_log_at_trx_commit = 2
innodb_io_capacity             = 1000    # HDD: 200 | SSD: 1000-2000 | NVMe: 4000+
innodb_io_capacity_max         = 2000
innodb_flush_neighbors         = 0       # 0 untuk SSD/NVMe, 1 untuk HDD

# === Koneksi ===
max_connections                = 150
thread_cache_size              = 16
table_open_cache               = 4000
table_definition_cache         = 2000

# === Buffer per sesi (kecil! dikalikan jumlah koneksi) ===
sort_buffer_size               = 2M
join_buffer_size               = 2M
read_buffer_size               = 256K
read_rnd_buffer_size           = 512K

# === Tabel sementara (laporan SIMGos berat) ===
tmp_table_size                 = 128M
max_heap_table_size            = 128M

# === Keamanan & visibilitas ===
skip_name_resolve              = 1
max_allowed_packet             = 64M
slow_query_log                 = 1
slow_query_log_file            = /var/log/mysql-slow.log
long_query_time                = 2
max_execution_time             = 60000   # ms — rem laporan liar (lihat 7.7)

performance_schema             = ON

# === Warm-up setelah restart ===
innodb_buffer_pool_dump_at_shutdown = 1
innodb_buffer_pool_load_at_startup  = 1

Catatan penting per setelan:

SetelanKenapa
innodb_flush_log_at_trx_commit = 2Lebih cepat menulis. Risikonya: kalau server mati listrik mendadak, transaksi ≤1 detik terakhir bisa hilang. Untuk RS dengan UPS ini wajar. Kalau tidak ada UPS, pakai 1.
skip_name_resolve = 1MySQL berhenti mencari nama host tiap koneksi. Menghilangkan jeda misterius. Syarat: semua grant di MySQL harus pakai IP, bukan hostname.
sort_buffer_size kecilIni dialokasikan per koneksi. sort_buffer_size = 32M × 500 koneksi = 16 GB. Kesalahan klasik.
tmp_table_size besarLaporan SIMGos (CetakMR2, RL) membuat tabel sementara besar. Kalau kekecilan, MySQL menulisnya ke disk = lambat.
buffer_pool_dump/loadSetelah restart, MySQL memuat ulang isi cache. Tanpa ini, 15–30 menit pertama setelah restart terasa lambat.

7.6 Slow query log — jangan dibiarkan mencemari dirinya sendiri

⚠️ Jangan menyalakan log_queries_not_using_indexes = ON di produksi. Di server-database RS-1 ini membuat slow log tumbuh jadi 14 GB / 715.000 entri, penuh query sepersekian detik pada tabel kecil — sehingga 27.800 query yang benar-benar lambat (≥10 detik) tenggelam dan tak terlihat.

Yang benar:

slow_query_log                       = 1
long_query_time                      = 2
log_queries_not_using_indexes        = OFF
log_slow_admin_statements            = OFF

Dan rotasi lognya:

# /etc/logrotate.d/mysql-slow
/var/log/mysql-slow.log {
    weekly
    rotate 4
    compress
    missingok
    notifempty
    create 0640 mysql mysql
    postrotate
        /usr/bin/mysqladmin flush-slow-logs 2>/dev/null || true
    endscript
}

7.7 max_execution_time — rem laporan liar

max_execution_time = 60000   # 60 detik, dalam milidetik

Hanya berlaku untuk SELECT, jadi aman untuk transaksi tulis.

Kenapa penting: di RS-1, stored procedure medicalrecord.CetakMR2 (cetak Resume Medis) untuk sebagian nomor pendaftaran meledak dan berjalan 10–14 jam. Akibat berantainya:

  1. Query menahan tabel tetap terbuka
  2. Backup malam (xtrabackup) sampai di tahap FLUSH TABLES → macet menunggu
  3. Backup menggantung 2,5 jam
  4. Restart mysqld terjadwal jam 06:00 datang → backup gagal dengan 1053 Server shutdown in progress
  5. Malam itu tidak ada backup sama sekali

Terjadi berulang: 18, 22, 23 Juni dan 12, 14 Juli.

Kalau 60 detik terlalu ketat untuk laporan sah Anda, naikkan ke 300000 (5 menit) — tapi jangan biarkan tak terbatas.

7.8 Lindungi backup

Tambahkan ke perintah xtrabackup di script backup Anda:

--ftwrl-wait-timeout=120 \
--kill-long-queries-timeout=120 \
--kill-long-query-type=all

Artinya: kalau ada query yang menghalangi lebih dari 2 menit, bunuh query itu dan lanjutkan backup. Lebih baik satu laporan gagal dicetak daripada tidak ada backup semalam.

7.9 Beban tersembunyi: trigger dan event

Sebelum menyalahkan "kurang RAM", periksa dulu:

# berapa banyak trigger, per database
mysql -uroot -e "
SELECT TRIGGER_SCHEMA, COUNT(*) FROM information_schema.TRIGGERS
GROUP BY TRIGGER_SCHEMA ORDER BY 2 DESC;"

# event terjadwal & apakah pernah error
mysql -uroot -e "SELECT db,name,status,interval_value,interval_field,last_executed FROM mysql.event;"
grep -i "PROCEDURE" /var/log/mysqld.log | tail -30

⚠️ information_schema.TRIGGERS hanya menampilkan trigger yang boleh dilihat user Anda. User simgos pernah hanya melihat 26 dari 460 trigger yang sebenarnya ada. Jalankan sebagai root.

Kasus nyata di server-database RS-1: 78 trigger kemkes-ihs + 6 trigger di tabel panas pendaftaran.kunjungan menyala setiap kali pendaftaran disimpan. Satu UPDATE ... WHERE NOMOR=<primary key> yang seharusnya instan malah memeriksa 6 juta sampai 110 juta baris — bukan karena index kurang (NOMOR sudah primary key), tapi karena rentetan trigger. Jangan buru-buru menyimpulkan "kurang index"; buktikan dengan EXPLAIN dulu.

7.10 Terapkan

mysqld --validate-config          # cek sintaks (MySQL 8.0.16+)
systemctl restart mysqld          # sebagian besar setelan butuh RESTART

Setelan yang bisa diubah tanpa restart:

mysql -uroot -e "SET GLOBAL max_connections = 1000;"
mysql -uroot -e "SET GLOBAL slow_query_log = ON;"
mysql -uroot -e "SET GLOBAL long_query_time = 2;"

Yang wajib restart: innodb_buffer_pool_size (bisa online di 8.0 tapi berisiko), innodb_redo_log_capacity, innodb_flush_method, skip_name_resolve.


8. Tuning Tomcat (server laporan)

Perannya: menjalankan JavaBridge + JasperReports, membuat PDF.

File: /opt/tomcat/bin/setenv.sh (instalasi manual) atau /etc/sysconfig/tomcat (instalasi paket).

8.1 Masalah klasik: heap kecil + SerialGC

Di server-laporan RS-1, server dengan 23 GB RAM dan 16 core menjalankan Tomcat dengan -Xms512M -Xmx1024M -XX:+UseSerialGC. Akibatnya: seluruh generasi heap penuh 99–100%, JVM tercekik GC, Tomcat tampak hang padahal tidak crash dan tidak kena OOM Killer.

Operator merespons dengan menekan tombol power fisik server — 8 kali dalam sehari (08:18–12:49). Padahal systemctl restart tomcat sudah cukup.

8.2 Ukuran heap

ServerSetelan
Klinik, laporan jarang dipakai-Xms256m -Xmx512m
RS kecil/menengah, Tomcat serumah dengan MySQL-Xms1G -Xmx2G
Server laporan khusus, 16–24 GB RAM-Xms4G -Xmx12G

Aturan: Xmx maksimal 50% RAM kalau berbagi server, maksimal 60% kalau server khusus. Sisakan ruang untuk metaspace, thread stack, dan buffer OS.

8.3 Konfigurasi lengkap

# /opt/tomcat/bin/setenv.sh
CATALINA_OPTS="$CATALINA_OPTS -Xms4G -Xmx12G"
CATALINA_OPTS="$CATALINA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
CATALINA_OPTS="$CATALINA_OPTS -XX:MaxMetaspaceSize=512m"
CATALINA_OPTS="$CATALINA_OPTS -Djava.awt.headless=true"

# Kalau kehabisan memori: buang jejak forensik lalu MATI BERSIH
CATALINA_OPTS="$CATALINA_OPTS -XX:+HeapDumpOnOutOfMemoryError"
CATALINA_OPTS="$CATALINA_OPTS -XX:HeapDumpPath=/opt/tomcat/logs"
CATALINA_OPTS="$CATALINA_OPTS -XX:+ExitOnOutOfMemoryError"

# Rem darurat JasperReports — batalkan laporan yang keterlaluan
CATALINA_OPTS="$CATALINA_OPTS -Dnet.sf.jasperreports.governor.max.pages=10000"
CATALINA_OPTS="$CATALINA_OPTS -Dnet.sf.jasperreports.governor.timeout=480000"

export CATALINA_OPTS

Penjelasan:

SetelanArtinya
-XX:+UseG1GCPetugas kebersihan bertim, bukan tunggal. Yang paling penting.
-XX:MaxGCPauseMillis=200Target jeda GC maks 200 ms
-XX:+ExitOnOutOfMemoryErrorKalau kehabisan heap, mati bersih supaya systemd bisa menghidupkan ulang otomatis. Tanpa ini, Tomcat menggantung selamanya.
-XX:+HeapDumpOnOutOfMemoryErrorMenulis file .hprof — bukti untuk mencari laporan mana penyebabnya
governor.max.pages=10000Batalkan laporan yang melebihi 10.000 halaman
governor.timeout=480000Batalkan laporan yang lebih dari 8 menit

⚠️ Governor bukan jaminan penuh. Dia tidak menangkap OOM yang terjadi karena driver MySQL menampung hasil query raksasa di memori sebelum Jasper mulai memproses. Obat sesungguhnya adalah membatasi rentang tanggal di sisi aplikasi. Laporan terberat di RS-1 adalah LaporanFarmasiPerPasien — punya filter tanggal, tapi rentangnya dipilih user, jadi user bisa minta setahun sekaligus (satu output pernah 142 MB).

8.4 systemd — supaya bisa pulih sendiri

# /etc/systemd/system/tomcat.service
[Service]
Type=forking
PIDFile=/opt/tomcat/temp/tomcat.pid    # WAJIB ada untuk Type=forking
Restart=on-failure
RestartSec=10
UMASK=0002                              # lihat catatan di bawah

⚠️ Type=forking tanpa PIDFile membuat systemd kehilangan jejak proses — Restart=on-failure jadi tidak berfungsi. Ini yang terjadi di RS-1 sebelum 26 Mei 2026: auto-recovery ada di config tapi tidak pernah jalan.

systemctl daemon-reload
systemctl restart tomcat

8.5 Batasi thread Tomcat

Di /opt/tomcat/conf/server.xml, connector sering memakai default ~200 thread. Untuk beban laporan berat, 200 laporan bersamaan pasti menghabiskan heap:

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           maxThreads="50"
           acceptCount="100"
           redirectPort="8443" />

maxThreads=50 + acceptCount=100 artinya: maksimal 50 laporan dikerjakan bersamaan, 100 berikutnya mengantre, sisanya ditolak. Antre lebih baik daripada semua gagal karena OOM.

8.6 Bersihkan aplikasi bawaan

rm -rf /opt/tomcat/webapps/{docs,examples,host-manager,manager}

Sisakan JavaBridge dan ROOT. Ini hanya soal keamanan, bukan performa — tapi sekalian saja.

8.7 Jangan hapus izin sudo Apache

SIMGos mensyaratkan baris berikut ada:

apache ALL=(ALL) NOPASSWD: ALL

Karena Jasper (berjalan sebagai user tomcat) menulis PDF dengan izin yang tidak bisa dibaca apache, dan kode SIMGos menjalankan sudo chmod tepat sebelum membacanya.

Gejala kalau baris ini hilang: semua cetak laporan gagal. Browser hanya menampilkan "Failed to load PDF document". HTTP statusnya 200 dengan Content-Length 0, sehingga lolos dari semua pemantauan berbasis status code. Error sebenarnya ada di /var/log/php-fpm/www-error.log sebagai Permission denied.

Ini pernah hilang saat migrasi OS RS-4 (CentOS 7 → AlmaLinux 9), karena migrasi biasanya sengaja tidak menyalin /etc/sudoers. Pasang sebagai drop-in:

echo 'apache ALL=(ALL) NOPASSWD: ALL' > /tmp/s && visudo -cf /tmp/s \
  && install -m 0440 -o root -g root /tmp/s /etc/sudoers.d/simgos

Solusi jangka panjang yang lebih aman: UMASK=0002 di unit systemd Tomcat, supaya Jasper menulis file yang bisa dibaca grup — menghilangkan kebutuhan sebagian besar chmod.


9. Sesi Redis — obat lemot paling ampuh

Ini bukan tuning kapasitas, tapi dampaknya paling besar dari semua yang ada di panduan ini. Baca sampai habis.

9.1 Masalahnya

Secara bawaan PHP menyimpan session sebagai file (session.save_handler = files). Setiap kali kode memanggil session_start(), PHP mengunci file session user itu selama seluruh permintaan berlangsung.

Di SIMGos:

  • Setiap REST resource dan RPC controller membuat objek Laminas\Authentication\Storage\Session di konstruktornya — jadi setiap permintaan memanggil session_start()
  • session_write_close() tidak pernah dipanggil di seluruh kode SIMGos — jadi kunci ditahan sampai permintaan selesai
  • ExtJS mengirim puluhan permintaan paralel setiap kali user membuka satu layar

Hasilnya: puluhan permintaan dari satu user berbaris satu-satu menunggu kunci file yang sama. Bukan CPU, bukan database — antrean kunci file.

Bukti dari server-aplikasi RS-1: slow log php-fpm mencatat 33.712 permintaan lambat (>5 detik) per hari, 85% di antaranya macet di baris yang samaDBService/src/Resource.php:116, yaitu konstruktor session. Sementara itu server-database RS-1 (MySQL) menganggur total: 0 slow query, 0 lock wait. Semua orang menyalahkan database; database tidak bersalah sama sekali.

9.2 Solusinya

Ganti penyimpanan session ke Redis, yang tidak mengunci (redis.session.locking_enabled = 0 adalah default phpredis).

# 1. Pasang
dnf install -y redis php-pecl-redis5

# 2. Setel redis: hanya localhost, batasi memori
cat >> /etc/redis/redis.conf <<'EOF'
maxmemory 512mb
maxmemory-policy allkeys-lru
save 3600 1
save 300 100
save 60 10000
EOF

systemctl enable --now redis
redis-cli ping        # harus PONG

⚠️ AlmaLinux/RHEL 9 mengirim semua baris save dalam keadaan dikomentari. Tanpa baris save, restart Redis akan menghapus semua session — semua user ter-logout. Baris save di atas wajib dipulihkan.

# 3. /etc/php-fpm.d/www.conf
php_value[session.save_handler] = redis
php_value[session.save_path]    = "tcp://127.0.0.1:6379"
php-fpm -t && systemctl reload php-fpm

9.3 Hasil nyata

Di server-aplikasi RS-1, setelah cutover: pegawai php-fpm turun dari 150 menjadi 43, dan gelombang permintaan lambat harian hilang.

9.4 Peringatan cutover

  • Tidak ada jalur migrasi dari file ke Redis. Saat pindah, semua user yang sedang login akan ter-logout sekali.
  • Lakukan di jam sepi. Di RS-2, pengukuran jam 01:50 (jam sepi!) sudah menunjukkan ~777 session aktif dalam 15 menit. Cutover di siang hari = ribuan petugas ter-logout di tengah pelayanan.
  • Session tidak bisa dikuras perlahan: session.gc_maxlifetime hanya 1440 detik (24 menit), tapi user aktif terus memperbaruinya.
  • Rollback mudah: komentari kembali 2 baris itu, systemctl reload php-fpm, kembali ke file.

9.5 Jebakan permanen: upgrade PHP

Setiap upgrade PHP akan menghapus ekstensi phpredis yang dibuat untuk versi lama. Akibatnya: session_start() gagal → session tidak tersimpan → user login, halaman kembali ke login, berulang terus.

Terjadi di RS-3 1 Juli 2026 setelah PHP naik ke 8.2. Redis server-nya hidup sehat (PONG), tapi dbsize = 0 — tidak ada yang bisa menulis ke sana.

Cara memastikan (jalankan setelah SETIAP upgrade PHP):

php-fpm -m | grep redis          # harus muncul "redis"

Perbaikan:

dnf install -y php-pecl-redis5
systemctl restart php-fpm

Masukkan pemeriksaan ini ke checklist upgrade PHP Anda. Ini adalah jenis kerusakan yang membuat seluruh rumah sakit tidak bisa login, dengan penyebab yang sama sekali tidak terlihat dari gejalanya.

9.6 Gejala mirip yang BUKAN masalah Redis

Kalau hanya satu browser/komputer yang mendapat error 500 di setiap panggilan /webservice (sementara komputer lain baik-baik saja), itu bukan Redis. Itu PHPSESSID basi yang ditolak Laminas\Session\Validator\Id. Pola ini berasal dari session.sid_bits_per_character — nilai 5 hanya menghasilkan karakter [0-9a-v], jadi session ID lama yang mengandung w/x/y/z ditolak. Solusi: hapus cookie di browser tersebut. Kalau session.use_strict_mode = 0, masalah ini tidak sembuh sendiri.


10. Resep siap pakai (per ukuran server & per peran)

Bagian ini bisa langsung disalin. Pilih peran server dulu, baru ukuran RAM + jumlah pengguna.

10.1 Cara memilih resep

Langkah 1 — server Anda perannya apa?

PeranIsinyaPakai resep
Semua-dalam-satuhttpd + php-fpm + mysqld + tomcat di satu mesin10.2 / 10.3
Server aplikasihttpd + php-fpm (+ redis). DB & Jasper di tempat lain10.4
Server databasemysqld saja10.5
Server laporantomcat + httpd + php-fpm ringan10.6

Langkah 2 — berapa pengguna bersamaan?

Yang dihitung adalah petugas yang login dan aktif bekerja pada jam yang sama, bukan total akun.

Cara mengukur cepat (jam sibuk, mis. jam 10 pagi):

# jumlah session aktif (kalau sudah pakai Redis)
redis-cli --scan --pattern 'PHPREDIS_SESSION:*' | wc -l

# kalau masih pakai file session
find /var/lib/php/session -name 'sess_*' -mmin -15 | wc -l

Kalau belum ada datanya, perkiraan kasar untuk SIMGos:

Skala fasilitasPerkiraan pengguna bersamaan di jam puncak
Klinik pratama5–15
Klinik utama / RS tipe D kecil15–40
RS tipe C40–100
RS tipe B100–250
RS tipe A / RS besar multi-gedung250–600

Langkah 3 — dua rumus yang mengikat semuanya:

Apache MaxRequestWorkers   ≈ pengguna bersamaan × 2      (minimal 100)
PHP-FPM pm.max_children    ≈ MaxRequestWorkers × 0,5     (naikkan sampai 1,0 kalau RAM longgar)
MySQL max_connections      = pm.max_children + 50

Kenapa max_children boleh setengah dari MaxRequestWorkers? Tidak semua permintaan butuh PHP. Apache juga melayani file statis ExtJS (.js, .css, gambar) dan menahan koneksi keep-alive yang menganggur — itu semua tidak menyentuh PHP sama sekali. Menyamakan keduanya (seperti di RS-2) hanya perlu kalau RAM Anda memang berlimpah.


10.2 CONTOH LENGKAP — 32 GB RAM, <100 pengguna, semua-dalam-satu

Profil: 8 vCPU, 32 GB RAM, SSD. httpd + php-fpm + mysqld + tomcat + redis dalam satu server. Database ± 8 GB. Setara RS tipe C.

Perhitungannya:

Pengguna bersamaan   : 100
MaxRequestWorkers    : 100 × 2 = 200   → ServerLimit 8 × ThreadsPerChild 25 = 200 ✓
pm.max_children      : 200 × 0,5 = 100
max_connections      : 100 + 50 = 150  → bulatkan 200
innodb_buffer_pool   : DB 8 GB × 1,2 = 9,6 → bulatkan 10G
                       (cek batas: 32 GB × 35% = 11 GB ✓ masih di bawah)
Tomcat Xmx           : 2G

Anggaran RAM:

OS + systemd + cron           2,0 GB
MySQL (10G × 1,3)            13,0 GB
PHP-FPM (100 × 20 MB)         2,0 GB
Tomcat (2G + metaspace)       2,8 GB
Apache                        0,5 GB
Redis                         0,5 GB
Opcache                       0,3 GB
                            ---------
Total                        21,1 GB dari 32 GB  →  sisa 10,9 GB ✓

Sisa 10 GB itu bukan pemborosan — Linux memakainya sebagai page cache untuk file database, dan itu memberi ruang aman kalau ada laporan berat mendadak.

File 1 — /etc/httpd/conf.d/mpm-event.conf

# SIMGos tuning — 32 GB / 8 vCPU / <100 pengguna
<IfModule mpm_event_module>
    StartServers             4
    ServerLimit              8
    ThreadsPerChild          25
    MaxRequestWorkers        200
    MinSpareThreads          25
    MaxSpareThreads          75
    MaxConnectionsPerChild   10000
</IfModule>

Timeout               60
KeepAlive             On
KeepAliveTimeout      5
MaxKeepAliveRequests  100

# jangan bocorkan versi
ServerTokens Prod
ServerSignature Off

File 2 — /etc/php-fpm.d/www.conf (bagian yang diubah)

pm = ondemand
pm.max_children = 100
pm.process_idle_timeout = 30s
pm.max_requests = 500

pm.status_path = /php-fpm-status
ping.path = /php-fpm-ping

request_slowlog_timeout = 10s
slowlog = /var/log/php-fpm/www-slow.log

; JANGAN pasang request_terminate_timeout di sini —
; operasi BPJS/INACBG yang sah bisa berjalan lama.

; Sesi Redis (lihat bagian 9 — cutover memutus semua login sekali)
php_value[session.save_handler] = redis
php_value[session.save_path]    = "tcp://127.0.0.1:6379"

File 3 — /etc/php.ini (baris yang diubah)

memory_limit         = 256M
max_execution_time   = 120
post_max_size        = 32M
upload_max_filesize  = 32M
max_input_vars       = 5000
realpath_cache_size  = 4M
realpath_cache_ttl   = 600
expose_php           = Off

File 4 — /etc/php.d/10-opcache.ini

zend_extension=opcache
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=1
opcache.revalidate_freq=2
opcache.save_comments=1
opcache.fast_shutdown=1

File 5 — /etc/my.cnf.d/simgos.cnf

# SIMGos tuning — 32 GB / 8 vCPU / <100 pengguna / DB ~8 GB
[mysqld]

# === Memori ===
innodb_buffer_pool_size        = 10G
innodb_buffer_pool_instances   = 8
innodb_redo_log_capacity       = 2G
innodb_log_buffer_size         = 64M

# === Disk I/O (SSD) ===
innodb_flush_method            = O_DIRECT
innodb_flush_log_at_trx_commit = 2
innodb_io_capacity             = 1000
innodb_io_capacity_max         = 2000
innodb_flush_neighbors         = 0

# === Koneksi ===
max_connections                = 200
thread_cache_size              = 32
table_open_cache               = 4000
table_definition_cache         = 2000

# === Buffer per sesi (kecil — dikalikan jumlah koneksi) ===
sort_buffer_size               = 2M
join_buffer_size               = 2M
read_buffer_size               = 256K
read_rnd_buffer_size           = 512K

# === Tabel sementara (laporan SIMGos) ===
tmp_table_size                 = 256M
max_heap_table_size            = 256M

# === Keamanan & visibilitas ===
skip_name_resolve              = 1
max_allowed_packet             = 64M
slow_query_log                 = 1
slow_query_log_file            = /var/log/mysql-slow.log
long_query_time                = 2
log_queries_not_using_indexes  = OFF
max_execution_time             = 120000    # 2 menit — rem laporan liar

performance_schema             = ON
innodb_buffer_pool_dump_at_shutdown = 1
innodb_buffer_pool_load_at_startup  = 1

File 6 — /etc/systemd/system/mysqld.service.d/limits.conf

[Service]
LimitNOFILE=32000

File 7 — /opt/tomcat/bin/setenv.sh

CATALINA_OPTS="$CATALINA_OPTS -Xms1G -Xmx2G"
CATALINA_OPTS="$CATALINA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
CATALINA_OPTS="$CATALINA_OPTS -XX:MaxMetaspaceSize=384m"
CATALINA_OPTS="$CATALINA_OPTS -Djava.awt.headless=true"
CATALINA_OPTS="$CATALINA_OPTS -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/tomcat/logs"
CATALINA_OPTS="$CATALINA_OPTS -XX:+ExitOnOutOfMemoryError"
CATALINA_OPTS="$CATALINA_OPTS -Dnet.sf.jasperreports.governor.max.pages=10000"
CATALINA_OPTS="$CATALINA_OPTS -Dnet.sf.jasperreports.governor.timeout=300000"
export CATALINA_OPTS

/opt/tomcat/conf/server.xml — connector:

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           maxThreads="20" acceptCount="50"
           redirectPort="8443" />

(20 laporan bersamaan sudah banyak untuk heap 2 GB — masing-masing laporan berat bisa memakan 100–300 MB.)

File 8 — /etc/redis/redis.conf (tambahan di akhir)

bind 127.0.0.1 -::1
protected-mode yes
maxmemory 512mb
maxmemory-policy allkeys-lru
save 3600 1
save 300 100
save 60 10000

File 9 — /etc/sysctl.d/99-simgos.conf

net.core.somaxconn = 4096
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 10000 65535
vm.swappiness = 10

⚠️ Kalau server ini adalah kontainer LXC, hapus baris vm.swappiness — harus disetel di host Proxmox, di dalam kontainer hanya gagal diam-diam.

Menerapkan

httpd -t && php-fpm -t && mysqld --validate-config
systemctl restart mysqld && systemctl restart php-fpm && \
systemctl reload httpd && systemctl restart tomcat

10.3 Resep semua-dalam-satu, ukuran lain

Struktur filenya sama persis seperti [10.2] — yang berubah hanya angkanya.

Profil8 GB / 4 vCPU<br><25 pengguna16 GB / 4 vCPU<br><50 pengguna32 GB / 8 vCPU<br><100 pengguna64 GB / 8–16 vCPU<br><250 pengguna96–128 GB / 8–16 vCPU<br>250+ pengguna
Apache ServerLimit4681632
Apache MaxRequestWorkers100150200400800
Apache StartServers22448
PHP-FPM pmondemandondemandondemandondemandondemand
PHP-FPM max_children2550100250600
Opcache MB128192256256320
MySQL buffer_pool2G5G10G24G48G
MySQL pool_instances24888
MySQL max_connections80100200300700
MySQL redo_log_capacity512M1G2G4G8G
MySQL tmp_table_size64M128M256M256M512M
MySQL LimitNOFILE1000016000320003200032000
Tomcat -Xms/-Xmx256m / 512m512m / 1G1G / 2G2G / 4G2G / 6G
Tomcat maxThreads1015203040
Redis maxmemory256mb256mb512mb1gb2gb
Sesi Redisopsionaldisarankanwajibwajibwajib
Perkiraan RAM terpakai~5,5 GB~11 GB~21 GB~44 GB~72 GB

Referensi yang sudah terbukti di lapangan:

ServerProfilSetelan
Klinik-A / Klinik-A23 vCPU, 10 GB, 10–15 pasien/hariApache SL4=100 · fpm 20 · bp 5G/4 inst · max_conn 64 · Tomcat 512m
Klinik-B3 vCPU, 12 GB, 20–30 pasien/hariApache SL6=150 · fpm 28 · bp 6G/6 inst · max_conn 64 · Tomcat 512m
Klinik-C4 vCPU, 14 GB (LXC)fpm 100 · bp 6G · Tomcat 1G
RS-24 vCPU, 96 GB, puncak 105 req/detikApache SL32=800 · fpm 800 · bp 48G · max_conn 1000

Perhatikan: kedua klinik di atas memakai max_connections = 64 dengan max_children = 20–28. Cadangannya lebih kecil dari rumus "+50" karena di server sekecil itu hanya ada beberapa cron. Sesuaikan dengan jumlah integrasi yang aktif (SATUSEHAT, BPJS, aplicares, RSOnline, SITB — masing-masing memakai koneksi sendiri).


10.4 Peran: server APLIKASI khusus

Isinya: httpd + php-fpm + redis. Database dan Jasper ada di server lain.

Yang berubah dibanding semua-dalam-satu:

  • Tidak ada buffer pool MySQL → hampir seluruh RAM boleh untuk PHP
  • RAM jarang menjadi batas. Pegawai PHP hanya ± 20 MB (USS). Yang mengikat adalah CPU dan kapasitas server database
  • Sesi Redis menjadi wajib, apa pun ukurannya
  • Timeout proxy menjadi krusial — server ini menunggu server lain
Profil8 GB / 4 vCPU<br><100 pengguna16 GB / 8 vCPU<br><250 pengguna32 GB / 16 vCPU<br>250+ pengguna
Apache ServerLimit81632
Apache MaxRequestWorkers200400800
PHP-FPM max_children150350700
Opcache MB256256320
Redis maxmemory512mb1gb2gb
RAM terpakai~6 GB~12 GB~22 GB

⚠️ Angka max_children di sini harus dijumlahkan lintas server kalau ada lebih dari satu server aplikasi. Dua server aplikasi masing-masing 350 → server database harus menyanggupi 700 + 50 = 750 koneksi.

/etc/php-fpm.d/www.conf

pm = ondemand
pm.max_children = 150
pm.process_idle_timeout = 30s
pm.max_requests = 500
pm.status_path = /php-fpm-status

request_slowlog_timeout = 10s
slowlog = /var/log/php-fpm/www-slow.log

; ⚠️ JANGAN pasang request_terminate_timeout global di sini.
; Di RS-1 ini sengaja dikosongkan — operasi BPJS/INACBG/tagihan
; memang berdurasi panjang dan akan ikut terbunuh.

php_value[session.save_handler] = redis
php_value[session.save_path]    = "tcp://127.0.0.1:6379"

; MySQL ada di server lain — beri batas waktu koneksi
php_value[mysql.connect_timeout] = 10
php_value[default_socket_timeout] = 120

Timeout proxy — beda per endpoint

# /etc/httpd/conf.d/php.conf

# Panggilan ke server laporan — selaras dengan
# request_terminate_timeout=600 di server laporan
<Location "/webservice/report">
    ProxySet timeout=600
</Location>

# ⚠️ Pembayaran / INACBG sengaja dibiarkan panjang.
# Memotongnya = transaksi tagihan gagal.
<Location "/webservice/pembayaran-tagihan">
    ProxySet timeout=7200
</Location>

Kenapa harus dibedakan: di RS-1, timeout laporan pernah 3600 detik sementara server laporan sudah menyerah di detik ke-600. Selisihnya berarti koneksi menggantung sia-sia selama 50 menit sambil menahan slot pegawai — sedikit demi sedikit seluruh pool habis dan seluruh situs ikut lemot. Ini disebut back-pressure: kalau tetangga Anda menyerah, Anda harus ikut menyerah pada waktu yang sama.

Pool terpisah (opsional, disarankan mulai 250 pengguna)

Pisahkan lalu lintas yang berbeda sifatnya supaya laporan berat tidak menghabiskan pegawai untuk pendaftaran. Pola ini sudah dipakai di RS-3 (www, report, pembayaran):

# /etc/php-fpm.d/report.conf
[report]
listen = /run/php-fpm/report.sock
pm = ondemand
pm.max_children = 30
request_terminate_timeout = 600s      ; boleh dipasang: ini khusus laporan

⚠️ Setiap pool wajib punya baris session Redis-nya sendiri. Kalau tertinggal, pool itu diam-diam kembali ke session file — dan masalah kunci file muncul lagi hanya di sebagian layar saja, yang jauh lebih sulit didiagnosis.


10.5 Peran: server DATABASE khusus

Isinya: mysqld saja. Tidak ada PHP, tidak ada Apache.

Yang berubah:

  • Buffer pool boleh 60–70% RAM (bukan 30–40%)
  • max_connections = jumlah max_children semua server aplikasi + 50
  • skip_name_resolve = 1 mewajibkan semua grant memakai IP, bukan hostname
  • Backup dan rem laporan menjadi tanggung jawab utama server ini
Profil16 GB / 4 vCPU<br>DB ≤ 10 GB32 GB / 8 vCPU<br>DB ≤ 20 GB64 GB / 16 vCPU<br>DB ≤ 45 GB128 GB / 16+ vCPU<br>DB ≤ 90 GB
innodb_buffer_pool_size10G20G40G80G
innodb_buffer_pool_instances8888
innodb_redo_log_capacity2G4G8G16G
max_connections2004008001200
table_open_cache4000600080008000
tmp_table_size256M512M1G1G
thread_cache_size3264128128
LimitNOFILE32000320006500065000
RAM terpakai~14 GB~27 GB~54 GB~106 GB

Aturan buffer pool: ambil yang lebih kecil antara ukuran DB × 1,2 dan RAM × 0,7. Melebihi ukuran DB tidak menambah kecepatan sama sekali. RS-2 punya pool 48 GB untuk DB 25 GB — hanya 11% halaman terpakai, hit rate 99,98%. Tidak berbahaya, tapi 20 GB itu bisa dipakai untuk hal lain.

/etc/my.cnf.d/simgos.cnf — server DB khusus, 64 GB

[mysqld]
# === Memori ===
innodb_buffer_pool_size        = 40G
innodb_buffer_pool_instances   = 8
innodb_redo_log_capacity       = 8G
innodb_log_buffer_size         = 128M

# === Disk I/O ===
innodb_flush_method            = O_DIRECT
innodb_flush_log_at_trx_commit = 2
innodb_io_capacity             = 2000
innodb_io_capacity_max         = 4000
innodb_flush_neighbors         = 0
innodb_read_io_threads         = 8
innodb_write_io_threads        = 8
innodb_purge_threads           = 4

# === Koneksi (= Σ max_children semua server app + 50) ===
max_connections                = 800
thread_cache_size              = 128
table_open_cache               = 8000
table_definition_cache         = 4000
skip_name_resolve              = 1      # ⚠️ semua grant HARUS pakai IP

# === Buffer per sesi ===
sort_buffer_size               = 2M
join_buffer_size               = 2M
read_buffer_size               = 256K
read_rnd_buffer_size           = 512K

# === Laporan berat ===
tmp_table_size                 = 1G
max_heap_table_size            = 1G
max_execution_time             = 300000   # 5 menit — WAJIB (lihat 7.7)

# === Visibilitas ===
slow_query_log                 = 1
slow_query_log_file            = /var/log/mysql-slow.log
long_query_time                = 2
log_queries_not_using_indexes  = OFF      # ⚠️ jangan ON (lihat 7.6)
log_slow_admin_statements      = OFF
performance_schema             = ON

innodb_buffer_pool_dump_at_shutdown = 1
innodb_buffer_pool_load_at_startup  = 1
max_allowed_packet             = 128M

Wajib ada di server DB khusus

1. Batas file:

# /etc/systemd/system/mysqld.service.d/limits.conf
[Service]
LimitNOFILE=65000

2. Backup yang tidak bisa disandera laporan:

xtrabackup --backup \
  --ftwrl-wait-timeout=120 \
  --kill-long-queries-timeout=120 \
  --kill-long-query-type=all \
  ...

Tanpa tiga baris ini, satu CetakMR2 yang tertahan menggagalkan backup semalam penuh. Sudah terjadi 5 kali di server-database RS-1.

3. Pemantauan query panjang (server-database RS-1 memakai /usr/local/bin/mysql-monitor.sh tiap 5 menit, memberi peringatan Telegram untuk query >300 detik dengan tombol "KILL"). Minimal, pasang cron sederhana:

*/5 * * * * root mysql -uroot -sNe "SELECT CONCAT(id,' ',time,'s ',LEFT(info,80)) FROM information_schema.processlist WHERE command!='Sleep' AND time>300" | logger -t mysql-longquery

4. ⚠️ Jangan pakai jemalloc. Terlihat sangat menggoda (di server-database RS-1: RSS turun 47 GB → 15 GB, swap 8,8 GB → 0). Tapi 3 jam kemudian mysqld SIGSEGV saat backup malam menjalankan FLUSH TABLES, dan backup malam itu gagal total. Penyebabnya bug use-after-free di MySQL 8.0.46 saat membongkar trigger/stored-procedure — glibc menutupinya, jemalloc membongkarnya. Sudah dikembalikan ke glibc dan tidak boleh dicoba lagi sampai jumlah trigger dikurangi.

5. Periksa beban tersembunyi sebelum menambah RAM:

mysql -uroot -e "SELECT TRIGGER_SCHEMA, COUNT(*) FROM information_schema.TRIGGERS GROUP BY 1 ORDER BY 2 DESC;"
mysql -uroot -e "SELECT db,name,status,last_executed FROM mysql.event;"

Di server-database RS-1, 78 trigger SATUSEHAT membuat satu UPDATE sederhana memeriksa 6–110 juta baris. Tidak ada jumlah RAM yang bisa memperbaiki itu.


10.6 Peran: server LAPORAN khusus

Isinya: tomcat (JavaBridge + JasperReports) + httpd + php-fpm ringan.

Yang berubah:

  • Tomcat mendapat porsi RAM terbesar (50–60%), bukan MySQL
  • request_terminate_timeout wajib dipasang di sini (kebalikan server aplikasi)
  • Server ini memakai session file lokal dan otentikasi berbasis IPtidak ikut Redis server aplikasi, jadi tidak perlu diubah
  • Batasi maxThreads Tomcat: lebih baik laporan mengantre daripada semua gagal karena kehabisan heap
Profil8 GB / 4 vCPU<br>laporan ringan16 GB / 8 vCPU<br>sedang24 GB / 16 vCPU<br>berat
Tomcat -Xms1G2G4G
Tomcat -Xmx4G8G12G
Tomcat MaxMetaspaceSize384m512m512m
Connector maxThreads254050
Connector acceptCount5075100
PHP-FPM max_children3060100
PHP-FPM request_terminate_timeout600s600s600s
Jasper governor.timeout300000 (5 mnt)480000 (8 mnt)480000 (8 mnt)
Jasper governor.max.pages50001000010000
Apache MaxRequestWorkers100150200
RAM terpakai~6 GB~11 GB~16 GB

(Kolom paling kanan = nilai riil server-laporan RS-1.)

/opt/tomcat/bin/setenv.sh — 24 GB

CATALINA_OPTS="$CATALINA_OPTS -Xms4G -Xmx12G"
CATALINA_OPTS="$CATALINA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
CATALINA_OPTS="$CATALINA_OPTS -XX:MaxMetaspaceSize=512m"
CATALINA_OPTS="$CATALINA_OPTS -Djava.awt.headless=true"
CATALINA_OPTS="$CATALINA_OPTS -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/tomcat/logs"
CATALINA_OPTS="$CATALINA_OPTS -XX:+ExitOnOutOfMemoryError"
CATALINA_OPTS="$CATALINA_OPTS -Dnet.sf.jasperreports.governor.max.pages=10000"
CATALINA_OPTS="$CATALINA_OPTS -Dnet.sf.jasperreports.governor.timeout=480000"
export CATALINA_OPTS

⚠️ Kalau ada baris -Djava.rmi.server.hostname=... (untuk JMX), jangan diubah — di server-laporan RS-1 itu dipatok ke IP tertentu dan mengubahnya memutus pemantauan.

/etc/systemd/system/tomcat.service

[Service]
Type=forking
PIDFile=/opt/tomcat/temp/tomcat.pid
Restart=on-failure
RestartSec=10
UMASK=0002

/etc/php-fpm.d/www.conf — server laporan

pm = ondemand
pm.max_children = 100
pm.process_idle_timeout = 30s

; ⚠️ DI SINI WAJIB — laporan yang tertahan harus dibunuh
request_terminate_timeout = 600s

request_slowlog_timeout = 10s
slowlog = /var/log/php-fpm/www-slow.log

www-slow.log adalah alat diagnosa terpenting di server laporan — setiap permintaan >10 detik dicatat lengkap dengan jalur script dan backtrace-nya, sehingga Anda tahu laporan mana yang bermasalah.

Yang wajib ada di server laporan

# 1. Izin sudo (tanpa ini SEMUA cetak gagal — lihat jebakan #18)
echo 'apache ALL=(ALL) NOPASSWD: ALL' > /tmp/s && visudo -cf /tmp/s \
  && install -m 0440 -o root -g root /tmp/s /etc/sudoers.d/simgos

# 2. Buang aplikasi bawaan Tomcat
rm -rf /opt/tomcat/webapps/{docs,examples,host-manager,manager}

# 3. Journal permanen (log bertahan setelah reboot)
mkdir -p /var/log/journal && systemd-tmpfiles --create --prefix /var/log/journal
systemctl restart systemd-journald

📌 Runbook operator: "server laporan macet"

Tempel ini di dekat server.

  1. JANGAN tekan tombol power. Di RS-1, operator menekannya 8 kali dalam sehari — setiap kali memutus JavaBridge dan membuat semua cetak dari server aplikasi menjadi 404.
  2. Jalankan: systemctl restart tomcat. Cukup. Sistem operasi tetap hidup, Apache tidak perlu disentuh.
  3. Kalau belum pulih dalam 30 detik:
    tail -50 /opt/tomcat/logs/catalina.out
    ls -lh /opt/tomcat/logs/java_pid*.hprof     # ada file = benar kehabisan memori
    tail -f /var/log/php-fpm/www-slow.log       # laporan mana yang macet
    

⚠️ Governor bukan jaminan penuh. Dia tidak menangkap kehabisan memori yang terjadi karena driver MySQL menampung hasil query raksasa sebelum Jasper mulai bekerja. Obat sesungguhnya ada di aplikasi: batasi rentang tanggal. Laporan terberat di RS-1 (LaporanFarmasiPerPasien) punya filter tanggal, tapi rentangnya dipilih user — dan ada yang meminta setahun sekaligus (output 142 MB).


10.7 Yang berubah saat layanan dipisah

Checklist saat memindahkan MySQL atau Tomcat ke server sendiri:

HalTindakan
Grant MySQLKarena skip_name_resolve = 1, semua grant harus pakai IP: GRANT ... TO 'simgos'@'192.168.0.10'. Grant berbasis hostname akan gagal login.
FirewallBuka hanya dari IP server aplikasi: MySQL 3306, Tomcat 8080. Jangan buka ke internet.
Config SIMGoswebservice/config/autoload/local.phphostname diarahkan ke IP server DB (bukan 127.0.0.1).
Timeout koneksiJaringan bisa putus; pasang mysql.connect_timeout = 10 di php.ini supaya tidak menggantung selamanya.
Timeout proxySelaraskan dengan request_terminate_timeout server tujuan (lihat 10.4). Selisih = koneksi menggantung.
Sesi RedisPasang Redis di server aplikasi (localhost), bukan di server DB. Server laporan tidak ikut — dia pakai otentikasi berbasis IP.
NTPSemua server harus sinkron jam. Selisih jam mengacaukan validasi session dan penelusuran log lintas server.
Zona waktu log⚠️ mysqld.log memakai UTC, log backup dan Apache biasanya WIB (+07:00). 21:35Z = 04:35 WIB. Wajib diingat saat mencocokkan kejadian antar-server.
BackupJadwalkan di server DB, dan pastikan tidak bentrok dengan restart terjadwal (di RS-1, restart 06:00 pernah menggagalkan backup yang masih menggantung sejak 02:00).
PemantauanSetiap server punya slow log sendiri. Saat mendiagnosis lemot, periksa ketiganya — penyebabnya sering di server yang tidak Anda curigai.

11. Cara menerapkan dengan aman

Urutan

  1. Backup semua file config + bundle tar
  2. Terapkan perubahan
  3. Validasi sintakshttpd -t, php-fpm -t, mysqld --validate-config
  4. Restart dengan urutan: mysqldphp-fpmhttpdtomcat (dari belakang ke depan; kalau terbalik, Apache menerima permintaan padahal PHP belum siap)
  5. Verifikasi (bagian 12)
  6. Pantau 24 jam

Kapan

PerubahanButuhJam
max_connections naikSET GLOBALkapan saja
Setelan php-fpmreloadkapan saja
MaxRequestWorkers (tanpa ubah ServerLimit)reload httpdkapan saja
ServerLimitrestart httpdjam sepi
Setelan MySQL InnoDBrestart mysqldjam sepi
Cutover session ke Redisreload php-fpmjam sepi — semua ter-logout
Heap Tomcatrestart tomcatkapan saja (laporan terputus)

Cari jendela maintenance yang sudah ada. RS-2 punya /root/weeklymtc.sh setiap Jumat 00:01 yang sudah menghentikan-menghidupkan httpd php-fpm mysqld. Itu jendela gratis untuk perubahan yang butuh restart penuh — tidak perlu bikin downtime baru.

Script

skrip tuning otomatis bisa dijadikan contoh. Sifat pentingnya:

  • Idempoten — dijalankan berulang hasilnya sama (menghapus blok lama lalu menulis ulang)
  • Backup dulu — per file .bak-<stempel> + bundle tar di /root/
  • Validasi sebelum restarthttpd -t dan php-fpm -t sebelum menyentuh service
  • Blok bertanda — setiap bagian dibungkus komentar # managed by ... supaya jelas mana yang dikelola script

Rollback

# php-fpm
cp /etc/php-fpm.d/www.conf.bak-<stempel> /etc/php-fpm.d/www.conf
systemctl reload php-fpm

# apache
rm /etc/httpd/conf.d/mpm-event.conf
systemctl reload httpd

# mysql
rm /etc/my.cnf.d/simgos.cnf
systemctl restart mysqld

# semuanya
tar -xzf /root/tuning-backup/pre-<stempel>.tgz -C /

12. Verifikasi setelah tuning

12.1 Semua layanan hidup

systemctl is-active mysqld php-fpm httpd tomcat
free -h

12.2 Setelan benar-benar berlaku

MySQL (bukan sekadar tertulis di file):

mysql -uroot -e "SHOW VARIABLES WHERE Variable_name IN (
 'innodb_buffer_pool_size','innodb_buffer_pool_instances',
 'max_connections','tmp_table_size','max_execution_time',
 'slow_query_log','skip_name_resolve');"

Apache — pastikan tidak ada AH00515:

grep AH00515 /var/log/httpd/error_log | tail -5

Kosong = benar. Ada = MaxRequestWorkers Anda melebihi ServerLimit × ThreadsPerChild, dan Apache diam-diam memakai angka yang lebih kecil.

PHP-FPM:

php-fpm -tt 2>&1 | grep -E "max_children|pm ="
php-fpm -m | grep -E "redis|opcache"

Tomcat:

ps -ef | grep java | tr ' ' '\n' | grep -E "^-X"

12.3 Ukur perbaikannya

MetrikPerintahTarget
Permintaan lambat php-fpmwc -l /var/log/php-fpm/www-slow.logturun drastis
Query lambat MySQLwc -l /var/log/mysql-slow.logmendekati 0 di jam kerja
Buffer pool hit ratelihat [7.3]≥ 99%
Load averageuptime< jumlah CPU
Swap terpakaifree -h0
Pegawai php-fpm puncakpgrep -c php-fpm< 70% dari max_children
AH00515grep error_logtidak ada

12.4 Pantau 24 jam pertama

watch -n5 'echo "load: $(uptime | awk -F"load average:" "{print \$2}")";
           echo "fpm : $(pgrep -c php-fpm)";
           echo "httpd: $(pgrep -c httpd)";
           echo "mysql: $(mysql -uroot -sNe "SHOW STATUS LIKE \"Threads_connected\"" | cut -f2)";
           free -h | head -2'

13. Kesalahan yang sering terjadi

Semua ini kejadian nyata di server produksi.

#KesalahanAkibatPencegahan
1pm.max_children = 2156 bawaan installerKlinik-C reboot 7× dalam 19 hari (butuh 84 GB untuk server 14 GB)Selalu periksa nilai ini setelah instalasi baru
2Menghitung RAM dari RSSPerkiraan meleset 4× lipat, keputusan kapasitas salahPakai USS dari /proc/<pid>/smaps_rollup
3Dua baris max_connections di my.cnfBaris terakhir menang; nilai 2048 di baris 20 mati diam-diamgrep -n cari duplikat sebelum mengubah
4MaxRequestWorkers > ServerLimit × ThreadsPerChildApache diam-diam memakai angka kecil + warning AH00515 setiap start selama berbulan-bulanHitung dan samakan; cek error log
5max_children (800) > max_connections (500)Bertukar dari "lemot" menjadi Too many connectionsmax_connections ≥ max_children + 50
6Naikkan max_connections tanpa LimitNOFILEMySQL kehabisan file descriptorDrop-in systemd LimitNOFILE=32000
7Session file (flock)33.712 permintaan lambat/hari, 85% macet di satu baris kode; DB menganggur totalPindah ke Redis session
8Upgrade PHP tanpa pasang ulang phpredisSeluruh RS tidak bisa login (loop halaman login)php-fpm -m | grep redis setelah SETIAP upgrade PHP
9Baris save Redis dikomentari (bawaan EL9)Restart Redis = semua session hilangPulihkan baris save
10Tomcat -Xmx1024M + SerialGC di server 23 GBTomcat "hang", operator menekan tombol power 8× sehariHeap memadai + G1GC + ExitOnOutOfMemoryError
11Type=forking tanpa PIDFileRestart=on-failure tidak pernah jalanTambahkan PIDFile=
12Log vhost di luar /var/log/httpd/requests.log = 258 GB selama 2 tahunBuat aturan logrotate sendiri; rotasi pertama -f
13log_queries_not_using_indexes = ONSlow log 14 GB, 27.800 query yang benar-benar lambat tersembunyiMatikan; andalkan long_query_time
14Laporan tanpa batas waktuCetakMR2 jalan 10–14 jam → backup malam gagal berulang kalimax_execution_time + --kill-long-queries-timeout
15Menyimpulkan "kurang index" tanpa EXPLAINSalah arah; ternyata rentetan 78 triggerEXPLAIN dulu; hitung trigger sebagai root
16information_schema.TRIGGERS sebagai user biasaTerlihat 26 dari 460 triggerJalankan sebagai root
17Mencoba jemalloc untuk mengatasi fragmentasi memoriRAM turun 47→15 GB, tapi mysqld SIGSEGV saat backup malam → backup gagalJangan pakai jemalloc di MySQL 8.0.46 dengan banyak trigger. Kalau tetap ingin, uji di server non-produksi.
18Menghapus apache NOPASSWD demi keamananSemua cetak laporan mati, HTTP 200 Content-Length 0, lolos dari monitoringPasang /etc/sudoers.d/simgos; masukkan ke checklist migrasi
19request_terminate_timeout global di server aplikasiTransaksi BPJS/INACBG yang sah ikut terbunuhHanya di server laporan, atau pakai pool terpisah
20Titik koma di akhir nilai config (= 30s;)Nilai tidak berlaku, tanpa errorPerhatikan; validasi dengan php-fpm -tt
21Ukur beban jam 01:0070 pegawai dikira normal; siang hari ~10× lipatSampel jam sibuk, atau pasang cron sampler seminggu
22Percaya Max_used_connectionsTidak berguna kalau mysqld baru direstart tadi malamCek Uptime dulu
23copytruncate pada log raksasaMenyalin 258 GBRename + reload; tunggu fd terlepas; baru kompres
24ionice -c 3 (idle) untuk kompresiMelambat jadi 5 MB/detik (ETA 12 jam)Pakai ionice -c 2 -n 7 (12 MB/detik)
25Drop-in my.cnf.d diabaikanTidak ada !includedir di /etc/my.cnfgrep includedir /etc/my.cnf dulu
26Setelan sysctl vm.* di dalam kontainer LXCRead-only, gagal diam-diamSetel di host Proxmox, bukan di kontainer

14. Diagnosa cepat: gejala → penyebab

"SIMGos lemot seharian, semua user"

Periksa berurutan — jangan langsung ke database:

# 1. Session file? (paling sering)
grep -c "session\|Resource.php" /var/log/php-fpm/www-slow.log
php -i | grep session.save_handler

Kalau files dan slow log penuh → bagian 9.

# 2. Pegawai PHP habis?
grep "reached pm.max_children" /var/log/php-fpm/error.log | tail
# 3. Database benar-benar sibuk?
mysql -uroot -e "SHOW STATUS LIKE 'Threads_running'; SHOW PROCESSLIST;" | head -30

Threads_running ≤ 5 → database tidak bersalah, cari di PHP.

# 4. Server laporan menahan koneksi?
tail -50 /var/log/php-fpm/www-slow.log
curl -s -o /dev/null -w "%{http_code} %{time_total}\n" http://<report-server>:8080/JavaBridge/

"Cetak laporan gagal / PDF kosong"

# izin sudo hilang?
sudo -l -U apache | head
grep "Permission denied" /var/log/php-fpm/www-error.log | tail

→ jebakan #18

# Tomcat kehabisan memori?
ls -lh /opt/tomcat/logs/java_pid*.hprof
tail -50 /opt/tomcat/logs/catalina.out

→ bagian 8

"Server hang / restart sendiri"

journalctl -b -1 -p err | tail -50           # log boot sebelumnya
dmesg | grep -i "killed process"             # OOM Killer beraksi?
free -h                                      # swap terpakai?
grep -c "max_children" /etc/php-fpm.d/www.conf

Kalau Killed process ... mysqld → RAM terlalu diklaim berlebih. Cek jebakan #1 dan #2.

⚠️ Aktifkan journal permanen supaya log bertahan setelah reboot:

mkdir -p /var/log/journal && systemd-tmpfiles --create --prefix /var/log/journal
systemctl restart systemd-journald

"Backup malam gagal"

grep -iE "error|failed" /var/log/autobackup.log | tail
grep "Forcing close of thread" /var/log/mysqld.log | tail

Nomor thread yang sama muncul di keduanya → ada query berjam-jam yang memblokir. → bagian 7.7 & 7.8

⚠️ Waktu di mysqld.log adalah UTC, sedangkan log backup biasanya WIB (+07:00). 21:35Z = 04:35 WIB.

"Login berputar kembali ke halaman login"

php-fpm -m | grep redis        # kosong = penyebabnya
redis-cli ping
redis-cli dbsize               # 0 padahal ada yang login = tidak bisa menulis
grep "save handler" /var/log/php-fpm/www-error.log | tail

→ jebakan #8

"Satu komputer error 500 terus, yang lain normal"

Bukan masalah server. Hapus cookie di browser tersebut. → bagian 9.6


15. Lampiran: perintah harian

Pemeriksaan pagi (2 menit)

#!/bin/bash
echo "=== $(hostname) $(date) ==="
systemctl is-active mysqld php-fpm httpd tomcat | paste -sd' '
uptime
free -h | head -2
df -h / | tail -1
echo "php-fpm workers : $(pgrep -c php-fpm)"
echo "httpd workers   : $(pgrep -c httpd)"
echo "mysql conns     : $(mysql -uroot -sNe 'SHOW STATUS LIKE "Threads_connected"' | cut -f2)"
echo "slow php (24j)  : $(find /var/log/php-fpm/www-slow.log -mtime -1 2>/dev/null && wc -l < /var/log/php-fpm/www-slow.log)"
echo "AH00515         : $(grep -c AH00515 /var/log/httpd/error_log)"
echo "OOM kills       : $(dmesg | grep -ci 'killed process')"

Mencari 10 query paling lambat

mysqldumpslow -s t -t 10 /var/log/mysql-slow.log

⚠️ Jangan jalankan pada slow log berukuran belasan GB di jam kerja — sangat lambat.

Melihat apa yang sedang dikerjakan MySQL

mysql -uroot -e "
SELECT id, user, host, db, time, LEFT(info,120) AS query
FROM information_schema.processlist
WHERE command != 'Sleep' AND time > 5
ORDER BY time DESC;"

Membunuh query yang tertahan

mysql -uroot -e "KILL <id>;"

Melihat permintaan PHP yang macet

tail -f /var/log/php-fpm/www-slow.log

Setiap entri menunjukkan file dan baris kode persis di mana permintaan tersangkut. Ini alat diagnosa terbaik yang Anda punya — pastikan request_slowlog_timeout sudah aktif.

Status pool php-fpm

Aktifkan di vhost:

<LocationMatch "^/(php-fpm-status|php-fpm-ping)$">
    Require ip 127.0.0.1
    ProxyPass unix:/run/php-fpm/www.sock|fcgi://localhost
</LocationMatch>

Lalu:

curl -s localhost/php-fpm-status?full | head -30

Ringkasan sepuluh baris

  1. Backup dulu. Selalu (Anda sudah diingatkan berkali-kali).
  2. Ukur di jam sibuk, bukan tengah malam. Pakai USS, bukan RSS.
  3. Jaga rantainya: Apache ≥ PHP-FPM, MySQL > PHP-FPM + 50.
  4. Rem di Apache, bukan di database.
  5. buffer_pool = 30–40% RAM kalau berbagi server, 60–70% kalau MySQL sendirian.
  6. Pakai Redis session. Ini perbaikan tunggal terbesar untuk SIMGos.
  7. Setelah setiap upgrade PHP: php-fpm -m | grep redis.
  8. Tomcat: G1GC, heap memadai, governor, ExitOnOutOfMemoryError, PIDFile.
  9. Beri batas waktu pada laporan. Laporan tanpa rem akan menggagalkan backup Anda.
  10. Angka besar tidak membuat cepat. Ia hanya menunda tumbangnya server sampai jam sibuk berikutnya.

Disusun dari catatan penanganan nyata di 8 rumah sakit dan 6 klinik pengguna SIMGos di Indonesia. Nama fasilitas disamarkan. Angka pada tabel referensi adalah konfigurasi yang benar-benar berjalan di produksi.

Tidak ingin menghitung manual?

Masukkan RAM, jumlah CPU, jumlah pengguna, dan susunan server Anda — kalkulator akan menghasilkan seluruh file konfigurasi siap salin beserta anggaran RAM-nya. Semua perhitungan berjalan di browser Anda; spesifikasi yang Anda isi tidak pernah dikirim ke mana pun.

Buka kalkulator