BerandaComputers and TechnologyMengapa Teknik Uber Beralih dari Postgres ke MySQL (2016)

Mengapa Teknik Uber Beralih dari Postgres ke MySQL (2016)

Pengantar

Arsitektur awal Uber terdiri dari aplikasi backend monolitik yang ditulis dengan Python yang digunakan Postgres untuk persistensi data. Sejak saat itu, arsitektur Uber telah berubah secara signifikan, menjadi model layanan mikro dan baru platform data. Secara khusus, dalam banyak kasus di mana kami sebelumnya menggunakan Postgres, kami sekarang menggunakan Skema , lapisan sharding database baru yang dibangun di atas MySQL. Pada artikel ini, kami akan menjelajahi beberapa kelemahan yang kami temukan dengan Postgres dan menjelaskan keputusan untuk membangun Schemaless dan layanan backend lainnya di atas MySQL.

Arsitektur Postgres

Kami menemukan banyak batasan Postgres:

  • Arsitektur yang tidak efisien untuk menulis
  • Replikasi data yang tidak efisien
  • Masalah dengan kerusakan tabel
  • Dukungan MVCC replika yang buruk
  • Kesulitan dalam mengupgrade ke rilis yang lebih baru

Kami akan melihat semua batasan ini melalui analisis representasi tabel dan data indeks Postgres pada disk, terutama jika dibandingkan dengan cara MySQL merepresentasikan data yang sama dengan Mesin penyimpanan InnoDB . Perhatikan bahwa analisis yang kami sajikan di sini terutama didasarkan pada pengalaman kami dengan seri rilis Postgres 9.2 yang agak lama. Sepengetahuan kami, arsitektur internal yang kami diskusikan dalam artikel ini tidak berubah secara signifikan dalam rilis Postgres yang lebih baru, dan desain dasar representasi on-disk di 9.2 tidak berubah secara signifikan sejak setidaknya rilis Postgres 8.3 (sekarang hampir 10 tahun).

Format Di Disk

Database relasional harus melakukan beberapa tugas utama:

  • Menyediakan kemampuan masukkan / perbarui / hapus
  • Memberikan kemampuan untuk membuat perubahan skema
  • Menerapkan kontrol konkurensi multiversi (MVCC) sehingga koneksi yang berbeda memiliki tampilan transaksional dari data tempat mereka bekerja

Mempertimbangkan bagaimana semua fitur ini akan bekerja bersama adalah bagian penting dalam merancang bagaimana database merepresentasikan data pada disk.

Salah satu aspek desain inti Postgres adalah data baris yang tidak dapat diubah. Baris yang tidak dapat diubah ini disebut “tuple” dalam bahasa Postgres. Tupel ini secara unik diidentifikasi oleh apa yang disebut Postgres sebagai ctid . A ctid secara konseptual mewakili lokasi on-disk (yaitu, offset disk fisik) untuk tupel. Beberapa ctid berpotensi dapat menggambarkan satu baris (misalnya, ketika beberapa versi baris ada untuk tujuan MVCC, atau saat versi lama dari sebuah baris belum diklaim oleh autovacuum ). Kumpulan tupel terorganisir membentuk sebuah tabel. Tabel itu sendiri memiliki indeks, yang diatur sebagai struktur data (biasanya pohon-B) yang memetakan bidang indeks ke ctid payload.

Biasanya, ini ctids transparan bagi pengguna, tetapi mengetahui cara kerjanya membantu Anda memahami struktur tabel Postgres di disk. Untuk melihat arus ctid untuk satu baris, Anda dapat menambahkan “ ctid ”ke daftar kolom di WHERE klausa:

  uber @ [local] uber=> PILIH ctid, FROM my_table LIMIT 1;      - [ RECORD 1 ] -------- + ----------------------- -------      ctid | (0,1)      ... bidang lain di sini ...  

Untuk menjelaskan detail tata letak, mari pertimbangkan contoh tabel pengguna sederhana. Untuk setiap pengguna, kami memiliki kunci utama ID pengguna yang bertambah secara otomatis, nama depan dan belakang pengguna, dan tahun lahir pengguna. Kami juga menentukan indeks sekunder majemuk pada nama lengkap pengguna (nama depan dan belakang) dan indeks sekunder lainnya pada tahun lahir pengguna. The DDL untuk membuatnya tabel mungkin seperti ini:

  BUAT pengguna TABEL (     ID SERIAL,      TEXT pertama,      TEXT terakhir,      INTEGER tahun lahir,      KUNCI UTAMA (id)     );     BUAT INDEKS ix_users_first_last ON pengguna (pertama, terakhir);     BUAT INDEKS ix_users_birth_year ON users (birth_year);  

Perhatikan tiga indeks dalam definisi ini: indeks kunci utama ditambah dua indeks sekunder yang kami tentukan.

Untuk contoh dalam artikel ini, kita akan mulai dengan data berikut di tabel kita, yang terdiri dari pilihan ahli matematika historis yang berpengaruh:

