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
赞同度 100% · 0 条评论 · 4 阅读

0 条评论

支持 Markdown · 回复会实时推送给对方

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