Salah satu tenet pemandu dari cara kami membangun solusi di Amazon adalah: Menghindari berjalan melalui pintu-pintu satu arah. Ini artinya bahwa kami menjaga jarak dengan pilihan-pilihan yang sulit untuk dikembalikan atau diperluas. Kami menerapkan tenet ini di semua langkah pengembangan software—dari mulai merancang produk, fitur, API, dan sistem backend hingga penerapan. Dalam artikel ini, saya akan menjelaskan cara kami menerapkan tenet ini ke penerapan software.

Sebuah penerapan memerlukan lingkungan software dari satu kondisi (versi) ke kondisi lainnya. Software tersebut mungkin berfungsi dengan sempurna dalam kondisi-kondisi tersebut. Namun, software mungkin tidak berfungsi dengan benar selama atau setelah transisi maju (upgrade atau roll-forward) atau transisi mundur (downgrade atau rollback). Saat software tidak berfungsi dengan baik, maka akan mengakibatkan gangguan layanan yang membuatnya tidak dapat diandalkan untuk pelanggan. Dalam artikel ini, saya mengasumsikan bahwa kedua versi software berfungsi sebagaimana yang diharapkan. Fokus saya adalah pada cara memastikan agar saat bergulir maju atau mundur selama penerapan tidak mengakibatkan kesalahan.

Sebelum merilis versi baru software, kami mengujinya dalam lingkungan pengujian beta atau gamma pada beberapa dimensi seperti fungsionalitas, konkurensi, kinerja, skala, dan penanganan kegagalan downstream. Pengujian ini membantu kami mengungkap beberapa masalah dalam versi baru dan memperbaikinya. Namun, memastikan keberhasilan penerapan saja mungkin tidak selalu memadai. Kami mungkin menghadapi kondisi tak terduga atau perilaku software yang kurang optimal dalam lingkungan produksi. Di Amazon, kami ingin menghindari memosisikan diri dalam situasi di mana melakukan rollback penerapan dapat menyebabkan kesalahan untuk pelanggan kita. Untuk menghindari agar tidak berada dalam situasi ini, kami sepenuhnya menyiapkan diri untuk rollback sebelum setiap penerapan. Versi software yang dapat di-rollback tanpa kesalahan atau gangguan pada fungsi yang tersedia dalam versi sebelumnya disebut kompatibel balik. Kami merencanakan dan memverifikasi bahwa software kami kompatibel balik pada setiap revisi.

Sebelum saya masuk ke dalam detail tentang cara Amazon mendekati update software, ayo kita diskusikan beberapa perbedaan antara penerapan software berdiri sendiri dan terdistribusi.

Penerapan software berdiri sendiri vs terdistribusi

Untuk software berdiri sendiri yang berjalan sebagai satu proses pada satu perangkat, penerapan bersifat atomik. Dua versi software tak pernah berjalan secara bersamaan. Jika software berdiri sendiri mempertahankan status, maka versi baru harus membaca (yaitu, mendeserialisasi) data yang ditulis (yaitu, diserialisasi) oleh versi lawas dan sebaliknya. Dengan memenuhi kondisi ini membuat penerapan aman untuk bergulir maju dan mundur.
 
Di sistem terdistribusi, penerapan menjadi lebih kompleks. Penerapan dilakukan melalui update bergulir sehingga ketersediaan tidak terdampak. Versi baru digulir keluar ke kumpulan host secara bersamaan sehingga host-host yang lain dapat terus melayani permintaan. Umumnya, host-host ini saling berkomunikasi melalui panggilan prosedur jarak jauh (RPC) atau status terus-menerus yang dibagikan (misalnya, metadata atau checkpoint). Komunikasi atau berbagi status semacam ini dapat menghadirkan tambahan tantangan. Penulis dan pembaca bisa jadi menjalankan versi software yang berbeda-beda. Akibatnya, mereka dapat menerjemahkan data secara berbeda. Pembaca mungkin gagal untuk membaca data sama sekali, yang menyebabkan penghentian operasi.

