Mendesain API untuk layanan mikro

Azure DevOps

Desain API yang baik penting dalam arsitektur layanan mikro, karena semua pertukaran data antar layanan terjadi baik melalui pesan atau panggilan API. API harus efisien untuk menghindari pembuatan I/O chatty. Karena layanan dirancang oleh tim yang bekerja secara independen, API harus memiliki semantik dan skema versi yang terdefinisi dengan baik, sehingga pembaruan tidak merusak layanan lain.

Desain API untuk layanan mikro

Penting untuk membedakan antara dua jenis API:

  • API publik yang disebut aplikasi klien.
  • API backend yang digunakan untuk komunikasi antar layanan.

Kedua kasus penggunaan ini memiliki persyaratan yang agak berbeda. API publik harus kompatibel dengan aplikasi klien, biasanya aplikasi browser atau aplikasi seluler asli. Sebagian besar waktu, itu berarti API publik akan menggunakan REST melalui HTTP. Namun, untuk API backend, Anda perlu mempertimbangkan kinerja jaringan. Tergantung pada granularitas layanan Anda, komunikasi antar layanan dapat menghasilkan banyak lalu lintas jaringan. Layanan dapat dengan cepat menjadi terikat I / O. Oleh karena itu, pertimbangan seperti kecepatan serialisasi dan ukuran muatan menjadi lebih penting. Beberapa alternatif populer untuk menggunakan REST over HTTP termasuk gRPC, Apache Avro, dan Apache Thrift. Protokol ini mendukung serialisasi biner dan umumnya lebih efisien daripada HTTP.

Pertimbangan

Berikut adalah beberapa hal yang perlu dipikirkan ketika memilih cara menerapkan API.

REST versus RPC. Pertimbangkan tradeoff antara menggunakan antarmuka bergaya REST versus antarmuka gaya RPC.

  • SUMBER DAYA model REST, yang dapat menjadi cara alami untuk mengekspresikan model domain Anda. Ini mendefinisikan antarmuka yang seragam berdasarkan kata kerja HTTP, yang mendorong evolvability. Semantik yang didefinisikan dengan baik dalam hal idempotensi, efek samping, dan kode respon. Dan itu memaksa komunikasi tanpa kewarganegaraan, yang meningkatkan skalabilitas

  • RPC lebih berorientasi pada operasi atau perintah. Karena antarmuka RPC terlihat seperti panggilan metode lokal, itu dapat membawa Anda ke desain API yang terlalu mengganggu. Namun, bukan berarti RPC harus mengganggu. Itu hanya berarti Anda perlu menggunakan perawatan saat merancang antarmuka.

Untuk antarmuka RESTful, pilihan yang paling umum adalah REST over HTTP menggunakan JSON. Untuk antarmuka bergaya RPC, ada beberapa kerangka kerja populer, termasuk gRPC, Apache Avro, dan Apache Thrift.

Efisiensi Pertimbangkan efisiensi dalam hal kecepatan, memori, dan ukuran muatan. Biasanya antarmuka berbasis gRPC lebih cepat daripada REST melalui HTTP.

Bahasa definisi antarmuka (IDL) IDL digunakan untuk menentukan metode, parameter, dan nilai pengembalian API. IDL dapat digunakan untuk menghasilkan kode klien, kode serialisasi, dan dokumentasi API. IDLs juga dapat dikonsumsi oleh alat pengujian API seperti Postman. Kerangka kerja seperti gRPC, Avro, dan Thrift menentukan spesifikasi IDL mereka sendiri. REST melalui HTTP tidak memiliki format IDL standar, tetapi pilihan umum adalah OpenAPI (sebelumnya Swagger). Anda juga dapat membuat API HTTP REST tanpa menggunakan bahasa definisi formal, tetapi kemudian Anda kehilangan manfaat pembuatan dan pengujian kode.

