
Baru-baru ini, perhatian penulis tertarik pada artikel di LWN tentang antarmuka kernel baru untuk polling. Ini membahas mekanisme polling baru di Linux AIO API (sebuah antarmuka untuk penanganan file asinkron), yang ditambahkan ke versi kernel 4.18. Idenya cukup menarik: penulis tambalan menyarankan menggunakan Linux AIO API untuk bekerja dengan jaringan.
Tapi tunggu sebentar! Bagaimanapun, Linux AIO diciptakan untuk bekerja dengan I / O asinkron dari disk ke disk! File pada disk tidak sama dengan koneksi jaringan. Apakah mungkin menggunakan Linux AIO API untuk jaringan?
Ternyata, ya, itu mungkin! Artikel ini menjelaskan cara menggunakan kekuatan Linux AIO API untuk membuat server jaringan yang lebih cepat dan lebih baik.
Tetapi mari kita mulai dengan menjelaskan apa itu Linux AIO.
Pengantar Linux AIO
Linux AIO menyediakan I / O disk-ke-disk asinkron untuk perangkat lunak pengguna.
Secara historis, di Linux, semua operasi disk diblokir. Jika Anda memanggil
open()
,
read()
,
write()
atau
fsync()
, maka streaming berhenti sampai metadata muncul di cache disk. Ini biasanya bukan masalah. Jika Anda tidak memiliki banyak operasi I / O dan memori yang cukup, panggilan sistem secara bertahap akan mengisi cache, dan semuanya akan bekerja cukup cepat.
Kinerja operasi I / O berkurang ketika jumlahnya cukup besar, misalnya, dalam kasus dengan basis data dan proxy. Untuk aplikasi seperti itu, tidak dapat diterima untuk menghentikan seluruh proses demi menunggu satu panggilan sistem
read()
.
Untuk mengatasi masalah ini, aplikasi dapat menggunakan tiga metode:
- Gunakan kumpulan utas dan fungsi pemblokiran panggilan pada utas terpisah. Inilah cara kerja POSIX AIO di glibc (jangan bingung dengan Linux AIO). Untuk informasi lebih lanjut, lihat dokumentasi IBM . Inilah cara kami memecahkan masalah di Cloudflare: kami menggunakan kumpulan utas untuk memanggil
read()
dan open()
.
- Lakukan pemanasan cache disk dengan
posix_fadvise(2)
dan berharap yang terbaik.
- Gunakan Linux AIO bersamaan dengan sistem file XFS, buka file dengan flag O_DIRECT dan hindari masalah yang tidak terdokumentasi .
Namun, tidak satupun dari metode ini yang ideal. Bahkan Linux AIO, ketika digunakan tanpa berpikir, dapat diblokir dalam panggilan
io_submit()
. Ini baru-baru ini disebutkan dalam
artikel lain
di LWN :
“Antarmuka I / O asinkron Linux memiliki banyak kritik dan sedikit pendukung, tetapi kebanyakan orang berharap setidaknya tidak ada sinkronisasi dari itu. Bahkan, operasi AIO dapat diblokir di kernel karena sejumlah alasan dalam situasi di mana utas panggilan tidak mampu. ”
Sekarang kita tahu kelemahan dari API AIO Linux, mari kita lihat kelebihannya.
Program sederhana menggunakan Linux AIO
Untuk menggunakan Linux AIO, pertama-tama Anda harus
menentukan sendiri lima panggilan sistem yang diperlukan - glibc tidak menyediakannya.
- Pertama, Anda perlu memanggil
io_setup()
untuk menginisialisasi struktur aio_context
. Kernel akan mengembalikan pointer buram ke struktur.
- Setelah itu, Anda dapat memanggil
io_submit()
untuk menambahkan vektor "blok kontrol I / O" ke antrian pemrosesan dalam bentuk struktur struct iocb.
- Sekarang, akhirnya, kita dapat memanggil
io_getevents()
dan menunggu jawaban darinya dalam bentuk vektor struktur struct io_event
- hasil dari masing-masing blok iocb.
Ada delapan perintah yang bisa Anda gunakan di iocb. Dua perintah untuk membaca, dua untuk menulis, dua opsi fsync, dan perintah POLL, yang ditambahkan dalam versi kernel 4.18 (perintah kedelapan adalah NOOP):
IOCB_CMD_PREAD = 0, IOCB_CMD_PWRITE = 1, IOCB_CMD_FSYNC = 2, IOCB_CMD_FDSYNC = 3, IOCB_CMD_POLL = 5, /* from 4.18 */ IOCB_CMD_NOOP = 6, IOCB_CMD_PREADV = 7, IOCB_CMD_PWRITEV = 8,
iocb
, yang diteruskan ke fungsi
io_submit
, cukup besar dan dirancang untuk bekerja dengan disk. Berikut ini versinya yang disederhanakan:
struct iocb { __u64 data; /* user data */ ... __u16 aio_lio_opcode; /* see IOCB_CMD_ above */ ... __u32 aio_fildes; /* file descriptor */ __u64 aio_buf; /* pointer to buffer */ __u64 aio_nbytes; /* buffer size */ ... }
Struktur lengkap
io_event
yang dikembalikan
io_getevents
:
struct io_event { __u64 data; /* user data */ __u64 obj; /* pointer to request iocb */ __s64 res; /* result code for this event */ __s64 res2; /* secondary result */ };
Sebuah contoh Program sederhana yang membaca file / etc / passwd menggunakan Linux AIO API:
fd = open("/etc/passwd", O_RDONLY); aio_context_t ctx = 0; r = io_setup(128, &ctx); char buf[4096]; struct iocb cb = {.aio_fildes = fd, .aio_lio_opcode = IOCB_CMD_PREAD, .aio_buf = (uint64_t)buf, .aio_nbytes = sizeof(buf)}; struct iocb *list_of_iocb[1] = {&cb}; r = io_submit(ctx, 1, list_of_iocb); struct io_event events[1] = {{0}}; r = io_getevents(ctx, 1, 1, events, NULL); bytes_read = events[0].res; printf("read %lld bytes from /etc/passwd\n", bytes_read);
Sumber lengkap, tentu saja,
tersedia di GitHub . Berikut adalah keluaran strace dari program ini:
openat(AT_FDCWD, "/etc/passwd", O_RDONLY) io_setup(128, [0x7f4fd60ea000]) io_submit(0x7f4fd60ea000, 1, [{aio_lio_opcode=IOCB_CMD_PREAD, aio_fildes=3, aio_buf=0x7ffc5ff703d0, aio_nbytes=4096, aio_offset=0}]) io_getevents(0x7f4fd60ea000, 1, 1, [{data=0, obj=0x7ffc5ff70390, res=2494, res2=0}], NULL)
Semuanya berjalan dengan baik, tetapi membaca dari disk tidak sinkron: panggilan io_submit diblokir dan melakukan semua pekerjaan, fungsi
io_getevents
dijalankan secara instan. Kita dapat mencoba membaca secara tidak sinkron, tetapi ini membutuhkan flag O_DIRECT, yang dengannya operasi disk mem-bypass cache.
Mari kita ilustrasikan bagaimana
io_submit
mengunci file biasa. Berikut adalah contoh serupa yang menunjukkan output dari strace sebagai hasil dari membaca blok 1 GB dari
/dev/zero
:
io_submit(0x7fe1e800a000, 1, [{aio_lio_opcode=IOCB_CMD_PREAD, aio_fildes=3, aio_buf=0x7fe1a79f4000, aio_nbytes=1073741824, aio_offset=0}]) \ = 1 <0.738380> io_getevents(0x7fe1e800a000, 1, 1, [{data=0, obj=0x7fffb9588910, res=1073741824, res2=0}], NULL) \ = 1 <0.000015>
Kernel menghabiskan 738 ms pada panggilan
io_submit
dan hanya 15 ns pada
io_getevents
. Berperilaku sama dengan koneksi jaringan - semua pekerjaan dilakukan oleh
io_submit
.
Foto Helix84 CC / BY-SA / 3.0
Linux AIO dan Jaringan
Implementasi
io_submit
cukup konservatif: jika deskriptor file yang dikirimkan tidak dibuka dengan flag O_DIRECT, maka fungsinya cukup memblokir dan melakukan tindakan yang ditentukan. Dalam hal koneksi jaringan, ini berarti bahwa:
- untuk memblokir koneksi, IOCV_CMD_PREAD akan menunggu paket respons;
- untuk koneksi yang tidak memblokir, IOCB_CMD_PREAD akan mengembalikan kode -11 (EAGAIN).
Semantik yang sama juga digunakan dalam panggilan sistem
read()
, jadi kita dapat mengatakan bahwa io_submit ketika bekerja dengan koneksi jaringan tidak lebih pintar daripada panggilan
read() / write()
.
Penting untuk dicatat bahwa permintaan
iocb
dieksekusi oleh kernel secara berurutan.
Terlepas dari kenyataan bahwa Linux AIO tidak akan membantu kami dengan operasi asinkron, Linux AIO dapat digunakan untuk menggabungkan panggilan sistem ke dalam batch.
Jika server web perlu mengirim dan menerima data dari ratusan koneksi jaringan, maka menggunakan
io_submit
mungkin ide yang bagus, karena menghindari ratusan panggilan kirim dan terima. Ini akan meningkatkan kinerja - beralih dari ruang pengguna ke kernel dan sebaliknya tidak gratis, terutama setelah pengenalan
langkah -
langkah untuk memerangi Specter dan Meltdown .
| Satu buffer
| Banyak buffer
|
Satu file deskriptor
| baca ()
| readv ()
|
Penjelasan File Berganda
| io_mengirimkan + IOCB_CMD_PREAD
| io_mengirimkan + IOCB_CMD_PREADV
|
Untuk mengilustrasikan pengelompokan panggilan sistem ke dalam paket menggunakan
io_submit
mari kita menulis sebuah program kecil yang mengirim data dari satu koneksi TCP ke yang lain. Dalam bentuknya yang paling sederhana (tanpa Linux AIO), tampilannya seperti ini:
while True: d = sd1.read(4096) sd2.write(d)
Kami dapat mengekspresikan fungsionalitas yang sama melalui Linux AIO. Kode dalam hal ini akan seperti ini:
struct iocb cb[2] = {{.aio_fildes = sd2, .aio_lio_opcode = IOCB_CMD_PWRITE, .aio_buf = (uint64_t)&buf[0], .aio_nbytes = 0}, {.aio_fildes = sd1, .aio_lio_opcode = IOCB_CMD_PREAD, .aio_buf = (uint64_t)&buf[0], .aio_nbytes = BUF_SZ}}; struct iocb *list_of_iocb[2] = {&cb[0], &cb[1]}; while(1) { r = io_submit(ctx, 2, list_of_iocb); struct io_event events[2] = {}; r = io_getevents(ctx, 2, 2, events, NULL); cb[0].aio_nbytes = events[1].res; }
Kode ini menambahkan dua pekerjaan ke
io_submit
: pertama permintaan tulis ke
sd2
, dan kemudian permintaan baca dari sd1. Setelah membaca, kode memperbaiki ukuran buffer tulis dan mengulangi loop dari awal. Ada satu trik: pertama kali menulis terjadi dengan buffer ukuran 0. Ini diperlukan karena kami memiliki kemampuan untuk menggabungkan tulis + baca dalam satu panggilan
io_submit
(tetapi tidak membaca + tulis).
Apakah kode ini lebih cepat daripada
read()
biasa
read()
/
write()
? Belum. Kedua versi menggunakan dua panggilan sistem: read + write dan io_submit + io_getevents. Tapi, untungnya, kodenya bisa diperbaiki.
Menyingkirkan io_getevents
Saat runtime
io_setup()
kernel mengalokasikan beberapa halaman memori untuk proses tersebut. Beginilah tampilan blok memori ini di / proc // maps:
marek:~$ cat /proc/`pidof -s aio_passwd`/maps ... 7f7db8f60000-7f7db8f63000 rw-s 00000000 00:12 2314562 /[aio] (deleted) ...
Blok memori [aio] (12 Kb dalam kasus ini) dialokasikan
io_setup
. Ini digunakan untuk buffer melingkar tempat acara disimpan. Dalam kebanyakan kasus, tidak ada alasan untuk memanggil
io_getevents
- data penyelesaian acara dapat diperoleh dari buffer cincin tanpa perlu beralih ke mode kernel. Ini adalah versi kode yang diperbaiki:
int io_getevents(aio_context_t ctx, long min_nr, long max_nr, struct io_event *events, struct timespec *timeout) { int i = 0; struct aio_ring *ring = (struct aio_ring*)ctx; if (ring == NULL || ring->magic != AIO_RING_MAGIC) { goto do_syscall; } while (i < max_nr) { unsigned head = ring->head; if (head == ring->tail) { /* There are no more completions */ break; } else { /* There is another completion to reap */ events[i] = ring->events[head]; read_barrier(); ring->head = (head + 1) % ring->nr; i++; } } if (i == 0 && timeout != NULL && timeout->tv_sec == 0 && timeout->tv_nsec == 0) { /* Requested non blocking operation. */ return 0; } if (i && i >= min_nr) { return i; } do_syscall: return syscall(__NR_io_getevents, ctx, min_nr-i, max_nr-i, &events[i], timeout); }
Versi lengkap kode tersedia
di GitHub . Antarmuka buffer cincin ini tidak terdokumentasi dengan baik, penulis mengadaptasi kode dari proyek
axboe / fio .
Setelah perubahan ini, versi kode kami menggunakan Linux AIO hanya membutuhkan satu panggilan sistem dalam satu lingkaran, yang membuatnya sedikit lebih cepat daripada kode asli menggunakan baca + tulis.
Kereta Foto Foto CC / BY-SA / 2.0
Alternatif epoll
Dengan tambahan IOCB_CMD_POLL ke kernel versi 4.18, menjadi mungkin untuk menggunakan
io_submit
sebagai pengganti select / poll / epoll. Misalnya, kode ini akan mengharapkan data dari koneksi jaringan:
struct iocb cb = {.aio_fildes = sd, .aio_lio_opcode = IOCB_CMD_POLL, .aio_buf = POLLIN}; struct iocb *list_of_iocb[1] = {&cb}; r = io_submit(ctx, 1, list_of_iocb); r = io_getevents(ctx, 1, 1, events, NULL);
Kode lengkap . Ini adalah output strace-nya:
io_submit(0x7fe44bddd000, 1, [{aio_lio_opcode=IOCB_CMD_POLL, aio_fildes=3}]) \ = 1 <0.000015> io_getevents(0x7fe44bddd000, 1, 1, [{data=0, obj=0x7ffef65c11a8, res=1, res2=0}], NULL) \ = 1 <1.000377>
Seperti yang Anda lihat, kali ini sinkronisasi tidak berfungsi: io_submit dieksekusi secara instan, dan
io_getevents
diblokir selama satu detik, menunggu data. Ini dapat digunakan sebagai pengganti panggilan sistem
epoll_wait()
.
Selain itu, bekerja dengan
epoll
biasanya membutuhkan penggunaan panggilan sistem epoll_ctl. Dan pengembang aplikasi mencoba menghindari panggilan yang sering ke fungsi ini - untuk memahami alasannya, cukup
baca flag EPOLLONESHOT dan EPOLLET
di manual . Menggunakan io_submit untuk menanyakan koneksi, Anda dapat menghindari kesulitan ini dan panggilan sistem tambahan. Cukup tambahkan koneksi ke vektor iocb, panggil io_submit sekali dan tunggu eksekusi. Semuanya sangat sederhana.
Ringkasan
Dalam posting ini, kami membahas Linux AIO API. API ini awalnya dirancang untuk bekerja dengan disk, tetapi juga berfungsi dengan koneksi jaringan. Namun, tidak seperti panggilan read () + write () biasa, menggunakan io_submit memungkinkan Anda untuk mengelompokkan panggilan sistem dan dengan demikian meningkatkan kinerja.
Dimulai dengan kernel versi 4.18,
io_submit io_getevents
dalam hal koneksi jaringan dapat digunakan untuk acara dari formulir POLLIN dan POLLOUT. Ini adalah alternatif untuk
epoll()
.
Saya bisa membayangkan layanan jaringan yang hanya menggunakan
io_submit io_getevents
daripada set standar baca, tulis, epoll_ctl dan epoll_wait. Dalam hal ini, pengelompokan panggilan sistem di
io_submit
dapat memberikan keuntungan besar, server seperti itu akan jauh lebih cepat.
Sayangnya, bahkan setelah perbaikan terbaru untuk Linux AIO API, diskusi tentang kegunaannya berlanjut. Sudah diketahui bahwa
Linus membencinya :
"AIO adalah contoh mengerikan dari desain setinggi lutut, di mana alasan utamanya adalah:" orang lain yang kurang berbakat muncul dengan ini, jadi kami harus mematuhi kompatibilitas sehingga pengembang database (yang jarang berselera) dapat menggunakannya. " Tapi AIO selalu sangat, sangat bengkok. "
Beberapa upaya telah dilakukan untuk membuat antarmuka yang lebih baik untuk pengelompokan panggilan dan sinkronisasi, tetapi mereka tidak memiliki visi yang sama. Misalnya,
penambahan sendto (MSG_ZEROCOPY) baru-baru ini memungkinkan untuk transfer data yang benar-benar tidak sinkron, tetapi tidak menyediakan untuk pengelompokan.
io_submit
menyediakan pengelompokan, tetapi tidak sinkron. Lebih buruk lagi - saat ini ada tiga cara untuk mengirimkan peristiwa asinkron di Linux: sinyal,
io_getevents
dan MSG_ERRQUEUE.
Bagaimanapun, sangat bagus bahwa ada cara baru untuk mempercepat pekerjaan layanan jaringan.