io_submit: alternatif untuk epoll yang belum pernah Anda dengar



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:

  1. 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() .
  2. Lakukan pemanasan cache disk dengan posix_fadvise(2) dan berharap yang terbaik.
  3. 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.

  1. Pertama, Anda perlu memanggil io_setup() untuk menginisialisasi struktur aio_context . Kernel akan mengembalikan pointer buram ke struktur.
  2. Setelah itu, Anda dapat memanggil io_submit() untuk menambahkan vektor "blok kontrol I / O" ke antrian pemrosesan dalam bentuk struktur struct iocb.
  3. 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.

Source: https://habr.com/ru/post/id439972/


All Articles