Serialisasi. Bagaimana objek diserialkan melalui kawat? Pilihan termasuk format berbasis teks (terutama JSON) dan format biner seperti buffer protokol. Format biner umumnya lebih cepat daripada format berbasis teks. Namun, JSON memiliki keunggulan dalam hal interoperabilitas, karena sebagian besar bahasa dan kerangka kerja mendukung serialisasi JSON. Beberapa format serialisasi memerlukan skema tetap, dan beberapa memerlukan kompilasi file definisi skema. Dalam hal ini, Anda harus memasukkan langkah ini ke dalam proses build Anda.

Kerangka kerja dan dukungan bahasa. HTTP didukung di hampir setiap kerangka kerja dan bahasa. gRPC, Avro, dan Thrift semuanya memiliki perpustakaan untuk C ++, C #, Java, dan Python. Thrift dan gRPC juga mendukung Go.

Kompatibilitas dan interoperabilitas. Jika Anda memilih protokol seperti gRPC, Anda mungkin memerlukan lapisan terjemahan protokol antara API publik dan back end. gateway dapat melakukan fungsi tersebut. Jika Anda menggunakan layanan mesh, pertimbangkan protokol mana yang kompatibel dengan layanan mesh. Misalnya, Linkerd memiliki dukungan bawaan untuk HTTP, Thrift, dan gRPC.

Rekomendasi dasar kami adalah memilih REST daripada HTTP kecuali Anda memerlukan manfaat kinerja dari protokol biner. REST melalui HTTP tidak memerlukan pustaka khusus. Ini menciptakan kopling minimal, karena penelepon tidak memerlukan rintisan klien untuk berkomunikasi dengan layanan. Terdapat ekosistem yang kaya dari alat-alat untuk mendukung definisi skema, pengujian, dan pemantauan titik akhir HTTP yang lengkap. Akhirnya, HTTP kompatibel dengan klien browser, jadi Anda tidak memerlukan lapisan terjemahan protokol antara klien dan backend.

Namun, jika Anda memilih REST over HTTP, Anda harus melakukan pengujian kinerja dan beban di awal proses pengembangan, untuk memvalidasi apakah kinerjanya cukup baik untuk skenario Anda.

Desain API RESTful

Terdapat banyak sumber daya untuk merancang API RESTful. Berikut adalah beberapa yang mungkin Bermanfaat bagi Anda:

Berikut adalah beberapa pertimbangan khusus yang perlu diingat.

  • Hati-hati dengan API yang membocorkan detail implementasi internal atau hanya mencerminkan skema database internal. API harus memodelkan domain. Ini adalah kontrak antar layanan, dan idealnya hanya harus berubah ketika fungsi baru ditambahkan, bukan hanya karena Anda refactored beberapa kode atau menormalkan tabel database.

  • Berbagai jenis klien, seperti aplikasi seluler dan browser web desktop, mungkin memerlukan ukuran muatan atau pola interaksi yang berbeda. Pertimbangkan untuk menggunakan pola Backends for Frontends untuk membuat backend terpisah untuk setiap klien, yang mengekspos antarmuka optimal untuk klien tersebut.

  • Untuk operasi dengan efek samping, pertimbangkan untuk menjadikannya idempotent dan menerapkannya sebagai metode PUT. Itu akan memungkinkan persidangan ulang yang aman dan dapat meningkatkan ketahanan. Artikel Komunikasi antar layanan membahas masalah ini secara lebih rinci.

  • Metode HTTP dapat memiliki semantik asinkron, di mana metode mengembalikan respons segera, tetapi layanan melakukan operasi secara asinkron. Dalam hal ini, metode harus mengembalikan kode respons HTTP 202, yang menunjukkan permintaan diterima untuk diproses, tetapi pemrosesan belum selesai. Untuk informasi selengkapnya, lihat Pola Permintaan-Balasan Asinkron.

Memetakan pola REST ke DDD

Pola seperti entitas, agregat, dan objek nilai dirancang untuk menempatkan batasan tertentu pada objek dalam model domain Anda. Dalam banyak diskusi tentang DDD, pola dimodelkan menggunakan konsep bahasa berorientasi objek (OO) seperti konstruktor atau pengambil properti dan setter. Misalnya, objek nilai seharusnya tidak berubah. Dalam bahasa pemrograman OO, Anda akan menegakkan ini dengan menetapkan nilai-nilai di konstruktor dan membuat properti hanya-baca:

