Kami menjadikan Shrimp lebih bermanfaat: menambahkan transcoding gambar ke format lain



Sejak awal 2017, tim kecil kami telah mengembangkan pustaka RESTinio OpenSource untuk menyematkan server HTTP dalam aplikasi C ++. Kami sangat terkejut, dari waktu ke waktu kami mendapat pertanyaan dari kategori "Dan mengapa server HTTP C ++ yang disematkan diperlukan?" Sayangnya, pertanyaan sederhana adalah yang paling sulit dijawab. Terkadang jawaban terbaik adalah kode sampel.

Beberapa bulan yang lalu kami memulai proyek demo kecil , Shrimp , yang dengan jelas menunjukkan skenario tipikal, di mana perpustakaan kami "diasah". Proyek demo adalah layanan Web sederhana yang menerima permintaan untuk menskala gambar yang disimpan di server dan yang mengembalikan gambar ukuran yang dibutuhkan pengguna.

Proyek demo ini bagus dalam hal itu, pertama, ia membutuhkan integrasi dengan kode yang sudah lama ditulis dan bekerja dengan benar dalam C atau C ++ (dalam hal ini, ImageMagick). Oleh karena itu, harus jelas mengapa masuk akal untuk menanamkan server HTTP dalam aplikasi C ++.

Dan kedua, dalam hal ini, pemrosesan permintaan yang tidak sinkron diperlukan sehingga server HTTP tidak memblokir ketika gambar sedang diskalakan (dan ini bisa memakan waktu ratusan milidetik atau bahkan detik). Dan kami memulai pengembangan RESTinio justru karena kami tidak dapat menemukan server tertanam C ++ yang dipusatkan secara khusus pada pemrosesan permintaan asinkron.

Kami membangun karya di atas Udang secara berulang: pertama, versi paling sederhana dibuat dan dijelaskan , yang hanya memperbesar gambar. Kemudian kami memperbaiki sejumlah kekurangan dari versi pertama dan menggambarkannya di artikel kedua . Akhirnya, kami berkeliling untuk memperluas fungsi udang sekali lagi: konversi gambar dari satu format ke format lainnya ditambahkan. Tentang bagaimana ini dilakukan dan akan dibahas dalam artikel ini.

Dukungan format target


Jadi, dalam versi udang berikutnya, kami menambahkan kemampuan untuk memberikan gambar berskala dalam format yang berbeda. Jadi, jika Anda mengeluarkan permintaan udang dari formulir:

curl "http://localhost:8080/my_picture.jpg?op=resize&max=1920" 

maka Shrimp akan membuat gambar dalam format JPG yang sama dengan gambar aslinya.

Tetapi jika Anda menambahkan parameter format target ke URL, maka Shrimp mengubah gambar ke format target yang ditentukan. Sebagai contoh:

 curl "http://localhost:8080/my_picture.jpg?op=resize&max=1920&target-format=webp" 

Dalam hal ini, Udang akan membuat gambar dalam format webp.

Udang yang diperbarui mendukung lima format gambar: jpg, png, gif, webp, dan heic (juga dikenal sebagai HEIF). Anda dapat bereksperimen dengan berbagai format pada halaman web khusus :



(pada halaman ini tidak ada cara untuk memilih format heic, karena browser desktop biasa tidak mendukung format ini secara default).

Untuk mendukung format target dalam udang, diperlukan untuk sedikit mengubah kode udang (yang kami sendiri terkejut, karena memang ada sedikit perubahan). Tetapi di sisi lain, saya harus bermain dengan perakitan ImageMagick, yang kami bahkan lebih terkejut dengannya Sebelumnya, kami harus berurusan dengan dapur ini, secara kebetulan. Tapi mari kita bicara tentang semuanya secara berurutan.

ImageMagick harus memahami format yang berbeda


ImageMagick menggunakan pustaka eksternal untuk menyandikan / mendekode gambar: libjpeg, libpng, libgif, dll. Pustaka ini harus diinstal pada sistem sebelum ImageMagick dikonfigurasi dan dibuat.