Seperti dijelaskan sebelumnya, setiap baris ini secara implisit memiliki ctid buram yang unik . Oleh karena itu, kita dapat membayangkan representasi internal dari tabel seperti ini:

ctid Indo pertama terakhir tahun lahir
SEBUAH 1 Blaise Pascal 1623
B 2 Gottfried Leibniz 1646
C 3 Emmy Noether 1882
D 4 Muhammad Al -Khwārizmī 780
E 5 Alan Turing 1912
F 6 Srinivasa Ramanujan 1887
G 7 Ada Lovelace 1815
H 8 Henri Poincaré 1854

Indeks kunci utama, yang memetakan id hingga ctids , didefinisikan seperti ini:

Indo ctid
1 SEBUAH
2 B
3 C
4 D
5
6 F
7 G
8

Pohon-B didefinisikan di Indo, dan setiap node di B-tree menyimpan ctid . Perhatikan bahwa dalam kasus ini, urutan bidang di B-tree kebetulan sama dengan urutan di tabel karena penggunaan auto-incrementing id , tetapi ini tidak perlu menjadi kasusnya.

Indeks sekunder terlihat serupa; Perbedaan utamanya adalah bahwa bidang disimpan dalam urutan yang berbeda, karena pohon-B harus diatur secara leksikografis. Pertama, terakhir ) indeks dimulai dengan nama depan di bagian atas alfabet:

pertama terakhir ctid
Ada Lovelace G
Alan Turing
Blaise Pascal SEBUAH
Emmy Noether C
Gottfried Leibniz B
Henri Poincaré
Muhammad Al -Khwārizmī D
Srinivasa Ramanujan F

Demikian pula, tahun lahir indeks dikelompokkan dalam urutan menaik, seperti ini:

tahun lahir ctid
780 D
1623 SEBUAH
1646 B
1815 G
1854
1887 F
1882 C
1912

Seperti yang Anda lihat, di keduanya kasus ini ctid di masing-masing indeks sekunder tidak meningkat secara leksikografis, tidak seperti dalam kasus kunci primer yang bertambah otomatis.

Misalkan kita perlu memperbarui record dalam tabel ini. Misalnya, kita memperbarui bidang tahun kelahiran untuk perkiraan lain tahun lahir al-Khwārizmī, 770 M. Seperti yang kami sebutkan sebelumnya, tupel baris tidak dapat diubah. Oleh karena itu, untuk memperbarui record, kami menambahkan tupel baru ke tabel. Tuple baru ini memiliki opaque ctid , yang akan kami sebut Saya . Postgres harus dapat membedakan tupel baru yang aktif di SAYA dari tupel lama di D . Secara internal, Postgres menyimpan dalam setiap tupel bidang versi dan penunjuk ke tupel sebelumnya (jika ada). Karenanya, struktur baru tabel terlihat seperti ini:

ctid sebelumnya Indo pertama terakhir tahun lahir
SEBUAH batal 1 Blaise Pascal 1623
B batal 2 Gottfried Leibniz 1646
C batal 3 Emmy Noether 1882
D batal 4 Muhammad Al -Khwārizmī 780
E batal 5 Alan Turing 1912
F batal 6 Srinivasa Ramanujan 1887
G batal 7 Ada Lovelace 1815
H batal 8 Henri Poincaré 1854
SAYA D 4 Muhammad Al -Khwārizmī 770

Selama dua versi al Baris -Khwārizmī ada, indeks harus menampung entri untuk kedua baris. Untuk singkatnya, kami menghilangkan indeks kunci utama dan hanya menampilkan indeks sekunder di sini, yang terlihat seperti ini:

pertama terakhir ctid
Ada Lovelace G
Alan Turing
Blaise Pascal SEBUAH
Emmy Noether C
Gottfried Leibniz B
Henri Poincaré
Muhammad Al -Khwārizmī D
Muhammad Al -Khwārizmī SAYA
Srinivasa Ramanujan F
tahun lahir ctid
770 SAYA
780 D
1623 SEBUAH
1646 B
1815 G
1854
1887 F
1882 C
1912

Kami telah mewakili versi lama di merah dan versi baris baru berwarna hijau. Di bawah tenda, Postgres menggunakan penahanan bidang lain versi baris untuk menentukan tupel mana yang paling baru. Bidang tambahan ini memungkinkan database menentukan tupel baris mana yang akan disajikan ke transaksi yang mungkin tidak diizinkan untuk melihat versi baris terbaru.

Postgres_Tuple_Property_
Dengan Postgres, indeks primer dan indeks sekunder semuanya mengarah langsung ke offset tuple di disk. Ketika lokasi tuple berubah, semua indeks harus diperbarui.

Replikasi

