Mengapa gambar Alpine Docker lebih dari 50% lebih lambat daripada gambar Ubuntu?

35

Saya perhatikan bahwa aplikasi Python saya jauh lebih lambat ketika menjalankannya python:2-alpine3.6daripada menjalankannya tanpa Docker di Ubuntu. Saya datang dengan dua perintah benchmark kecil dan ada perbedaan besar yang terlihat antara kedua sistem operasi, baik ketika saya menjalankannya di server Ubuntu, dan ketika saya menggunakan Docker untuk Mac.

$ BENCHMARK="import timeit; print(timeit.timeit('import json; json.dumps(list(range(10000)))', number=5000))"
$ docker run python:2-alpine3.6 python -c $BENCHMARK
7.6094589233
$ docker run python:2-slim python -c $BENCHMARK
4.3410820961
$ docker run python:3-alpine3.6 python -c $BENCHMARK
7.0276606959
$ docker run python:3-slim python -c $BENCHMARK
5.6621271420

Saya juga mencoba 'patokan' berikut, yang tidak menggunakan Python:

$ docker run -ti ubuntu bash
root@6b633e9197cc:/# time $(i=0; while (( i < 9999999 )); do (( i ++
)); done)

real    0m39.053s
user    0m39.050s
sys     0m0.000s
$ docker run -ti alpine sh
/ # apk add --no-cache bash > /dev/null
/ # bash
bash-4.3# time $(i=0; while (( i < 9999999 )); do (( i ++ )); done)

real    1m4.277s
user    1m4.290s
sys     0m0.000s

Apa yang bisa menyebabkan perbedaan ini?

Underyx
sumber
1
@Seth look again: timing dimulai setelah bash diinstal, di dalam bash shell yang diluncurkan
Underyx

Jawaban:

45

Saya telah menjalankan tolok ukur yang sama seperti yang Anda lakukan, hanya menggunakan Python 3:

$ docker run python:3-alpine3.6 python --version
Python 3.6.2
$ docker run python:3-slim python --version
Python 3.6.2

menghasilkan perbedaan lebih dari 2 detik:

$ docker run python:3-slim python -c "$BENCHMARK"
3.6475560404360294
$ docker run python:3-alpine3.6 python -c "$BENCHMARK"
5.834922112524509

Alpine menggunakan implementasi berbeda libc(pustaka sistem dasar) dari proyek musl ( mirror URL ). Ada banyak perbedaan di antara perpustakaan-perpustakaan itu . Akibatnya, setiap perpustakaan mungkin berkinerja lebih baik dalam kasus penggunaan tertentu.

Berikut ini perbedaan strace antara perintah-perintah di atas . Outputnya mulai berbeda dari baris 269. Tentu saja ada alamat yang berbeda dalam memori, tetapi sebaliknya sangat mirip. Sebagian besar waktu jelas dihabiskan menunggu pythonperintah selesai.

Setelah memasang straceke kedua kontainer, kita bisa mendapatkan jejak yang lebih menarik (saya telah mengurangi jumlah iterasi dalam benchmark menjadi 10).

Misalnya, glibcmemuat pustaka dengan cara berikut (baris 182):

