Elinizdeki makineden başlayın
Aşağıdaki her ayar iki sayıya bağlı: sunucuda ne kadar RAM var ve bunun ne kadarı MySQL ile PHP’ye gidiyor. Bu hesabı yapmadan bir yazıdan yapılandırma kopyalamak, kampanya başlar başlamaz sunucunun swap’e düşmesinin sebebidir. Önce şu üç şeyi not edin.
free -m
# PHP-FPM işçisi başına ortalama bellek
ps --no-headers -o rss -C php-fpm8.2 | awk '{s+=$1; n++} END {printf "ortalama %.0f MB / %d işçi\n", s/n/1024, n}'
# InnoDB veri seti ne kadar?
mysql -e "SELECT ROUND(SUM(data_length+index_length)/1024/1024) AS innodb_mb
FROM information_schema.tables WHERE engine='InnoDB';"OpenCart 3.x ve 4.x’te bir işçi, her istekte yüklenen eklenti sayısına göre genelde 60–120 MB arasında oturur. Altmış eklentili bir mağaza ile altı eklentili bir mağaza aynı makine değildir. Aşağıdaki rakamlar barındırdığımız orta ölçekli bir ev tekstili mağazasından: mağazayı, veritabanını ve Redis’i tek makinede çalıştıran 8 GB, 4 vCPU’lu bir VPS.
- Örnek VPS
- 8 GB / 4 vCPU
- MySQL’e ayrılan
- 2,5 GB
- PHP-FPM’e kalan
- 4 GB
Nginx server bloğu
En sık yanlış kurulan parça SEO URL’ler. OpenCart web sunucusundan yönlendirme beklemez; yolun tamamının _route_ sorgu parametresiyle gelmesini ister — Apache için paketle gelen .htaccess de tam olarak bunu yapar. Tek bir try_files satırı bu işi görür ve satır 3.x ile 4.x’te aynıdır.
server {
listen 443 ssl;
http2 on; # nginx 1.25.1+; eski sürümlerde: listen 443 ssl http2;
server_name example.com www.example.com;
root /var/www/example.com/public;
index index.php;
client_max_body_size 32m; # admin ürün görseli yüklemeleri
# OpenCart SEO URL
location / {
try_files $uri $uri/ /index.php?_route_=$uri;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/opencart.sock;
fastcgi_read_timeout 120s;
fastcgi_buffer_size 32k;
fastcgi_buffers 16 16k;
}
location ~* \.(?:css|js|jpg|jpeg|png|gif|webp|avif|svg|ico|woff2)$ {
expires 30d;
add_header Cache-Control "public";
access_log off;
try_files $uri =404;
}
# yazılabilir dizinlerden PHP asla çalışmasın
location ~* ^/(image|system/storage)/.*\.php$ { deny all; }
location ^~ /system/storage/ { deny all; }
location ~ /\.(?!well-known) { deny all; }
}Deny kuralları göründüğünden önemli. Bir yükleme açığı bulan saldırganın ilk yaptığı iş image/ altına .php dosyası bırakmaktır; PHP orada çalışmıyorsa açık bir ihlale değil, bir rahatsızlığa dönüşür. system/storage ise logları, oturumları ve indirilebilir ürünleri tutar — Apache tarafında onu bir .htaccess korur ve Nginx o dosyayı hiç okumaz.
Admin dizinini bilinen adreslere kapatmak on dakikalık bir iş ve içinde bir tuzak var. ^~ önekli bir location, Nginx’e durup regex location’lara hiç bakmamasını söyler; yani o yolun altındaki PHP artık çalıştırılmaz, düz metin olarak indirilir — config.php dahil. Handler’ın blok içinde tekrarlanması şart.
limit_req_zone $binary_remote_addr zone=ocadmin:10m rate=30r/m;
location ^~ /admin/ {
allow 203.0.113.10; # ofis
allow 198.51.100.0/24; # vpn
deny all;
limit_req zone=ocadmin burst=20 nodelay;
location ~ \.php$ { # zorunlu: ^~ dıştaki regex bloğunu devre dışı bıraktı
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/opencart.sock;
fastcgi_read_timeout 300s; # dışa aktarma ve toplu düzenleme uzun sürer
}
}Sıkıştırma ve önbellek başlıkları
OpenCart sayfaları metin ağırlıklıdır: HTML, büyük bir stil dosyası, birkaç JavaScript paketi ve AJAX sepetten dönen JSON. Seviye 5 makul orta nokta; daha yukarı çıkmak bu boyuttaki dosyalarda çok az şey kazandırır.
gzip on;
gzip_vary on;
gzip_min_length 256;
gzip_comp_level 5;
gzip_types text/css text/xml application/javascript application/json
image/svg+xml application/rss+xml;
# ngx_brotli modülü derlenmişse
brotli on;
brotli_comp_level 5;
brotli_types text/css application/javascript application/json image/svg+xml;Takılınan iki ayrıntı: text/html gzip tarafından zaten her zaman sıkıştırılır ve gzip_types içinde yeri yoktur; JPEG, WebP, woff2 gibi hâlihazırda sıkıştırılmış biçimler ise iki listede de bulunmamalıdır. Web sunucusunda sıkıştırıyorsanız OpenCart’ın kendi çıktı sıkıştırma ayarını kapalı bırakın; aynı işi iki kez yapmak kazanç getirmez.
PHP-FPM havuzu ve pm.max_children hesabı
Yoğun bir OpenCart mağazasında “site kapandı” çağrılarının sebebi nadiren CPU’dur. İşçisi biten bir havuzdur: istekler sitedeki en yavaş sayfanın arkasında kuyruğa girer, Nginx 502 döner, grafiklerde ise canı sıkılan bir makine görürsünüz. Çözüm daha büyük bir sayı değil, doğru sayıdır.
PHP’ye kalan bellek bölü ortalama işçi boyutu. Örnek sunucuda MySQL, Redis ve işletim sisteminden sonra yaklaşık 4 GB kalıyor, işçiler ortalama 90 MB — aşağı yuvarlayarak 45 çocuk. Aynı makinede bunu 200 yapmak daha çok müşteriye hizmet etmez; bellek bitene kadar eder, sonra çekirdek süreçleri öldürmeye başlar.
; /etc/php/8.2/fpm/pool.d/opencart.conf
[opencart]
user = www-data
group = www-data
listen = /run/php/opencart.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 45
pm.start_servers = 10
pm.min_spare_servers = 8
pm.max_spare_servers = 16
pm.max_requests = 500
pm.status_path = /fpm-status
request_terminate_timeout = 120s
slowlog = /var/log/php-fpm/opencart-slow.log
request_slowlog_timeout = 5s
php_admin_value[memory_limit] = 256M
php_admin_value[upload_max_filesize] = 32M
php_admin_value[post_max_size] = 32MBu satırlardan ikisi çoğu izleme panosundan fazla teşhis işi görür. pm.max_requests bir işçiyi beş yüz istekten sonra yeniler; tek bir eklentideki yavaş sızıntı sınırsız büyüyemez. request_slowlog_timeout ise beş saniyeyi aşan her istek için tam bir PHP çağrı yığını yazar — her kategori sayfasında dış bir API’ye giden modülü genelde böyle buluyoruz.
- pm = dynamic: trafiği dalgalı normal bir vitrin için.
- pm = ondemand: staging ve düşük trafikli siteler; boşta kalan işçiler belleği bırakır.
- pm = static: yalnızca tek mağazaya ayrılmış makinede, işçi boyutunu ölçtükten sonra.
- /fpm-status üzerindeki listen queue değerini izleyin: sıfırın üstü, boş işçi bekleyen müşteri demektir.
OPcache ve realpath önbelleği
OpenCart binlerce küçük PHP dosyasıdır; 4.x bunun üstüne derlenmiş Twig şablonları ekler. OPcache olmadan her istek bu dosyaları yeniden okuyup yeniden ayrıştırır.
; php.ini (fpm)
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=30000
opcache.validate_timestamps=1
opcache.revalidate_freq=60
opcache.save_comments=1
realpath_cache_size=4096k
realpath_cache_ttl=600max_accelerated_files değerini gerçeğe göre belirleyin: find . -name "*.php" | wc -l ile dosyaları sayın ve sınırı sonucun rahatça üstüne koyun (PHP değeri bir asal sayıya yuvarlar). Dolan bir OPcache kimseyi uyarmaz; sessizce yeni dosyaları önbelleğe almayı bırakır ve en son kurduğunuz eklenti sonsuza kadar önbelleksiz çalışır. Realpath önbelleği daha sessiz bir kazanç: varsayılan 4 KB, OpenCart’ın istek başına çözdüğü include yolu sayısı için fazlasıyla küçüktür.
PHP 8 JIT’i kapalı bırakıyoruz: OpenCart’ta istek süresine veritabanı gidiş gelişleri ve dosya G/Ç hâkimdir, sıkı sayısal döngüler değil.
Redis: OpenCart tarafındaki iki anahtar
Varsayılan dosya önbelleği system/storage/cache altına çok sayıda minik dosya yazar. Yoğun bir mağazada bu sürekli disk trafiğine dönüşür ve kampanya sırasında önbelleği temizlemek dakikalar alabilir. İki anahtar da tek dosyada.
// system/config/default.php
$_['cache_engine'] = 'redis';
$_['cache_expire'] = 3600;
// oturumlar: 'file' (varsayılan) veya 'db'
$_['session_engine'] = 'db';Önbellek anahtarını çevirmeden önce bir uyarı: OpenCart ile gelen Redis sürücüsü, system/library/cache/redis.php, bağlantıyı kendi içine yazılmış değerlerle açar. Redis’iniz başka bir sunucuda dinliyorsa ya da parola istiyorsa önce o dosyayı okuyun — bağlantı bilgileri default.php’de değildir.
Burada en çok zaman kaybettiren konu oturumlar. OpenCart 3 ve 4, PHP’nin yerleşik oturum yöneticisini kullanmaz; kendi sürücüleri vardır. php.ini’de session.save_handler’ı Redis’e çevirmek hiçbir şeyi taşımaz — sepetler session_engine ne diyorsa oraya yazılmaya devam eder. Mağaza birden fazla uygulama sunucusunun arkasındaysa db kullanın ve süresi dolmuş satırları silen bir şey olduğundan emin olun; varsayılan kurulumda bunu yapan yok.
# /etc/redis/redis.conf — yalnızca önbellek örneği
maxmemory 512mb
maxmemory-policy allkeys-lru
save ""
appendonly noÖnemli satır allkeys-lru: bir önbellek dolduğunda en soğuk kayıtları atmalı, yazma reddetmeye başlamamalıdır. Bu tam olarak güvenli, çünkü bu örnek yalnızca önbellek tutuyor — oturumlar veritabanında. RDB anlık görüntülerini kapatmak periyodik fork duraklamalarını kaldırır; yeniden başlatmada önbelleği kaybetmenin bedeli bir ısınma süresinden ibarettir.
MySQL: gerçekten fark yaratan ayar
Tek bir şey değiştirecekseniz innodb_buffer_pool_size olsun. Bu, MySQL’in veri ve index sayfalarını tuttuğu bellektir; hedef çalışma setini sığdırmaktır. İlk bölümdeki sorgu toplamı verir — bu 1,8 GB ise 2,5 GB’lık bir havuz hepsini tutar ve mağaza bir ürünü okumak için diske gitmeyi bırakır.
[mysqld]
innodb_buffer_pool_size = 2560M
innodb_buffer_pool_instances = 2
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 1
innodb_file_per_table = 1
max_connections = 80
table_open_cache = 4000
tmp_table_size = 64M
max_heap_table_size = 64M
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1max_connections, pm.max_children’ın biraz üstünde olmalı; on katı değil. Her bağlantı iş yapsa da yapmasa da bellek ayırır ve kontrolden çıkmış bir döngünün altı yüz bağlantı açmasına izin verecek kadar yüksek bir sınır, yalnızca sunucunun daha eksiksiz ölmesi demektir. Ödeme alan bir mağazada innodb_flush_log_at_trx_commit’i 1’de tutun.
Yıllardır ayakta olan mağazalarda son bir kontrol. OpenCart 1.5 veya erken 2.x’ten yükseltilen siteler sıklıkla hâlâ MyISAM tablo taşır; bu tablolar InnoDB havuzunu tamamen görmezden gelir ve her yazmada tam tablo kilidi alır:
SELECT table_name, engine
FROM information_schema.tables
WHERE table_schema = DATABASE() AND engine <> 'InnoDB';
-- sonra bakım penceresinde, tablo tablo:
ALTER TABLE oc_customer_online ENGINE=InnoDB;Bitti demeden önce
Bir sunucu profili, yapılandırma dosyaları yazıldığında değil, mağazanın bu profil altında hâlâ doğru davrandığını kanıtladığınızda biter:
- 1Admin panelinden önbelleği temizleyin, ardından redis-cli dbsize ile Redis’te anahtarların oluştuğunu doğrulayın.
- 2Canlı TLS adresi üzerinde, ödeme sağlayıcısından dönüş dahil, uçtan uca gerçek bir test siparişi verin.
- 3İzinli bir adresten admin’e girin; engelli bir adresin config.php’yi metin olarak indirmek yerine reddedildiğini görün.
- 4Bir kategori sayfasına kısa bir yük bindirin ve /fpm-status üzerindeki listen queue değerinin sıfırda kaldığını izleyin.
- 5Yavaş logların döndüğünü ve system/storage altındaki error.log’u okuyan biri olduğunu doğrulayın.
- 6Bir kez yeniden başlatın. Nginx, PHP-FPM, Redis ve MySQL kendiliğinden geri gelmeli.
Bu son adım, diğer beşinin toplamından daha çok sorun yakalar. Plansız bir yeniden başlatmadan sağ çıkamayan bir sunucu ayarlı değildir; dengededir.
Ne zaman bize yazın
Bunu cuma akşamı canlı mağazada denemek istemiyorsanız, sınırları belli bir iş olarak yapıyoruz: ölçüyoruz, profili öneriyoruz, önce staging kopyasında uyguluyoruz, sonra geçişi yapıyoruz. Saatlik 10 $ + KDV lansman ücretiyle faturalanır, ilk yanıtı iki saat içinde veriyoruz, Pazartesi–Cumartesi 09:00–22:00 (GMT+3) klavyenin başındayız. Buradaki her şey aynı zamanda yönetilen OpenCart hosting tarafındaki standart kurulumumuz; zaten sunucu değiştirecekseniz ayar da taşımayla birlikte geliyor.
Docker, ArgoCD ve dahili geliştirici platformları; edge computing.