Saat kami memasukkan baris baru ke dalam tabel, Postgres perlu mereplikasi jika replikasi streaming diaktifkan. Untuk tujuan pemulihan kerusakan, database sudah memiliki log depan tulis (WAL) dan menggunakannya untuk mengimplementasikan komit dua fase . Basis data harus mempertahankan WAL ini meskipun replikasi streaming tidak diaktifkan karena WAL memungkinkan aspek atomicity dan durabilitas ASAM.

Kita dapat memahami WAL dengan mempertimbangkan apa yang terjadi jika database tidak bekerja secara tidak terduga, seperti saat listrik mati secara tiba-tiba. WAL mewakili buku besar dari perubahan yang direncanakan database untuk konten tabel dan indeks pada disk. Ketika daemon Postgres pertama kali dijalankan, proses membandingkan data di buku besar ini dengan data aktual di disk. Jika buku besar berisi data yang tidak tercermin pada disk, database mengoreksi tupel atau data indeks untuk mencerminkan data yang ditunjukkan oleh WAL. Kemudian mengembalikan data apa pun yang muncul di WAL tetapi berasal dari transaksi yang diterapkan sebagian (artinya transaksi tersebut tidak pernah dilakukan).

Postgres mengimplementasikan replikasi streaming dengan mengirimkan WAL pada database master ke replika. Setiap database replika secara efektif bertindak seolah-olah berada dalam pemulihan kerusakan, terus-menerus menerapkan pembaruan WAL seperti yang akan dilakukan jika dimulai setelah terjadi kerusakan. Satu-satunya perbedaan antara replikasi streaming dan pemulihan kerusakan yang sebenarnya adalah bahwa replika dalam mode “siaga panas” melayani permintaan baca saat menerapkan streaming WAL, sedangkan database Postgres yang sebenarnya dalam mode pemulihan kerusakan biasanya menolak untuk melayani permintaan apa pun sampai instance database menyelesaikan proses pemulihan kerusakan.

Karena WAL sebenarnya dirancang untuk tujuan pemulihan kerusakan, WAL berisi informasi tingkat rendah tentang pembaruan di disk. Konten WAL berada pada level representasi aktual on-disk dari baris tupel dan offset disknya (yaitu baris ctids ). Jika Anda menjeda master Postgres dan replika saat replika tersangkut sepenuhnya, konten aktual di dalam disk pada replika sama persis dengan yang ada di byte master untuk byte. Oleh karena itu, alat seperti rsync dapat memperbaiki replika yang rusak jika ketinggalan zaman dengan master.

Konsekuensi Desain Postgres

