PHP 8.1'de array_map yerine generator: 40 bin satırlık CSV içe aktarımı
这篇内容暂时只有 Türkçe 版本,以下是原文。
Üretimde bir muhasebe entegrasyonu var: kullanıcı 30–45 bin satırlık CSV yüklüyor, PHP 8.1, 512M memory_limit, klasik nginx + php-fpm. Dosyayı file_get_contents ile alıp array_map ile satırlara bölüp her satırı diziye çeviren bir kod vardı. 20 bin satıra kadar sorun yok; 35 bin civarında istek 500 dönmeye başladı, log’da Allowed memory size exhausted.
Önce ölçtüm, tahmin etmedim:
$before = memory_get_usage(true);
$lines = explode("\n", $csv);
$rows = array_map('str_getcsv', $lines);
error_log(sprintf('peak=%dMB satir=%d', memory_get_peak_usage(true)/1048576, count($rows)));
Sonuç: 35 bin satırda peak ~430MB. Satır başına ~12KB; str_getcsv her satır için ayrı dizi, explode de tüm dosyayı ikinci kez bellekte tutuyor.
Yaptığım değişiklik: dosyayı tek seferde okumayı bırakıp akışa çevirdim. Önce fopen + fgetcsv, sonra işleme kısmını generator’a aldım:
function readCsv(string $path): Generator {
$fh = fopen($path, 'rb');
if ($fh === false) throw new RuntimeException('acilamadi');
try {
$header = fgetcsv($fh, 0, ';');
while (($row = fgetcsv($fh, 0, ';')) !== false) {
yield array_combine($header, $row);
}
} finally {
fclose($fh);
}
}
foreach (readCsv($tmpPath) as $i => $row) {
$batch[] = normalize($row);
if (count($batch) === 500) {
insertBatch($batch); // tek INSERT, 500 satır
$batch = [];
}
}
if ($batch) insertBatch($batch);
Aynı dosyada peak 18MB’a düştü, süre 2.1s’den 2.6s’ye çıktı. Yani bellek kazancının bedeli biraz CPU ve disk I/O; ama 500’lük batch sayesinde toplam sorgu sayısı da düştü, o yüzden net süre neredeyse aynı kaldı.
Mekanizma tarafı: fgetcsv satır satır okuyor, generator her yield sonrası önceki satırı serbest bırakıyor; array_map ise tüm sonucu tek seferde üretip bellekte tutuyor. memory_get_peak_usage gerçek sayıyı veriyor, memory_get_usage döngü içinde yanıltıcı olabilir. Batch boyutunu 500’den 5000’e çıkardığımda bellek 60MB’a fırladı, MySQL tarafında da max_allowed_packet sınırına yaklaştım; 500 bizim için tatlı nokta.
Nerede işe yaramaz: array_combine başlıkta tekrar eden kolon adı varsa false döner ve sessizce bozulur; başlığı önce array_unique ile kontrol etmek gerekiyor. Ayrıca dosya UTF-8 BOM ile gelirse ilk kolon adı \xEF\xBB\xBFid oluyor, fgetcsv bunu temizlemiyor. Bir de dosya php://input üzerinden geliyorsa fopen stream’i yeniden açamıyor; o durumda php://temp’e kopyalamak zorunda kaldım. Son olarak CLI’da memory_limit=-1 olduğu için sorun görünmüyor, sadece web’de patlıyor; testi mutlaka fpm üzerinden yapın.
Sizin içe aktarımlarda batch boyutunu neye göre seçiyorsunuz: bellek mi, tek sorgu süresi mi, yoksa max_allowed_packet mı belirliyor?


0 条评论
还没有评论,写下第一条。