Hal yang sama harus terjadi agar ImageMagick mendukung format webp dan heic: pertama Anda perlu membangun dan menginstal libwebp dan libheif, kemudian mengkonfigurasi dan menginstal ImageMagick. Dan jika semuanya sederhana dengan libwebp, maka sekitar libhe jika saya harus menari dengan rebana. Meskipun setelah beberapa waktu, setelah semuanya akhirnya berkumpul dan bekerja, itu belum jelas: mengapa Anda harus menggunakan rebana, semuanya tampak sepele? ;)

Secara umum, jika seseorang ingin berteman dengan heic dan ImageMagick, Anda harus menginstal:


Ada dalam urutan ini (Anda mungkin harus menginstal nasm agar x265 bekerja pada kecepatan maksimum). Kemudian, ketika mengeluarkan perintah ./configure , ImageMagick akan dapat menemukan semua yang diperlukan untuk mendukung file .heic.

Dukungan untuk format target dalam string kueri permintaan yang masuk


Setelah kami berteman dengan ImageMagick dengan format webp dan heic, saatnya untuk memodifikasi kode Shrimp. Pertama-tama, kita perlu belajar bagaimana mengenali argumen format-target dalam permintaan HTTP yang masuk.

Dari sudut pandang RESTinio, ini sama sekali bukan masalah. Nah, argumen lain muncul di string kueri, jadi apa? Namun dari sudut pandang Shrimp, situasinya ternyata agak lebih rumit, sehingga kode fungsi yang bertanggung jawab untuk mem-parsing permintaan HTTP menjadi lebih rumit.

Faktanya adalah bahwa sebelum itu perlu untuk membedakan hanya dua situasi:

  • datang permintaan berupa "/filename.ext" tanpa parameter lainnya. Jadi Anda hanya perlu memberikan file "filename.ext" apa adanya;
  • Permintaan datang dalam bentuk "/filename.ext?op=resize & ...". Dalam hal ini, Anda perlu skala gambar dari file "filename.ext".

Tetapi setelah menambahkan format target, kita perlu membedakan antara empat situasi:

  • datang permintaan berupa "/filename.ext" tanpa parameter lainnya. Jadi Anda hanya perlu memberikan file "filename.ext" apa adanya, tanpa scaling dan tanpa transcoding ke format lain;
  • datang permintaan berupa "/filename.ext?target-format=fmt" tanpa parameter lainnya. Ini berarti untuk mengambil gambar dari file "filename.ext" dan transcode ke dalam format "fmt" sambil mempertahankan ukuran aslinya;
  • permintaan datang dari formulir "/filename.ext?op=resize & ..." tetapi tanpa format target. Dalam hal ini, Anda perlu skala gambar dari file "filename.ext" dan berikan dalam format asli;
  • Permintaan datang dari formulir "/filename.ext?op=resize&...&target-format=fmt". Dalam hal ini, Anda perlu melakukan penskalaan, dan kemudian mentranskode hasilnya ke format "fmt".