Desain Postgres mengakibatkan inefisiensi dan kesulitan untuk kami

    data di Uber .

    Tulis Amplifikasi

    Masalah pertama dengan desain Postgres dikenal dalam konteks lain sebagai tulis amplifikasi . Biasanya, penulisan amplifikasi mengacu pada masalah penulisan data ke disk SSD: pembaruan logis kecil (misalnya, menulis beberapa byte) menjadi pembaruan yang jauh lebih besar dan lebih mahal saat diterjemahkan ke lapisan fisik. Masalah yang sama muncul di Postgres. Dalam contoh kami sebelumnya ketika kami membuat pembaruan logis kecil untuk tahun kelahiran al-Khwārizmī, kami harus mengeluarkan setidaknya empat pembaruan fisik:

    1. Tulis tupel baris baru ke tablespace
    2. Perbarui indeks kunci utama untuk menambahkan catatan untuk tupel baru
    3. Perbarui ( pertama , terakhir ) indeks ke tambahkan catatan untuk tupel baru
    4. Perbarui birth_year indeks untuk menambahkan catatan untuk tupel baru

    Faktanya, keempat pembaruan ini hanya mencerminkan penulisan yang dibuat ke tablespace utama; masing-masing penulisan ini juga perlu direfleksikan di WAL, sehingga jumlah total penulisan pada disk menjadi lebih besar.

    Yang perlu diperhatikan di sini adalah pembaruan 2 dan 3. Ketika kami memperbarui tahun kelahiran untuk al-Khwārizmī, kami tidak benar-benar mengubah kunci utamanya, atau apakah kita mengubah nama depan dan belakangnya. Namun, indeks ini masih harus diperbarui dengan pembuatan tupel baris baru dalam database untuk record baris. Untuk tabel dengan indeks sekunder dalam jumlah besar, langkah-langkah yang berlebihan ini dapat menyebabkan inefisiensi yang sangat besar. Misalnya, jika kita memiliki tabel dengan selusin indeks yang ditentukan di atasnya, pembaruan ke bidang yang hanya dicakup oleh indeks tunggal harus disebarkan ke semua 12 indeks untuk mencerminkan ctid untuk baris baru.

    Replikasi

    Masalah amplifikasi tulis ini secara alami diterjemahkan ke dalam lapisan replikasi juga karena replikasi terjadi pada tingkat perubahan pada disk. Alih-alih mereplikasi catatan logis kecil, seperti “Ubah tahun lahir untuk ctid D sekarang menjadi 770, “database malah menulis entri WAL untuk keempat penulisan yang baru saja kita jelaskan, dan keempat entri WAL ini menyebar melalui jaringan. Dengan demikian, masalah amplifikasi tulis juga diterjemahkan menjadi masalah amplifikasi replikasi, dan aliran data replikasi Postgres dengan cepat menjadi sangat bertele-tele, berpotensi menempati bandwidth dalam jumlah besar.

    Dalam kasus di mana replikasi Postgres terjadi murni dalam satu pusat data, bandwidth replikasi mungkin tidak menjadi masalah. Peralatan dan switch jaringan modern dapat menangani bandwidth dalam jumlah besar, dan banyak penyedia hosting menawarkan bandwidth intra-data center gratis atau murah. Namun, ketika replikasi harus terjadi antar pusat data, masalah dapat meningkat dengan cepat. Misalnya, Uber awalnya menggunakan server fisik di ruang colocation di West Coast. Untuk tujuan pemulihan bencana, kami menambahkan server di ruang kolokasi Pantai Timur kedua. Dalam desain ini kami memiliki contoh master Postgres (ditambah replika) di pusat data barat kami dan satu set replika di bagian timur.

    Replikasi bertingkat membatasi persyaratan bandwidth antar-pusat data ke jumlah replikasi yang diperlukan hanya antara master dan satu replika, bahkan jika ada banyak replika di pusat data kedua. Namun, verbositas protokol replikasi Postgres masih dapat menyebabkan data yang sangat banyak untuk database yang menggunakan banyak indeks. Membeli tautan lintas negara dengan bandwidth yang sangat tinggi itu mahal, dan bahkan dalam kasus di mana uang tidak menjadi masalah, sangat tidak mungkin untuk mendapatkan tautan jaringan lintas negara dengan bandwidth yang sama dengan interkoneksi lokal. Masalah bandwidth ini juga menyebabkan masalah bagi kami dengan pengarsipan WAL. Selain mengirim semua pembaruan WAL dari Pantai Barat ke Pantai Timur, kami mengarsipkan semua WAL ke layanan web penyimpanan file, baik untuk jaminan ekstra bahwa kami dapat memulihkan data jika terjadi bencana dan agar WAL yang diarsipkan dapat muncul. replika baru dari snapshot database. Selama lalu lintas puncak sejak awal, bandwidth kami ke layanan web penyimpanan tidak cukup cepat untuk mengimbangi kecepatan penulisan WAL untuk itu.

    Korupsi Data

    Selama promosi database master rutin untuk meningkatkan kapasitas database, kami mengalami bug Postgres 9.2. Replika yang diikuti sakelar garis waktu salah , menyebabkan beberapa dari mereka salah menerapkan beberapa catatan WAL. Karena bug ini, beberapa record yang seharusnya telah ditandai sebagai tidak aktif oleh mekanisme pembuatan versi sebenarnya tidak ditandai sebagai tidak aktif.

    Kueri berikut menggambarkan bagaimana bug ini akan memengaruhi contoh tabel pengguna kami:

    PILIH DARI pengguna WHERE id=4;

    Kueri ini akan mengembalikan dua catatan: baris al-Khwārizmī asli dengan tahun kelahiran 780 M, ditambah baris baru al-Khwārizmī dengan tahun 770 M. tahun lahir. Jika kami menambahkan ctid ke WHERE daftar, kita akan melihat yang berbeda ctid nilai untuk dua record yang dikembalikan, seperti yang diharapkan untuk dua tupel baris yang berbeda.

    Masalah ini sangat menjengkelkan karena beberapa alasan. Untuk memulai, kami tidak dapat dengan mudah mengetahui berapa banyak baris yang terpengaruh oleh masalah ini. Hasil duplikat yang dikembalikan dari database menyebabkan logika aplikasi gagal dalam beberapa kasus. Kami akhirnya menambahkan pernyataan pemrograman defensif untuk mendeteksi situasi tabel yang diketahui memiliki masalah ini. Karena bug memengaruhi semua server, baris yang rusak berbeda pada contoh replika yang berbeda, artinya pada satu baris replika X mungkin buruk dan baris Y akan bagus, tetapi di baris replika lain X mungkin bagus dan baris Y mungkin buruk. Faktanya, kami tidak yakin tentang jumlah replika dengan data yang rusak dan apakah masalah tersebut telah memengaruhi master.

    Dari apa yang kami ketahui, masalah hanya termanifestasi pada beberapa baris per database, tetapi kami sangat khawatir, karena replikasi terjadi di tingkat fisik , kami dapat merusak indeks basis data kami sepenuhnya. Aspek penting dari B-tree adalah bahwa mereka harus secara berkala menyeimbangkan kembali , dan operasi penyeimbangan ulang ini dapat sepenuhnya mengubah struktur pohon saat sub-pohon dipindahkan ke lokasi disk baru. Jika data yang salah dipindahkan, ini dapat menyebabkan sebagian besar pohon menjadi tidak valid sama sekali.

    Pada akhirnya, kami dapat melacak bug yang sebenarnya dan menggunakannya untuk menentukan bahwa master yang baru dipromosikan tidak memiliki baris yang rusak. Kami memperbaiki masalah korupsi pada replika dengan menyinkronkan ulang semuanya dari snapshot master yang baru, proses yang melelahkan; kami hanya memiliki kapasitas yang cukup untuk mengeluarkan beberapa replika dari kumpulan load balancing pada satu waktu.

    Bug yang kami temui hanya mempengaruhi rilis tertentu dari Postgres 9.2 dan telah diperbaiki untuk waktu yang lama sekarang. Namun, kami masih mengkhawatirkan bahwa bug kelas ini bisa terjadi sama sekali. Versi baru Postgres dapat dirilis kapan saja dengan bug seperti ini, dan karena cara kerja replikasi, masalah ini berpotensi menyebar ke semua basis data dalam hierarki replikasi.

    Replika MVCC

    Postgres tidak memiliki dukungan MVCC replika yang sebenarnya. Fakta bahwa replika menerapkan pembaruan WAL menghasilkan mereka memiliki salinan data pada disk yang identik dengan master pada titik waktu tertentu. Desain ini menimbulkan masalah bagi Uber.

    Postgres perlu menyimpan salinan versi baris lama untuk MVCC. Jika replika streaming memiliki transaksi terbuka, pembaruan ke database diblokir jika memengaruhi baris yang dibuka oleh transaksi. Dalam situasi ini, Postgres menjeda utas aplikasi WAL hingga transaksi berakhir. Ini bermasalah jika transaksi membutuhkan waktu lama, karena replika bisa sangat tertinggal di belakang master. Oleh karena itu, Postgres menerapkan batas waktu dalam situasi seperti itu: jika transaksi memblokir aplikasi WAL untuk menetapkan jumlah waktu , Postgres membunuh transaksi itu.

    Desain ini berarti replika dapat secara rutin tertinggal beberapa detik di belakang master, dan oleh karena itu mudah untuk menulis kode yang mengakibatkan transaksi yang terbunuh. Masalah ini mungkin tidak terlihat oleh pengembang aplikasi yang menulis kode yang mengaburkan di mana transaksi dimulai dan diakhiri. Misalnya, pengembang memiliki beberapa kode yang harus mengirimkan tanda terima melalui email kepada pengguna. Bergantung pada cara penulisannya, kode mungkin secara implisit memiliki transaksi database yang tetap dibuka hingga email selesai dikirim. Meskipun selalu merupakan bentuk yang buruk untuk membiarkan kode Anda menahan transaksi basis data terbuka saat melakukan I / O pemblokiran yang tidak terkait, kenyataannya adalah bahwa sebagian besar teknisi bukanlah ahli basis data dan mungkin tidak selalu memahami masalah ini, terutama saat menggunakan ORM yang mengaburkan detail tingkat rendah seperti transaksi terbuka.

    Peningkatan Postgres

    Karena catatan replikasi bekerja pada tingkat fisik, tidak mungkin untuk mereplikasi data antara rilis ketersediaan umum Postgres yang berbeda. Database master yang menjalankan Postgres 9.3 tidak dapat mereplikasi ke replika yang menjalankan Postgres 9.2, juga tidak dapat mereplikasi master yang menjalankan 9.2 ke replika yang menjalankan Postgres 9.3.

    Kami mengikuti langkah-langkah ini untuk meningkatkan dari satu rilis Postgres GA ke yang lain:

  • Matikan database master.
  • Jalankan perintah bernama pg_upgrade pada master, yang memperbarui master data di tempat. Ini bisa memakan waktu berjam-jam untuk database besar, dan tidak ada lalu lintas yang dapat dilayani dari master saat proses ini berlangsung.
  • Mulai master lagi.
  • Buat snapshot baru dari master. Langkah ini sepenuhnya menyalin semua data dari master, sehingga membutuhkan waktu berjam-jam untuk database yang besar.
  • Hapus setiap replika dan pulihkan snapshot baru dari master ke replika.
  • Kembalikan setiap replika ke dalam hierarki replikasi. Tunggu hingga replika sepenuhnya mengikuti semua pembaruan yang diterapkan oleh master saat replika tersebut dipulihkan.