openat(AT_FDCWD, "/usr/local/lib/python3.6", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
getdents(3, /* 205 entries */, 32768)   = 6824
getdents(3, /* 0 entries */, 32768)     = 0

Kode yang sama di musl:

open("/usr/local/lib/python3.6", O_RDONLY|O_DIRECTORY|O_CLOEXEC) = 3
fcntl(3, F_SETFD, FD_CLOEXEC)           = 0
getdents64(3, /* 62 entries */, 2048)   = 2040
getdents64(3, /* 61 entries */, 2048)   = 2024
getdents64(3, /* 60 entries */, 2048)   = 2032
getdents64(3, /* 22 entries */, 2048)   = 728
getdents64(3, /* 0 entries */, 2048)    = 0

Saya tidak mengatakan ini adalah perbedaan utama, tetapi mengurangi jumlah operasi I / O di perpustakaan inti mungkin berkontribusi pada kinerja yang lebih baik. Dari diff Anda dapat melihat bahwa mengeksekusi kode Python yang sama dapat menyebabkan panggilan sistem yang sedikit berbeda. Mungkin yang paling penting dapat dilakukan dalam mengoptimalkan kinerja loop. Saya tidak cukup memenuhi syarat untuk menilai apakah masalah kinerja disebabkan oleh alokasi memori atau instruksi lainnya.

  • glibc dengan 10 iterasi:

    write(1, "0.032388824969530106\n", 210.032388824969530106)
    
  • musl dengan 10 iterasi:

    write(1, "0.035214247182011604\n", 210.035214247182011604)
    

musllebih lambat oleh 0,0028254222124814987 detik. Sebagai perbedaan tumbuh dengan jumlah iterasi, saya berasumsi perbedaannya adalah dalam alokasi memori objek JSON.

Jika kami mengurangi tolok ukur hanya untuk mengimpor, jsonkami melihat perbedaannya tidak terlalu besar:

$ BENCHMARK="import timeit; print(timeit.timeit('import json;', number=5000))"
$ docker run python:3-slim python -c "$BENCHMARK"
0.03683806210756302
$ docker run python:3-alpine3.6 python -c "$BENCHMARK"
0.038280246779322624

Memuat pustaka Python terlihat sebanding. Menghasilkan list()menghasilkan perbedaan yang lebih besar:

$ BENCHMARK="import timeit; print(timeit.timeit('list(range(10000))', number=5000))"
$ docker run python:3-slim python -c "$BENCHMARK"
0.5666235145181417
$ docker run python:3-alpine3.6 python -c "$BENCHMARK"
0.6885563563555479

Jelas operasi yang paling mahal adalah json.dumps(), yang mungkin menunjukkan perbedaan alokasi memori antara perpustakaan-perpustakaan itu.

Melihat lagi patokannya , muslalokasi alokasi memori benar-benar sedikit lebih lambat:

                          musl  | glibc
-----------------------+--------+--------+
Tiny allocation & free |  0.005 | 0.002  |
-----------------------+--------+--------+
Big allocation & free  |  0.027 | 0.016  |
-----------------------+--------+--------+

Saya tidak yakin apa yang dimaksud dengan "alokasi besar", tetapi muslhampir 2 × lebih lambat, yang mungkin menjadi signifikan ketika Anda mengulangi operasi seperti itu ribuan atau jutaan kali.

Tombart
sumber
12
Hanya sedikit koreksi. musl bukan implementasi Alpine sendiri untuk glibc. Musl pertama bukanlah implementasi (kembali) dari glibc, tetapi implementasi berbeda dari libc per standar POSIX. Musl ke-2 bukan milik Alpine sendiri , ini proyek mandiri, tidak terkait, dan musl tidak digunakan hanya di Alpine.
Jakub Jirutka
mengingat bahwa musl libc sepertinya lebih baik berdasarkan standar *, belum lagi implementasi yang lebih baru mengapa tampaknya berkinerja buruk glibc dalam kasus ini? * lih. wiki.musl-libc.org/functional-differences-from-glibc.html
Forest
Apakah perbedaan 0,0028 detik signifikan secara statistik? Penyimpangan relatif hanya 0,0013% dan Anda mengambil 10 sampel. Berapa standar deviasi (estimasi) untuk 10 run (atau bahkan perbedaan max-min)?
Peter Mortensen
@PeterMortensen Untuk pertanyaan tentang hasil benchmark, Anda harus merujuk pada kode Eta Labs: etalabs.net/libc-bench.html Misalnya, uji tekanan malloc diulangi sebanyak 100k kali. Hasilnya bisa sangat tergantung pada versi perpustakaan, versi GCC dan CPU yang digunakan, hanya untuk menyebutkan beberapa aspek.
Tombart