Akibatnya, fungsi untuk menentukan parameter kueri mengambil bentuk berikut :

 void add_transform_op_handler( const app_params_t & app_params, http_req_router_t & router, so_5::mbox_t req_handler_mbox ) { router.http_get( R"(/:path(.*)\.:ext(.{3,4}))", restinio::path2regex::options_t{}.strict( true ), [req_handler_mbox, &app_params]( auto req, auto params ) { if( has_illegal_path_components( req->header().path() ) ) { //     . return do_400_response( std::move( req ) ); } //   . const auto qp = restinio::parse_query( req->header().query() ); const auto target_format = qp.get_param( "target-format"sv ); //        // .   target-format,    //   .   target-format  // ,    ,  //    . const auto image_format = try_detect_target_image_format( params[ "ext" ], target_format ); if( !image_format ) { //     .   . return do_400_response( std::move( req ) ); } if( !qp.size() ) { //    ,    . return serve_as_regular_file( app_params.m_storage.m_root_dir, std::move( req ), *image_format ); } const auto operation = qp.get_param( "op"sv ); if( operation && "resize"sv != *operation ) { //    ,     resize. return do_400_response( std::move( req ) ); } if( !operation && !target_format ) { //      op=resize, //   target-format=something. return do_400_response( std::move( req ) ); } handle_resize_op_request( req_handler_mbox, *image_format, qp, std::move( req ) ); return restinio::request_accepted(); } ); } 

Di udang versi sebelumnya, di mana Anda tidak perlu mentranskode gambar, bekerja dengan parameter permintaan tampak sedikit lebih mudah .

Permintaan antrian dan cache gambar yang disesuaikan dengan format target


Poin berikutnya dalam implementasi dukungan format-target adalah pekerjaan pada antrian permintaan tunggu dan cache gambar yang sudah jadi di agen a_transform_manager. Kami berbicara tentang hal-hal ini secara lebih rinci di artikel sebelumnya , tetapi mari kita sedikit mengingatkan Anda tentang apa itu.

Ketika permintaan untuk konversi gambar tiba, mungkin ternyata gambar yang sudah jadi dengan parameter tersebut sudah ada dalam cache. Dalam hal ini, Anda tidak perlu melakukan apa pun, cukup kirim gambar dari cache sebagai tanggapan. Jika gambar perlu diubah, maka mungkin ternyata tidak ada pekerja gratis saat ini dan Anda perlu menunggu sampai muncul. Untuk melakukan ini, informasi permintaan harus diantrikan. Tetapi pada saat yang sama, perlu untuk memeriksa keunikan permintaan - jika kami memiliki tiga permintaan yang identik menunggu untuk diproses (mis., Kami perlu mengonversi gambar yang sama dengan cara yang sama), maka kami harus memproses gambar hanya sekali, dan memberikan hasil pemrosesan sebagai tanggapan untuk tiga permintaan ini. Yaitu Dalam antrian tunggu, permintaan identik harus dikelompokkan.

Sebelumnya di Shrimp, kami menggunakan kunci komposit sederhana untuk mencari cache gambar dan antrian tunggu: kombinasi nama file asli + opsi pengubahan ukuran gambar . Sekarang, dua faktor baru harus diperhitungkan:

  • pertama, format gambar target (mis., gambar asli bisa dalam jpg, dan gambar yang dihasilkan bisa di png);
  • kedua, fakta bahwa memperbesar gambar mungkin tidak perlu. Ini terjadi dalam situasi di mana klien hanya memesan konversi gambar dari satu format ke format lain, tetapi dengan ukuran asli gambar dipertahankan.

Saya harus mengatakan bahwa di sini kami menyusuri jalan yang paling sederhana, tanpa berusaha mengoptimalkan apa pun. Misalnya, seseorang dapat mencoba membuat dua cache: satu akan menyimpan gambar dalam format asli, tetapi diskalakan ke ukuran yang diinginkan, dan yang kedua, gambar skala dikonversi ke format target.

Mengapa caching ganda seperti itu dibutuhkan? Faktanya adalah ketika mengubah gambar, dua operasi termahal dalam waktu adalah mengubah ukuran dan membuat serial gambar ke format target. Oleh karena itu, jika kami menerima permintaan untuk mengubah skala gambar example.jpg ke ukuran 1920 dan mengubahnya menjadi format webp, maka kami dapat menyimpan dua gambar dalam memori kami: example_1920px_width.jpg dan example_1920px_width.webp. Kami akan memberikan gambar example_1920px_width.webp ketika kami menerima permintaan kedua. Tetapi gambar example_1920px_width.jpg dapat digunakan ketika menerima permintaan untuk menskalakan example.jpg dengan ukuran 1920 lebar dan mengubahnya menjadi format heic. Kami dapat melewati operasi pengubahan ukuran dan hanya melakukan konversi format (mis., Gambar yang sudah selesai example_1920px_width.jpg akan ditranskode ke dalam format heic).

Peluang potensial lain: ketika permintaan datang untuk mentranskode gambar ke format lain tanpa mengubah ukuran, Anda dapat menentukan ukuran sebenarnya dari gambar dan menggunakan ukuran ini di dalam kunci komposit. Misalnya, misalkan example.jpg memiliki ukuran 3000x2000 piksel. Jika kami selanjutnya menerima permintaan untuk menskalakan example.jpg hingga ketinggian 2000px, maka kami dapat segera menentukan bahwa kami sudah memiliki gambar dalam ukuran ini.

Secara teori, semua pertimbangan ini patut mendapat perhatian. Tetapi dari sudut pandang praktis, tidak jelas seberapa tinggi kemungkinan perkembangan peristiwa tersebut. Yaitu seberapa sering kita akan menerima permintaan untuk penskalaan example.jpg ke 1920px dengan konversi ke webp, dan kemudian permintaan untuk penskalaan yang sama dari gambar yang sama, tetapi dengan konversi ke png? Tidak memiliki statistik nyata sulit untuk dikatakan. Oleh karena itu, kami memutuskan untuk tidak mempersulit kehidupan kami dalam proyek demo kami, tetapi untuk pergi dulu di jalan yang paling sederhana. Dengan harapan bahwa jika seseorang membutuhkan skema caching yang lebih canggih, maka ini dapat ditambahkan nanti, mulai dari skenario nyata, bukan fiktif, untuk menggunakan udang.

Akibatnya, dalam versi udang yang diperbarui, kami sedikit memperluas kunci, menambahkannya juga parameter seperti format target:

 class resize_request_key_t { std::string m_path; image_format_t m_format; resize_params_t m_params; public: resize_request_key_t( std::string path, image_format_t format, resize_params_t params ) : m_path{ std::move(path) } , m_format{ format } , m_params{ params } {} [[nodiscard]] bool operator<(const resize_request_key_t & o ) const noexcept { return std::tie( m_path, m_format, m_params ) < std::tie( o.m_path, o.m_format, o.m_params ); } [[nodiscard]] const std::string & path() const noexcept { return m_path; } [[nodiscard]] image_format_t format() const noexcept { return m_format; } [[nodiscard]] resize_params_t params() const noexcept { return m_params; } }; 

Yaitu permintaan untuk mengubah ukuran example.jpg hingga 1920px dengan konversi ke png berbeda dari pengubahan ukuran yang sama, tetapi dengan konversi ke webp atau heic.

Tetapi fokus utama bersembunyi di implementasi baru dari kelas resize_params_t , yang menentukan ukuran baru dari gambar yang diskalakan. Sebelumnya, kelas ini mendukung tiga opsi: hanya lebar yang ditetapkan, hanya tinggi yang ditetapkan, atau sisi panjang yang ditetapkan (tinggi atau lebar ditentukan oleh ukuran gambar saat ini). Dengan demikian, metode resize_params_t :: value () selalu mengembalikan beberapa nilai nyata (nilai apa yang ditentukan oleh metode resize_params_t :: mode () ).

Tetapi dalam udang baru, mode lain ditambahkan - keep_original, yang berarti bahwa penskalaan tidak dilakukan dan gambar ditampilkan dalam ukuran aslinya. Untuk mendukung mode ini, resize_params_t harus membuat beberapa perubahan. Pertama, sekarang metode resize_params_t :: make () menentukan apakah mode keep_original digunakan (dianggap bahwa mode ini digunakan jika tidak ada parameter lebar, tinggi dan maks dalam string kueri permintaan yang masuk ditentukan). Ini memungkinkan kami untuk tidak menulis ulang fungsi handle_resize_op_request () , yang mendorong permintaan untuk menskala gambar yang akan dieksekusi.

Kedua, metode resize_params_t :: value () sekarang dapat dipanggil tidak selalu, tetapi hanya ketika mode penskalaan berbeda dari keep_original.

Tetapi yang paling penting adalah bahwa resize_params_t :: operator <() terus bekerja seperti yang dimaksudkan.

Berkat semua perubahan ini di a_transform_manager, cache gambar yang diskalakan dan antrian permintaan tunggu tetap sama. Tetapi sekarang, informasi tentang berbagai pertanyaan disimpan dalam struktur data ini. Jadi, kunci {"example.jpg", "jpg", keep_original} akan berbeda dari kunci {"example.jpg", "png", keep_original}, dan dari kunci {"example.jpg", "jpg", lebar = 1920px}.

Ternyata setelah sedikit manja dengan definisi struktur data sederhana seperti resize_params_t dan resize_params_key_t, kami menghindari mengubah struktur yang lebih kompleks seperti cache gambar yang dihasilkan dan antrian permintaan tunggu.

Dukungan untuk format target di a_transformer


Nah, langkah terakhir dalam mendukung format target adalah untuk memperluas logika agen a_transformer sehingga gambar, mungkin sudah diskalakan, kemudian dikonversi ke format target.

Ternyata menjadi yang paling mudah untuk melakukan ini, yang diperlukan hanyalah memperluas kode metode a_transform_t :: handle_resize_request () :

 [[nodiscard]] a_transform_manager_t::resize_result_t::result_t a_transformer_t::handle_resize_request( const transform::resize_request_key_t & key ) { try { m_logger->trace( "transformation started; request_key={}", key ); auto image = load_image( key.path() ); const auto resize_duration = measure_duration( [&]{ //       //    keep_original. if( transform::resize_params_t::mode_t::keep_original != key.params().mode() ) { transform::resize( key.params(), total_pixel_count, image ); } } ); m_logger->debug( "resize finished; request_key={}, time={}ms", key, std::chrono::duration_cast<std::chrono::milliseconds>( resize_duration).count() ); image.magick( magick_from_image_format( key.format() ) ); datasizable_blob_shared_ptr_t blob; const auto serialize_duration = measure_duration( [&] { blob = make_blob( image ); } ); m_logger->debug( "serialization finished; request_key={}, time={}ms", key, std::chrono::duration_cast<std::chrono::milliseconds>( serialize_duration).count() ); return a_transform_manager_t::successful_resize_t{ std::move(blob), std::chrono::duration_cast<std::chrono::microseconds>( resize_duration), std::chrono::duration_cast<std::chrono::microseconds>( serialize_duration) }; } catch( const std::exception & x ) { return a_transform_manager_t::failed_resize_t{ x.what() }; } } 

Dibandingkan dengan versi sebelumnya, ada dua tambahan mendasar.

Pertama, memanggil metode image.magick () yang benar-benar ajaib setelah mengubah ukuran. Metode ini memberi tahu ImageMagick format gambar yang dihasilkan. Pada saat yang sama, representasi gambar dalam memori tidak berubah - ImageMagick terus menyimpannya sesuai keinginan. Tetapi kemudian nilai yang ditetapkan oleh metode magick () akan diperhitungkan selama panggilan berikutnya ke Image :: write ().

Kedua, versi yang diperbarui mencatat waktu yang diperlukan untuk membuat serialisasi gambar ke format yang ditentukan. Versi baru dari Udang sekarang secara terpisah memperbaiki waktu yang dihabiskan untuk penskalaan, dan waktu yang dihabiskan untuk mengonversi ke format target.

Sisa agen a_transformer_t belum mengalami perubahan apa pun.

Paralelisasi ImageMagick


Secara default, ImageMagic dibangun dengan dukungan OpenMP. Yaitu dimungkinkan untuk memparalelkan operasi pada gambar yang dilakukan ImageMagick. Anda dapat mengontrol jumlah alur kerja yang digunakan ImageMagick dalam hal ini menggunakan variabel lingkungan MAGICK_THREAD_LIMIT.

Misalnya, pada mesin pengujian saya dengan nilai MAGICK_THREAD_LIMIT = 1 (mis., Tanpa paralelisasi nyata), saya mendapatkan hasil berikut:

 curl "http://localhost:8080/DSC08084.jpg?op=resize&max=2400" -v > /dev/null > GET /DSC08084.jpg?op=resize&max=2400 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.47.0 > Accept: */* > < HTTP/1.1 200 OK < Connection: keep-alive < Content-Length: 2043917 < Server: Shrimp draft server < Date: Wed, 15 Aug 2018 11:51:24 GMT < Last-Modified: Wed, 15 Aug 2018 11:51:24 GMT < Access-Control-Allow-Origin: * < Access-Control-Expose-Headers: Shrimp-Processing-Time, Shrimp-Resize-Time, Shrimp-Encoding-Time, Shrimp-Image-Src < Content-Type: image/jpeg < Shrimp-Image-Src: transform < Shrimp-Processing-Time: 1323 < Shrimp-Resize-Time: 1086.72 < Shrimp-Encoding-Time: 236.276 

Waktu yang dihabiskan untuk mengubah ukuran ditunjukkan di tajuk Shrimp-Resize-Time. Dalam hal ini, adalah 1086,72 ms.

Tetapi jika Anda menetapkan MAGICK_THREAD_LIMIT = 3 pada mesin yang sama dan menjalankan Shrimp, maka kami mendapatkan nilai yang berbeda:

 curl "http://localhost:8080/DSC08084.jpg?op=resize&max=2400" -v > /dev/null > GET /DSC08084.jpg?op=resize&max=2400 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.47.0 > Accept: */* > < HTTP/1.1 200 OK < Connection: keep-alive < Content-Length: 2043917 < Server: Shrimp draft server < Date: Wed, 15 Aug 2018 11:53:49 GMT < Last-Modified: Wed, 15 Aug 2018 11:53:49 GMT < Access-Control-Allow-Origin: * < Access-Control-Expose-Headers: Shrimp-Processing-Time, Shrimp-Resize-Time, Shrimp-Encoding-Time, Shrimp-Image-Src < Content-Type: image/jpeg < Shrimp-Image-Src: transform < Shrimp-Processing-Time: 779.901 < Shrimp-Resize-Time: 558.246 < Shrimp-Encoding-Time: 221.655 

Yaitu mengubah ukuran waktu dikurangi menjadi 558,25 ms.

Karena itu, karena ImageMagick menyediakan kemampuan untuk memparalelkan perhitungan, Anda dapat menggunakan kesempatan ini. Tetapi pada saat yang sama, diinginkan untuk dapat mengontrol berapa banyak benang kerja yang dibutuhkan udang untuk dirinya sendiri. Dalam versi udang sebelumnya, tidak mungkin untuk mempengaruhi berapa banyak alur kerja yang diciptakan udang. Dan dalam versi udang yang diperbarui, ini bisa dilakukan. Atau melalui variabel lingkungan, misalnya:

 SHRIMP_IO_THREADS=1 \ SHRIMP_WORKER_THREADS=3 \ MAGICK_THREAD_LIMIT=4 \ shrimp.app -p 8080 -i ... 

Atau melalui argumen baris perintah, misalnya:

 MAGICK_THREAD_LIMIT=4 \ shrimp.app -p 8080 -i ... --io-threads 1 --worker-threads 4 

Nilai yang ditentukan melalui baris perintah memiliki prioritas yang lebih tinggi.

Harus ditekankan bahwa MAGICK_THREAD_LIMIT hanya memengaruhi operasi yang dilakukan ImageMagick sendiri. Misalnya, pengubahan ukuran dilakukan oleh ImageMagick. Tetapi konversi dari satu format ke delegasi ImageMagick lain ke perpustakaan eksternal. Dan bagaimana operasi di perpustakaan eksternal ini diparalelkan adalah masalah terpisah yang tidak kami pahami.

Kesimpulan


Mungkin, dalam versi udang ini, kami membawa proyek demo kami ke kondisi yang dapat diterima. Mereka yang ingin melihat dan bereksperimen dapat menemukan teks sumber Shrimp di BitBucket atau GitHub . Anda juga dapat menemukan Dockerfile di sana untuk membuat udang untuk percobaan Anda.

Secara umum, kami telah mencapai tujuan kami yang kami tetapkan sendiri dengan memulai proyek demo ini. Sejumlah ide muncul untuk pengembangan lebih lanjut dari RESTinio dan SObjectizer, dan beberapa dari mereka telah menemukan perwujudan mereka. Oleh karena itu, apakah udang akan berkembang di suatu tempat lebih lanjut sepenuhnya tergantung pada pertanyaan dan keinginan. Jika ada, maka udang bisa berkembang. Jika tidak, maka Shrimp akan tetap menjadi proyek demo dan tempat pelatihan untuk bereksperimen dengan versi baru RESTinio dan SObjectizer.

Sebagai penutup, saya ingin mengucapkan terima kasih khusus kepada aensidhe atas bantuan dan saran mereka, yang tanpanya tarian kami dengan rebana akan jauh lebih lama dan sedih.

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


All Articles