Kami mulai dengan Postgres 9.1 dan berhasil diselesaikan proses peningkatan untuk pindah ke Postgres 9.2. Namun, prosesnya memakan waktu berjam-jam sehingga kami tidak mampu melakukan proses itu lagi. Pada saat Postgres 9.3 keluar, pertumbuhan Uber meningkatkan kumpulan data kami secara substansial, sehingga peningkatan versi akan menjadi lebih lama. Untuk alasan ini, instans Postgres lama kami menjalankan Postgres 9.2 hingga hari ini, meskipun rilis Postgres GA saat ini adalah 9,5.

Jika Anda menjalankan Postgres 9.4 atau lebih baru, Anda dapat menggunakan sesuatu seperti pglogical , yang mengimplementasikan lapisan replikasi logis untuk Postgres. Dengan menggunakan pglogical, Anda dapat mereplikasi data di antara rilis Postgres yang berbeda, yang berarti dimungkinkan untuk melakukan peningkatan seperti 9.4 hingga 9.5 tanpa menimbulkan waktu henti yang signifikan. Kemampuan ini masih bermasalah karena tidak terintegrasi ke dalam pohon jalur utama Postgres, dan pglogical masih belum menjadi pilihan bagi orang yang menjalankan rilis Postgres yang lebih lama.