Masalah dengan perubahan protokol

Kami mendapati bahwa alasan paling umum atas yang menyebabkan tidak bisa bergulir balik adalah perubahan protokol. Misalnya, sebuah perubahan kode yang mulai mengomppresi data selagi mempertahankannya ke disk. Setelah versi baru menulis beberapa data terkompresi, menggulir balik bukanlah sebuah opsi. Versi lawas tidak tahu bahwa versi ini harus mendekompresi data setelah membaca dari disk. Jika data disimpan dalam sebuah blob atau gudang dokumen, maka server lainnya akan gagal untuk membacanya bahkan saat penerapan sedang berlangsung. Jika data ini dilewatkan di antara dua proses dan server, maka penerima akan gagal untuk membacanya.

Terkadang, perubahan protokol dapat sangat halus. Misalnya, bayangkan dua server yang berkomunikasi secara tidak sinkron melalui sebuah koneksi. Untuk menjaga agar satu sama lain tetap sadar bahwa mereka hidup, mereka setuju untuk saling mengirimkan detak jantung setiap lima detik. Jika server tidak melihat detak jantung di dalam waktu yang ditetapkan, maka server menganggap bahwa server lain berhenti dan menutup koneksi.

Sekarang, bayangkan penerapan yang menambah periode detak jantung ke 10 detik. Komit kode terlihat minor—hanya perubahan angka. Namun, saat ini tidaklah aman untuk bergulir maju dan mundur. Selama penerapan, server yang menjalankan versi baru mengirimkan detak jantung setiap 10 detik. Akibatnya, server yang menjalankan versi lawas tidak melihat detak jantung selama lebih dari lima detik dan menghentikan koneksi dengan server yang menjalankan versi baru. Di armada yang besar, situasi ini dapat terjadi dengan beberapa koneksi, yang mengakibatkan menurunnya ketersediaan.

Perubahan halus semacam ini susah dianalisis dengan membaca dokumen kode atau desain. Oleh karena itu, kami secara eksplisit memastikan bahwa tiap penerapan aman untuk bergulir maju dan mundur.

Teknik penerapan dua-fase

Satu cara yang kami pastikan bahwa kami dapat bergulir balik dengan aman adalah dengan menggunakan teknik yang umum disebut sebagai penerapan dua-fase. Pertimbangkan skenario hipotetis berikut dengan layanan yang mengelola data (menulis ke, membaca dari) pada Amazon Simple Storage Service (Amazon S3). Layanan ini berjalan pada armada server di seluruh Availability Zone untuk menskalakan dan ketersediaan.

Saat ini, layanan menggunakan format XML untuk mempertahankan data. Sebagaimana ditampilkan di diagram berikut dalam versi V1, semua server menulis dan membaca XML. Untuk alasan bisnis, kami ingin mempertahankan data dalam format JSON. Jika kami melakukan perubahan dalam satu penerapan, server yang mengambil perubahan akan menulis dalam JSON. Tapi, server lainnya tidak tahu cara membaca JSON. Situasi ini menyebabkan kesalahan. Oleh karena itu, kami membagi perubahan tersebut menjadi dua bagian dan melakukan penerapan dua-fase.

Sebagaimana ditampilkan di diagram sebelumnya, kami menyebut fase pertama Siapkan. Di fase ini, kami menyiapkan semua server untuk membaca JSON (selain XML) tapi server-server itu terus menulis XML dengan menerapkan versi V2. Perubahan ini tidak mengubah apa pun dari sudut pandang operasional. Semua server masih dapat membaca XML, dan semua data masih ditulis dalam XML. Jika kita memutuskan untuk menggulir balik perubahan ini, server akan kembali ke kondisi di mana server-server itu tidak dapat membaca JSON. Ini bukanlah masalah karena belum ada data yang ditulis dalam JSON sebelumnya.