export class Location {
    readonly latitude: number;
    readonly longitude: number;

    constructor(latitude: number, longitude: number) {
        if (latitude < -90 || latitude > 90) {
            throw new RangeError('latitude must be between -90 and 90');
        }
        if (longitude < -180 || longitude > 180) {
            throw new RangeError('longitude must be between -180 and 180');
        }
        this.latitude = latitude;
        this.longitude = longitude;
    }
}

Praktik pengkodean semacam ini sangat penting ketika membangun aplikasi monolitik tradisional. Dengan basis kode yang besar, banyak subsistem mungkin menggunakan Location objek, jadi penting bagi objek untuk menegakkan perilaku yang benar.

Contoh lainnya adalah pola Repositori, yang memastikan bahwa bagian lain dari aplikasi tidak membuat pembacaan langsung atau menulis ke penyimpanan data:

Diagram repositori Drone.

Namun, dalam arsitektur layanan mikro, layanan tidak memiliki basis kode yang sama dan tidak berbagi penyimpanan data. Sebaliknya, mereka berkomunikasi melalui API. Pertimbangkan kasus di mana layanan Penjadwal meminta informasi tentang drone dari layanan Drone. Layanan Drone memiliki model internal drone, yang diekspresikan melalui kode. Tapi Penjadwal tidak melihat itu. Sebaliknya, ia mendapatkan kembali representasi dari entitas drone - mungkin objek JSON dalam respons HTTP.

Contoh ini sangat ideal untuk industri pesawat terbang dan kedirgantaraan.

Diagram layanan Drone.

Layanan Penjadwal tidak dapat memodifikasi model internal layanan Drone, atau menulis ke penyimpanan data layanan Drone. Artinya kode yang mengimplementasikan layanan Drone memiliki luas permukaan yang lebih kecil, dibandingkan dengan kode dalam monolit tradisional. Jika layanan Drone mendefinisikan kelas Lokasi, ruang lingkup kelas itu terbatas - tidak ada layanan lain yang akan langsung mengkonsumsi kelas.

Untuk alasan ini, panduan ini tidak terlalu fokus pada praktik pengkodean karena berkaitan dengan pola DDD taktis. Tetapi ternyata Anda juga dapat memodelkan banyak pola DDD melalui REST API.

Contohnya:

  • Agregat memetakan secara alami ke sumber daya di REST. Misalnya, agregat Pengiriman akan diekspos sebagai sumber daya oleh API Pengiriman.

  • Agregat adalah batas konsistensi. Operasi agregat seharusnya tidak pernah meninggalkan agregat dalam keadaan tidak konsisten. Oleh karena itu, Anda harus menghindari pembuatan API yang memungkinkan klien memanipulasi keadaan internal agregat. Sebaliknya, mendukung API berbutir kasar yang mengekspos agregat sebagai sumber daya.

  • Entitas memiliki identitas yang unik. Di REST, sumber daya memiliki pengidentifikasi unik dalam bentuk URL. Buat URL sumber daya yang sesuai dengan identitas domain entitas. Pemetaan dari URL ke identitas domain mungkin buram bagi klien.

  • Entitas anak dari agregat dapat dicapai dengan menavigasi dari entitas root. Jika Anda mengikuti prinsip HATEOAS, entitas anak dapat dihubungi melalui tautan dalam representasi entitas induk.

  • Karena objek nilai tidak berubah, pembaruan dilakukan dengan mengganti seluruh objek nilai. Di REST, terapkan pembaruan melalui permintaan PUT atau PATCH.

  • Repositori memungkinkan klien meminta, menambahkan, atau menghapus objek dalam koleksi, mengabstraksi detail penyimpanan data yang mendasarinya. Di REST, koleksi dapat menjadi sumber daya yang berbeda, dengan metode untuk query koleksi atau menambahkan entitas baru ke koleksi.