Arsitektur MySQL

Selain menjelaskan beberapa batasan Postgres, kami juga menjelaskan mengapa MySQL adalah alat penting untuk proyek penyimpanan Teknik Uber yang lebih baru, seperti Schemaless. Dalam banyak kasus, kami menemukan MySQL lebih disukai untuk penggunaan kami. Untuk memahami perbedaannya, kami memeriksa arsitektur MySQL dan bagaimana kontrasnya dengan Postgres. Kami secara khusus menganalisis cara kerja MySQL dengan Mesin penyimpanan InnoDB . Kami tidak hanya menggunakan InnoDB di Uber; ini mungkin mesin penyimpanan MySQL paling populer.

Representasi On-Disk InnoDB

Seperti Postgres, InnoDB mendukung fitur-fitur canggih seperti MVCC dan data yang dapat berubah. Pembahasan menyeluruh tentang format dalam disk InnoDB berada di luar cakupan artikel ini; sebagai gantinya, kami akan fokus pada perbedaan intinya dari Postgres.

Perbedaan arsitektur yang paling penting adalah bahwa sementara Postgres secara langsung memetakan catatan indeks ke lokasi di disk, InnoDB mempertahankan struktur sekunder. Alih-alih menahan penunjuk ke lokasi baris di disk (seperti ctid tidak di Postgres), catatan indeks sekunder InnoDB menyimpan penunjuk ke nilai kunci utama. Jadi, indeks sekunder di MySQL mengaitkan kunci indeks dengan kunci utama:

pertama terakhir id (kunci utama)
Ada Lovelace 7
Alan Turing 5
Blaise Pascal 1
Emmy Noether 3
Gottfried Leibniz 2
Henri Poincaré 8
Muhammad Al -Khwārizmī 4
Srinivasa Ramanujan 6

Untuk melakukan pencarian indeks indeks (pertama, terakhir), kita sebenarnya perlu melakukan dua pencarian. Pencarian pertama mencari tabel dan menemukan kunci utama untuk sebuah rekaman. Setelah kunci utama ditemukan, pencarian kedua mencari indeks kunci utama untuk menemukan lokasi di disk untuk baris tersebut.

Desain ini berarti bahwa InnoDB sedikit kurang menguntungkan bagi Postgres saat melakukan pencarian kunci sekunder, karena dua indeks harus dicari dengan InnoDB dibandingkan dengan hanya satu untuk Postgres. Namun, karena datanya dinormalisasi, pembaruan baris hanya perlu memperbarui rekaman indeks yang sebenarnya diubah oleh pembaruan baris. Selain itu, InnoDB biasanya melakukan pembaruan baris di tempat. Jika transaksi lama perlu mereferensikan baris untuk keperluan MVCC, MySQL menyalin baris lama ke area khusus yang disebut segmen kembalikan .

Mari kita ikuti apa yang terjadi ketika kita memperbarui tahun kelahiran al-Khwārizmī. Jika ada spasi, field tahun lahir di baris dengan id 4 diperbarui di tempat (pada kenyataannya, pembaruan ini selalu terjadi di tempat, karena tahun kelahiran adalah bilangan bulat yang menempati jumlah ruang tetap). Indeks tahun lahir juga diperbarui untuk mencerminkan tanggal baru. Data baris lama disalin ke segmen rollback. Indeks kunci utama tidak perlu diperbarui, begitu pula ( pertama , terakhir ) indeks nama. Jika kami memiliki banyak indeks di tabel ini, kami masih hanya perlu memperbarui indeks yang sebenarnya mengindeks di atas tahun_ lahir . Jadi katakanlah kita memiliki indeks di atas bidang seperti signup_date , waktu_login terakhir , dll. Kami tidak perlu memperbarui indeks ini, sedangkan Postgres harus melakukannya.

Desain ini juga membuat penyedotan dan pemadatan lebih efisien. Semua baris yang memenuhi syarat untuk disedot tersedia langsung di segmen rollback. Sebagai perbandingan, proses autovacuum Postgres harus melakukan pemindaian tabel lengkap untuk mengidentifikasi baris yang dihapus.

MySQL_Index_Property_
MySQL menggunakan lapisan tambahan tipuan: catatan indeks sekunder mengarah ke catatan indeks utama, dan indeks utama itu sendiri menyimpan lokasi baris di disk. Jika offset baris berubah, hanya indeks utama yang perlu diperbarui.