Sebagaimana ditampilkan dalam diagram sebelumnya, kita menyebut fase kedua Aktifkan. Di fase ini, kita menyiapkan server untuk menggunakan format JSON untuk menulis dengan menerapkan versi V3. Saat tiap server mengambil perubahan ini, maka server mulai menulis dalam JSON. Server yang belum mengambil perubahan ini masih dapat membaca JSON karena mereka disiapkann di fase pertama. Jika kita memutuskan untuk menggulir balik perubahan ini, semua data yang ditulis oleh server yang sementara ada di fase Aktifkan akan dalam format JSON. Data yang ditulis oleh server yang tidak dalam fase Aktifkan akan dalam format XML. Situasi ini tidak menjadi masalah karena, sebagaimana ditampilkan di V2, server masih bisa membaca XML dan JSON setelah rollback.

Meski diagram sebelumnya menunjukkan perubahan format serialisasi dari XML ke JSON, teknik umum berlaku untuk semua situasi yang dijelaskan di bagian Perubahan protokol sebelumnya. Misalnya, pikirkan untuk kembali ke skenario sebelumnya di mana periode detak jantung antarserver harus ditambah dari lima ke 10 detik. Di fase Siapkan, kita dapat membuat semua server melonggarkan periode detak jantung yang diharapkan ke 10 detik meski semua server terus mengirim detak jantung setelah setiap lima detik. Di fase Aktifkan, kita mengubah frekuensi ke sekali setiap 10 detik.

Tindakan pencegahan dengan penerapan dua-fase

Saat ini, saya akan menjelaskan tindakan pencegahan yang kita lakukan selagi mengikuti teknik penerapan dua-fase. Meski saya mengacu pada skenario contoh yang dijelaskan di bagian sebelumnya, tindakan pencegahan ini berlaku untuk penerapan dua-fase paling banyak.

Ada banyak alat penerapan yang memungkinkan pengguna untuk mempertimbangkan keberhasilan penerapan jika jumlah minimal host mengambil perubahan dan melaporkan diri sendiri sebagai sehat. Misalnya, AWS CodeDeploy memiliki konfigurasi penerapan yang disebut minimumHealthyHosts.

Asumsi kritis dalam contoh penerapan dua-fase tersebut adalah, di akhir fase pertama, semua server telah di-upgrade untuk membaca XML dan JSON. Jika satu atau lebih server gagal untuk meng-upgrade selama fase pertama, maka mereka akan gagal untuk membaca data selama dan setelah fase kedua. Oleh karena itu, kami secara eksplisit memverfikasi bahwa semua server telah mengambil perubahan di fase Siapkan.

Saat saya berkerja dengan Amazon DynamoDB, kami memutuskan untuk mengubah protokol komunikasi antara sejumlah besar server yang mencakup beberapa layanan mikro. Saya mengkoordinasikan penerapan-penerapan di antara semua layanan mikro agar semua server mencapai fase Siapkan terlebih dulu lalu diteruskan ke fase Aktifkan. Sebagai tindakan pencegahan, saya secara eksplisit memverifikasikan bahwa penerapan berhasil pada tiap server di akhir tiap fase.

Meski tiap dua fase aman untuk menggulir balik, kita tidak bisa menggulir balik kedua perubahan. Di contoh sebelumnya, di akhir fase Aktifkan, server menulis data dalam format JSON. Versi software yang digunakan sebelum perubahan Siapkan dan Aktifkan tidak tahu cara membaca JSON. Oleh karena itu, sebagai tindakan pencegahan, kami membiarkan priode waktu yang cukup berlalu di antara fase Siapkan dan Aktifkan. Kami menyebut waktu ini periode memanggang, dan durasinya biasanya beberapa hari. Kami menunggu untuk memastikan bahwa kami tidak harus bergulir balik ke versi sebelumnya.

Setelah fase Aktifkan, kami tidak dapat dengan aman menghapus kemampuan software untuk membaca XML. Kemampuan software ini tidak aman untuk dihapus karena semua data yang ditulis sebelum fase Siapkan dalam format XML. Kita hanya bisa menghapus kemampuannya untuk membaca XML setelah memastikan bahwa setiap obyek telah ditulis ulang di JSON. Kami menyebut proses ini backfilling. Ini mungkin memerlukan tambahan alat yang dapat berfungsi secara bersamaan selagi layanan menulis dan membaca data.

Praktik terbaik untuk serialisasi

Kebanyakan software melibatkan menserialisasikan data—baik untuk persistensi atau transfer melalui jaringan. Seiring perkembangan, perubahan logika serialisasi adalah hal yang umum. Perubahan dapat mencakup dari menambahkan bidang baru hinggan mengubah format secara penuh. Selama bertahun-tahun, kami telah melalui beberapa praktik terbaik yang kami ikuti untuk serialisasi:

• Kami biasanya menghindari mengembangkan format serialisasi khusus.

Logika awal untuk serialisasi khusus mungkin terlihat sepele dan bahkan memberikan kinerja yang lebih baik. Namun, iterasi berikutnya dari format menghadirkan tantangan-tantangan yang telah dipecahkan oleh kerangka kerja yang dibangun dengan baik seperti JSON, Protocol Buffers, Cap’n Proto, dan FlatBuffers. Jika digunakan dengan benar, kerangka kerja ini memberikan fitur aman seperti melepaskan, kompatibilitas mundur, dan pelacakan eksistensi atribut (yaitu, apakah sebuah bidang diatur secara eksplisit atau secara eksplisit menetapkan nilai default).

• Dengan tiap perubahan, kami secara eksplisit menetapkan versi berbeda ke serializer.

Kami melakukan kode sumber atau penentuan versi build independen ini. Kami juga menyimpan versi serializer dengan data terserialisasi atau di metadata. Versi serializer lebih lawas terus berfungsi di software baru. Kami mendapati bahwa biasanya memancarkan metrik untuk versi data yang ditulis atau dibaca akan membantu. Ini memberikan visibilitas dan informasi pemecahan masalah kepada operator jika tidak ada kesalahan. Semua ini berlaku untuk versi RPC dan API juga.

• Kami menghindari untuk menserialisasi struktur data yang tidak bisa kami kontrol.

Misalnya, kami dapat menserialisasi obyek koleksi Java menggunakan refleksi. Tapi saat kami mencoba untuk meng-upgrade JDK, implementasi yang mendasari dari kelas-kelas tersebut mungkin berubah, yang menyebabkan deserialisasi gagal. Risiko ini juga berlaku untuk kelas-kelas dari perpustakaan yang dibagi ke seluruh tim.

• Biasanya, kami merancang serializer untuk memungkinkan keberadaan atribut tak dikenal.
 
Jika dimungkinkan, serializer kami menyimpan atribut tak dikenal selagi menulis kembali data tersebut. Dengan akomodasi ini, meski sebuah server yang menjalankan versi software baru menyertakan atribut baru di data selagi menserialisasi, server yang menjalankan versi lawas tidak akan menghapus atribut selagi memperbarui data yang sama. Jadi, penerapan dua-fase tidaklah perlu.

Sama halnya dengan praktik-praktik terbaik kami, kami membagikannya dengan kehati-hatian karena panduan kami tidak berlaku untuk semua aplikasi dan skenario.

Memverifikasi bahwa perubahan aman untuk rollback

Biasanya, kami secara eksplisit memverifikasi bahwa perubahan software aman untuk menggulir maju dan mundur melalui apa yang kami sebut pengujian upgrade-downgrade. Untuk proses ini, kami menyiapkan lingkungan pengujian yang representatif di lingkungan produksi. Selama bertahun-tahun, kami telah mengidentifikasi beberapa pola yang kami hindari saat menyiapkan lingkungan pengujian.

Saya telah melihat situasi di mana menerapkan perubahan dalam produksi menyebabkan kesalahan meski perubahan telah lulus semua pengujian di lingkungan pengujian. Pada satu kesempatan, layanan di lingkungan pengujian masing-masing hanya memiliki satu server. Jadi, semua penerapan bersifat atomik yang menghalangi kemungkinan menjalankan versi-versi yang berbeda dari software secara bersamaan. Saat ini, meski lalu lintas lingkungan pengujian tidak terlihat sebanyak di lingkungan produksi, kami menggunakan beberapa server dari Availability Zone yang berbeda-beda di belakang tiap layanan, sama seperti apa yang ada dalam produksi. Kami menyukai penghematan di Amazon, tapi tidak saat memastikan kualitas.

Pada kesempatan lain, lingkungan pengujian memiliki beberapa server. Namun demikian, penerapan dilakukan ke semua server secara bersamaan untuk mempercepat pengujian. Pendekatan ini juga mencegah versi lawas dan baru software agar tidak beroperasi secara bersamaan. Masalah dengan gulir-maju tidak terdeteksi. Saat ini kami menggunakan konfigurasi penerapan yang sama di semua lingkungan pengujian dan produksi.

Untuk perubahan yang melibatkan koordinasi di antara layanan mikro, kami mempertahankan urutan penerapan yang sama di seluruh layanan mikro di lingkungan pengujian dan produksi. Namun, urutan untuk menggulir maju dan mudur bisa jadi berbeda. Misalnya, kami biasanya mengikuti urutan khusus dalam konteks serialisasi. Yaitu, pembaca mendahului penulis selagi menggulir maju sedangkan penulis mendahului pembaca selagi menggulir mundur. Urutan yang benar biasanya diikuti di lingkungan pengujian dan produksi.

Jika penyiapan lingkungan pengujian sama dengan lingkungan produksi, kami menyimulasikan lalu lintas produksi sedekat mungkin. Misalnya, kami membuat dan membaca beberapa catatan (atau pesan) dengan urutan cepat. Semua API digunakan secara terus-menerus. Lalu, kami mengambil lingkungan itu melalui tiga tahap, masing-masing tahap berakhir selama durasi yang wajar untuk mengidentifikasi potensi bug. Durasi cukup panjang untuk semua API, alur kerja backend, dan pekerjaan batch untuk dijalankan sedikitnya sekali.

Pertama, kami menerapkan perubahan ke sekitar setengah armada untuk memastikan koeksistensi versi software. Kedua, kami menyelesaikan penerapan. Ketiga, kami menginisiasi penerapan rollback dan mengikuti langkah yang sama hingga semua server menjalankan software lawas. Jika tidak terdapat kesalahan atau perilaku yang tak diharapkan selama tahap ini, maka kami anggap pengujian telah berhasil.

Penutup

Memastikan bahwa kami dapat menggulir balik penerapan tanpa gangguan apa pun untuk pelanggan kami adalah sangat penting dalam membuat layanan yang andal. Pengujian secara eksplisit untuk keamanan rollback mengeliminasi kebutuhan mengandalkan analisis manual, yang bisa jadi rawan salah. Saat kami menemukan bahwa sebuah perubahan tidak aman untuk menggulir balik, biasanya kami dapat membaginya menjadi dua perubahan, tiap perubahan aman untuk menggulir maju dan mundur.

Baca lebih lanjut

Untuk informasi lebih lanjut tentang cara Amazon meningkatkan keamanan dan ketersediaan layanan selagi meningkatkan kepuasan pelanggan dan produktivitas pengembang, lihat  Lebih cepat dengan pengiriman berkelanjutan


Tentang penulis

Sandeep Pokkunuri adalah Principal Engineer di AWS. Sejak bergabung dengan Amazon pada tahun 2011, beliau pernah mengerjakan beberapa layanan termasuk Amazon DynamoDB dan Amazon Simple Queue Service (SQS). Saat ini beliau berfokus pada teknologi ML yang melibatkan bahasa manusia (mis. ASR, NLP, NLU, dan Penerjemahan Mesin), dan adalah engineer kepala untuk Amazon Lex. Sebelum bergabung dengan AWS, beliau bekerja di Google pada bidang masalah-masalah ML seperti pendeteksian spam & konten merusak di media sosial dan pendeteksian anomali di catatan akses jaringan.

Lebih cepat dengan pengiriman berkelanjutan