Saat Anda mendesain API Anda, pikirkan bagaimana mereka mengekspresikan model domain, bukan hanya data di dalam model, tetapi juga operasi bisnis dan batasan pada data.

Konsep DDD Setara REST Contoh
Agregat Sumber daya { "1":1234, "status":"pending"... }
Identitas URL https://delivery-service/deliveries/1
Entitas anak Tautan { "href": "/deliveries/1/confirmation" }
Memperbarui objek nilai PUT atau PATCH PUT https://delivery-service/deliveries/1/dropoff
Repositori Koleksi https://delivery-service/deliveries?status=pending

Penerapan versi API

API adalah kontrak antara layanan dan klien atau konsumen layanan tersebut. Jika API berubah, ada risiko melanggar klien yang bergantung pada API, apakah itu klien eksternal atau layanan mikro lainnya. Oleh karena itu, ada baiknya untuk meminimalkan jumlah perubahan API yang Anda buat. Seringkali, perubahan dalam implementasi yang mendasarinya tidak memerlukan perubahan pada API. Namun, secara realistis, pada titik tertentu Anda ingin menambahkan fitur baru atau kemampuan baru yang memerlukan perubahan API yang ada.

Jika memungkinkan, buat perubahan API kompatibel ke belakang. Misalnya, hindari menghapus bidang dari model, karena itu dapat menghancurkan klien yang mengharapkan bidang berada di sana. Menambahkan bidang tidak merusak kompatibilitas, karena klien harus mengabaikan bidang apa pun yang tidak mereka pahami dalam respons. Namun, layanan harus menangani kasus di mana klien yang lebih tua menghilangkan bidang baru dalam permintaan.

Mendukung pembuatan versi dalam kontrak API Anda. Jika Anda memperkenalkan perubahan API yang melanggar, perkenalkan versi API baru. Terus mendukung versi sebelumnya, dan biarkan klien memilih versi mana yang akan dihubungi. Ada beberapa cara untuk melakukan ini. Salah satunya adalah hanya untuk mengekspos kedua versi dalam layanan yang sama. Pilihan lain adalah menjalankan dua versi layanan berdampingan, dan mengarahkan permintaan ke satu atau versi lain, berdasarkan aturan perutean HTTP.

Diagram memperlihatkan dua opsi untuk mendukung pembuatan versi.

Diagram memiliki dua bagian. "Layanan mendukung dua versi" menunjukkan Klien v1 dan Klien v2 keduanya menunjuk ke satu Layanan. "Penyebaran berdampingan" menunjukkan Klien v1 menunjuk ke Layanan v1, dan Klien v2 menunjuk ke Layanan v2.

Ada biaya untuk mendukung beberapa versi, dalam hal waktu pengembang, pengujian, dan overhead operasional. Oleh karena itu, ada baiknya untuk mendepresiasi versi lama secepat mungkin. Untuk API internal, tim yang memiliki API dapat bekerja dengan tim lain untuk membantu mereka bermigrasi ke versi baru. Ini adalah ketika memiliki proses tata kelola lintas tim berguna. Untuk API eksternal (publik), mungkin lebih sulit untuk mendepresiasi versi API, terutama jika API dikonsumsi oleh pihak ketiga atau oleh aplikasi klien asli.

Ketika implementasi layanan berubah, berguna untuk menandai perubahan dengan versi. Versi ini memberikan informasi penting saat memecahkan masalah kesalahan. Hal ini dapat sangat membantu untuk analisis akar penyebab untuk mengetahui dengan tepat versi layanan yang disebut. Pertimbangkan untuk menggunakan versi semantik untuk versi layanan. Versi semantik menggunakan MAJOR. KECIL. Format PATCH . Namun, klien hanya boleh memilih API dengan nomor versi utama, atau mungkin versi minor jika ada perubahan signifikan (tetapi tidak melanggar) antara versi minor. Dengan kata lain, masuk akal bagi klien untuk memilih antara versi 1 dan versi 2 dari API, tetapi tidak untuk memilih versi 2.1.3. Jika Anda mengizinkan tingkat granularitas itu, Anda berisiko harus mendukung proliferasi versi.

Untuk diskusi lebih lanjut tentang versi API, lihat Versi API web RESTful.

Merancang operasi idempoten

Operasi dikatakan idempotent jika operasi tersebut dapat panggil beberapa kali tanpa menghasilkan efek samping tambahan setelah panggilan pertama. Idempotency dapat menjadi strategi ketahanan yang berguna, karena memungkinkan layanan hulu untuk memanggil operasi dengan aman beberapa kali. Untuk diskusi tentang hal ini, lihat Transaksi terdistribusi.

Spesifikasi HTTP menyatakan bahwa metode GET, PUT, dan DELETE harus idempotent. Metode POST tidak dijamin untuk menjadi idempotent. Jika metode POST menciptakan sumber daya baru, umumnya tidak ada jaminan bahwa operasi ini adalah idempotent. Spesifikasi mendefinisikan idempotent dengan cara ini:

Metode permintaan dianggap "idempotent" jika efek yang dimaksudkan pada server beberapa permintaan identik dengan metode itu sama dengan efek untuk satu permintaan tersebut. (RFC 7231)

Penting untuk memahami perbedaan antara semantik PUT dan POST saat membuat entitas baru. Dalam kedua kasus tersebut, klien mengirimkan representasi entitas dalam badan permintaan. Tetapi arti dari URI berbeda.

  • Untuk metode POST, URI mewakili sumber daya induk entitas baru, seperti koleksi. Misalnya, untuk membuat pengiriman baru, URI mungkin /api/deliveries. Server membuat entitas dan menetapkan URI baru, seperti /api/deliveries/39660. URI ini dikembalikan di header Lokasi respons. Setiap kali klien mengirimkan permintaan, server akan membuat entitas baru dengan URI baru.

  • Untuk metode PUT, URI mengidentifikasi entitas. Jika sudah ada entitas dengan URI tersebut, server akan mengganti entitas yang ada dengan versi dalam permintaan. Jika tidak ada entitas dengan URI itu, server akan membuatnya. Misalnya, misalkan klien mengirim permintaan PUT ke api/deliveries/39660. Dengan asumsi tidak ada pengiriman dengan URI itu, server membuat yang baru. Sekarang jika klien mengirim permintaan yang sama lagi, server akan menggantikan entitas yang ada.

Berikut adalah implementasi layanan Pengiriman dari metode PUT.

[HttpPut("{id}")]
[ProducesResponseType(typeof(Delivery), 201)]
[ProducesResponseType(typeof(void), 204)]
public async Task<IActionResult> Put([FromBody]Delivery delivery, string id)
{
    logger.LogInformation("In Put action with delivery {Id}: {@DeliveryInfo}", id, delivery.ToLogInfo());
    try
    {
        var internalDelivery = delivery.ToInternal();

        // Create the new delivery entity.
        await deliveryRepository.CreateAsync(internalDelivery);

        // Create a delivery status event.
        var deliveryStatusEvent = new DeliveryStatusEvent { DeliveryId = delivery.Id, Stage = DeliveryEventType.Created };
        await deliveryStatusEventRepository.AddAsync(deliveryStatusEvent);

        // Return HTTP 201 (Created)
        return CreatedAtRoute("GetDelivery", new { id= delivery.Id }, delivery);
    }
    catch (DuplicateResourceException)
    {
        // This method is mainly used to create deliveries. If the delivery already exists then update it.
        logger.LogInformation("Updating resource with delivery id: {DeliveryId}", id);

        var internalDelivery = delivery.ToInternal();
        await deliveryRepository.UpdateAsync(id, internalDelivery);

        // Return HTTP 204 (No Content)
        return NoContent();
    }
}

Diharapkan bahwa sebagian besar permintaan akan membuat entitas baru, sehingga metode optimis memanggil CreateAsync objek repositori, dan kemudian menangani pengecualian duplikat-sumber daya dengan memperbarui sumber daya sebagai gantinya.

Langkah berikutnya

Pelajari cara menggunakan gateway API pada batas antara aplikasi klien dan layanan mikro.