Replikasi

MySQL mendukung banyak mode replikasi yang berbeda :

  • Replikasi berbasis pernyataan mereplikasi pernyataan SQL logis (misalnya, secara harfiah mereplikasi pernyataan literal seperti: UPDATE pengguna SET birth_year=770 WHERE id=4 )
  • Replikasi berbasis baris mereplikasi rekaman baris yang diubah
  • Replikasi campuran menggabungkan kedua mode ini

Ada berbagai pengorbanan untuk mode ini. Replikasi berbasis pernyataan biasanya paling kompak tetapi dapat memerlukan replika untuk menerapkan pernyataan mahal untuk memperbarui sejumlah kecil data. Di sisi lain, replikasi berbasis baris, mirip dengan replikasi Postgres WAL, lebih bertele-tele tetapi menghasilkan pembaruan yang lebih dapat diprediksi dan efisien pada replika.

Di MySQL, hanya indeks utama yang memiliki penunjuk ke offset baris di disk. Ini memiliki konsekuensi penting dalam hal replikasi. Aliran replikasi MySQL hanya perlu berisi informasi tentang pembaruan logis untuk baris. Pembaruan replikasi adalah variasi “Ubah stempel waktu untuk baris X dari T _ 1 hingga T _ 2 . ”Replika secara otomatis menyimpulkan perubahan indeks apa pun yang perlu dibuat sebagai hasil dari pernyataan ini.

Sebaliknya, aliran replikasi Postgres berisi perubahan fisik, seperti “Pada disk offset 8.382.491, tulis byte XYZ . ”Dengan Postgres, setiap perubahan fisik yang dilakukan pada disk perlu disertakan dalam aliran WAL. Perubahan logika kecil (seperti memperbarui stempel waktu) memerlukan banyak perubahan pada disk: Postgres harus memasukkan tupel baru dan memperbarui semua indeks agar mengarah ke tupel tersebut. Dengan demikian, banyak perubahan akan dimasukkan ke aliran WAL. Perbedaan desain ini berarti bahwa log biner replikasi MySQL secara signifikan lebih kompak daripada aliran WAL PostgreSQL.

Bagaimana setiap aliran replikasi bekerja juga memiliki konsekuensi penting tentang bagaimana MVCC bekerja dengan replika. Karena aliran replikasi MySQL memiliki pembaruan logis, replika dapat memiliki semantik MVCC yang sebenarnya; oleh karena itu, membaca kueri tentang replika tidak akan memblokir aliran replikasi. Sebaliknya, aliran WAL Postgres berisi perubahan fisik pada disk, sehingga replika Postgres tidak dapat menerapkan pembaruan replikasi yang bertentangan dengan kueri baca, sehingga mereka tidak dapat mengimplementasikan MVCC.

Arsitektur replikasi MySQL berarti bahwa jika bug menyebabkan kerusakan tabel, masalahnya kemungkinan besar tidak akan menyebabkan kegagalan besar. Replikasi terjadi di lapisan logis, jadi operasi seperti menyeimbangkan kembali B-tree tidak pernah dapat menyebabkan indeks rusak. Masalah replikasi MySQL yang khas adalah kasus pernyataan yang dilewati (atau, lebih jarang, diterapkan dua kali). Hal ini dapat menyebabkan data hilang atau tidak valid, tetapi tidak akan menyebabkan pemadaman database.

Akhirnya, arsitektur replikasi MySQL membuatnya mudah untuk mereplikasi antara rilis MySQL yang berbeda. MySQL hanya menambah versinya jika format replikasi berubah, yang tidak biasa di antara berbagai rilis MySQL. Format replikasi logis MySQL juga berarti bahwa perubahan pada disk di lapisan mesin penyimpanan tidak memengaruhi format replikasi. Cara umum untuk melakukan pemutakhiran MySQL adalah dengan menerapkan pembaruan ke satu replika dalam satu waktu, dan setelah Anda memperbarui semua replika, Anda akan mempromosikan salah satunya menjadi replika master baru. Ini dapat dilakukan dengan waktu henti hampir nol, dan ini menyederhanakan menjaga MySQL tetap mutakhir.

Keunggulan Desain MySQL Lainnya

Sejauh ini, kami telah berfokus pada arsitektur on-disk untuk Postgres dan MySQL. Beberapa aspek penting lainnya dari arsitektur MySQL menyebabkannya berkinerja jauh lebih baik daripada Postgres, juga.

The Buffer Pool

Pertama, caching bekerja secara berbeda di dua database. Postgres mengalokasikan beberapa memori untuk cache internal, tetapi cache ini biasanya kecil dibandingkan dengan jumlah total memori pada mesin. Untuk meningkatkan kinerja, Postgres memungkinkan kernel untuk secara otomatis menyimpan data disk yang baru diakses melalui cache halaman . Misalnya, replika Postgres terbesar kami memiliki memori 768 GB yang tersedia, tetapi sebenarnya hanya sekitar 25 GB dari memori itu Memori RSS disalahkan oleh proses Postgres. Ini menyisakan lebih dari 700 GB memori kosong ke cache halaman Linux.

Masalah dengan desain ini adalah bahwa mengakses data melalui cache halaman sebenarnya agak mahal dibandingkan dengan mengakses memori RSS. Untuk mencari data dari disk, masalah proses Postgres lseek (2) dan baca (2) panggilan sistem untuk menemukan data. Masing-masing panggilan sistem ini menimbulkan sakelar konteks, yang lebih mahal daripada mengakses data dari memori utama. Faktanya, Postgres bahkan tidak sepenuhnya dioptimalkan dalam hal ini: Postgres tidak menggunakan pread (2) panggilan sistem, yang menyatu mencari + operasi baca ke dalam satu panggilan sistem.

Sebagai perbandingan, mesin penyimpanan InnoDB mengimplementasikan LRU-nya sendiri dalam sesuatu yang disebut InnoDB kumpulan buffer . Ini secara logis mirip dengan cache halaman Linux tetapi diterapkan di ruang pengguna. Meskipun jauh lebih rumit daripada desain Postgres, desain kumpulan penyangga InnoDB memiliki beberapa keuntungan besar:

  1. Itu memungkinkan untuk menerapkan LRU kustom rancangan. Misalnya, dimungkinkan untuk mendeteksi pola akses patologis yang akan meledakkan LRU dan mencegahnya melakukan terlalu banyak kerusakan.
  2. Ini menghasilkan sakelar konteks yang lebih sedikit. Data yang diakses melalui kumpulan buffer InnoDB tidak memerlukan sakelar konteks pengguna / kernel. Perilaku kasus terburuk adalah terjadinya TLB miss , yang relatif murah dan dapat diminimalkan dengan menggunakan halaman besar .

Penanganan Koneksi

MySQL mengimplementasikan koneksi serentak dengan menghasilkan thread-per-connection. Ini adalah biaya overhead yang relatif rendah; setiap utas memiliki beberapa overhead memori untuk ruang tumpukan, ditambah beberapa memori yang dialokasikan di heap untuk buffer khusus koneksi. Tidak jarang untuk menskalakan MySQL hingga 10.000 atau lebih koneksi bersamaan, dan pada kenyataannya kami mendekati koneksi ini mengandalkan beberapa instance MySQL kami hari ini.

Postgres, bagaimanapun, menggunakan desain proses-per-koneksi. Ini jauh lebih mahal daripada desain utas per sambungan karena sejumlah alasan. Proses forking baru membutuhkan lebih banyak memori daripada menelurkan utas baru. Selain itu, IPC jauh lebih mahal antar proses daripada antar utas. Postgres 9.2 menggunakan Sistem V IPC primitif untuk IPC, bukan ringan futex saat menggunakan utas. Futex lebih cepat daripada Sistem V IPC karena dalam kasus umum di mana futex tidak terkontrol, tidak perlu membuat sakelar konteks.

Selain memori dan overhead IPC yang terkait dengan desain Postgres, Postgres tampaknya hanya memiliki dukungan yang buruk untuk menangani jumlah koneksi yang besar, bahkan ketika ada cukup memori yang tersedia . Kami mengalami masalah signifikan saat menskalakan Postgres setelah beberapa ratus koneksi aktif. Sementara dokumentasinya tidak terlalu spesifik tentang mengapa , sangat disarankan untuk menggunakan mekanisme penyatuan koneksi di luar proses untuk mengukur jumlah koneksi yang besar dengan Postgres. Karenanya, menggunakan pgbouncer untuk melakukan penyatuan koneksi dengan Postgres umumnya berhasil bagi kami. Namun, kami terkadang mengalami bug aplikasi dalam layanan backend kami yang menyebabkan mereka membuka lebih banyak koneksi aktif (biasanya koneksi “menganggur dalam transaksi”) daripada layanan yang seharusnya digunakan, dan bug ini telah menyebabkan waktu henti yang lama bagi kami.

Kesimpulan

Postgres sangat membantu kami di masa-masa awal Uber, tetapi kami mengalami masalah signifikan dalam menskalakan Postgres dengan pertumbuhan kami. Hari ini, kami memiliki beberapa contoh Postgres lama, tetapi sebagian besar database kami dibangun di atas MySQL (biasanya menggunakan Skema ) atau, dalam beberapa kasus khusus, database NoSQL seperti Cassandra. Kami umumnya cukup senang dengan MySQL, dan kami mungkin memiliki lebih banyak artikel blog di masa mendatang yang menjelaskan beberapa kegunaannya yang lebih canggih di Uber.

Evan Klitzke adalah seorang staf insinyur perangkat lunak di dalam Uber Engineering grup infrastruktur inti. Dia juga seorang basis data antusias dan bergabung dengan Uber sebagai teknisi burung awal pada September 2012.

Read More

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments