Apa yang kita ketahui tentang layanan mikro

Halo! Nama saya Vadim Madison, saya memimpin pengembangan Platform Sistem Avito. Telah dikatakan lebih dari sekali bagaimana kami di perusahaan beralih dari arsitektur monolitik ke arsitektur layanan mikro. Saatnya berbagi bagaimana kita mentransformasikan infrastruktur untuk mendapatkan hasil maksimal dari layanan mikro dan mencegah diri kita tersesat di dalamnya. Bagaimana PaaS membantu kami di sini, bagaimana kami menyederhanakan penerapan dan mengurangi pembuatan layanan mikro menjadi satu klik - baca terus. Tidak semua yang saya tulis di bawah ini diterapkan sepenuhnya di Avito, beberapa di antaranya adalah cara kami mengembangkan platform kami.

(Dan di akhir artikel ini, saya akan berbicara tentang kesempatan menghadiri seminar tiga hari dari pakar arsitektur layanan mikro Chris Richardson).

Apa yang kita ketahui tentang layanan mikro

Bagaimana kami sampai pada layanan mikro

Avito adalah salah satu situs rahasia terbesar di dunia, lebih dari 15 juta iklan baru dipublikasikan setiap hari. Backend kami menerima lebih dari 20 ribu permintaan per detik. Saat ini kami memiliki beberapa ratus layanan mikro.

Kami telah membangun arsitektur layanan mikro selama beberapa tahun sekarang. Bagaimana tepatnya - rekan kami secara detail diberitahu di bagian kami di RIT++ 2017. Di CodeFest 2017 (lihat. Video), Sergey Orlov dan Mikhail Prokopchuk menjelaskan secara rinci mengapa kami memerlukan transisi ke layanan mikro dan apa peran Kubernetes di sini. Nah, sekarang kami melakukan segalanya untuk meminimalkan biaya penskalaan yang melekat pada arsitektur seperti itu.

Awalnya, kami tidak menciptakan ekosistem yang secara komprehensif akan membantu kami mengembangkan dan meluncurkan layanan mikro. Mereka hanya mengumpulkan solusi open source yang masuk akal, meluncurkannya di rumah dan mengundang pengembang untuk menanganinya. Akibatnya, dia pergi ke selusin tempat (dasbor, layanan internal), setelah itu dia menjadi lebih kuat dalam keinginannya untuk memotong kode dengan cara lama, dalam sebuah monolit. Warna hijau pada diagram di bawah menunjukkan apa yang dilakukan pengembang dengan tangannya sendiri, dan warna kuning menunjukkan otomatisasi.

Apa yang kita ketahui tentang layanan mikro

Sekarang di utilitas PaaS CLI, layanan baru dibuat dengan satu perintah, dan database baru ditambahkan dengan dua perintah lagi dan disebarkan ke Stage.

Apa yang kita ketahui tentang layanan mikro

Bagaimana mengatasi era "fragmentasi layanan mikro"

Dengan arsitektur monolitik, demi konsistensi perubahan produk, pengembang terpaksa mencari tahu apa yang terjadi dengan tetangganya. Saat mengerjakan arsitektur baru, konteks layanan tidak lagi bergantung satu sama lain.

Selain itu, agar arsitektur layanan mikro dapat efektif, banyak proses yang perlu ditetapkan, yaitu:

• penebangan;
• meminta penelusuran (Jaeger);
• agregasi kesalahan (Sentry);
• status, pesan, peristiwa dari Kubernetes (Pemrosesan Aliran Peristiwa);
• batas balapan/pemutus sirkuit (Anda dapat menggunakan Hystrix);
• kontrol konektivitas layanan (kami menggunakan Netramesh);
• pemantauan (Grafana);
• perakitan (TeamCity);
• komunikasi dan pemberitahuan (Slack, email);
• pelacakan tugas; (Jira)
• persiapan dokumentasi.

Untuk memastikan bahwa sistem tidak kehilangan integritasnya dan tetap efektif seiring skalanya, kami memikirkan kembali pengorganisasian layanan mikro di Avito.

Bagaimana kami mengelola layanan mikro

Bantuan berikut untuk menerapkan “kebijakan partai” terpadu di antara banyak layanan mikro Avito:

  • membagi infrastruktur menjadi beberapa lapisan;
  • Konsep Platform sebagai Layanan (PaaS);
  • memantau segala sesuatu yang terjadi dengan layanan mikro.

Lapisan abstraksi infrastruktur mencakup tiga lapisan. Mari kita beralih dari atas ke bawah.

A. Atas - jaring servis. Awalnya kami mencoba Istio, tetapi ternyata Istio menggunakan terlalu banyak sumber daya, sehingga terlalu mahal untuk volume kami. Oleh karena itu, insinyur senior di tim arsitektur Alexander Lukyanchenko mengembangkan solusinya sendiri - Netramesh (tersedia dalam Open Source), yang saat ini kami gunakan dalam produksi dan mengkonsumsi sumber daya beberapa kali lebih sedikit daripada Istio (tetapi tidak melakukan semua yang bisa dibanggakan Istio).
B.Media - Kubernetes. Kami menerapkan dan mengoperasikan layanan mikro di dalamnya.
C. Bawah - logam telanjang. Kami tidak menggunakan cloud atau hal-hal seperti OpenStack, tetapi sepenuhnya mengandalkan bare metal.

Semua lapisan digabungkan oleh PaaS. Dan platform ini, pada gilirannya, terdiri dari tiga bagian.

I. Generator, dikontrol melalui utilitas CLI. Dialah yang membantu pengembang membuat layanan mikro dengan cara yang benar dan dengan sedikit usaha.

II. Kolektor konsolidasi dengan kontrol semua alat melalui dasbor umum.

AKU AKU AKU. Penyimpanan. Terhubung dengan penjadwal yang secara otomatis menetapkan pemicu untuk tindakan signifikan. Berkat sistem seperti itu, tidak ada satu tugas pun yang terlewat hanya karena seseorang lupa menyiapkan tugas di Jira. Kami menggunakan alat internal yang disebut Atlas untuk ini.

Apa yang kita ketahui tentang layanan mikro

Implementasi layanan mikro di Avito juga dilakukan menurut skema tunggal, yang menyederhanakan kontrol atas layanan tersebut pada setiap tahap pengembangan dan rilis.

Bagaimana cara kerja jalur pengembangan layanan mikro standar?

Secara umum, rantai pembuatan layanan mikro terlihat seperti ini:

CLI-push → Integrasi Berkelanjutan → Panggang → Penerapan → Tes buatan → Tes Canary → Pengujian Squeeze → Produksi → Pemeliharaan.

Mari kita bahas persis dalam urutan ini.

CLI-dorongan

• Membuat layanan mikro.
Kami berjuang lama untuk mengajari setiap pengembang cara melakukan layanan mikro. Ini termasuk menulis instruksi rinci di Confluence. Namun skemanya berubah dan ditambah. Hasilnya adalah hambatan muncul di awal perjalanan: butuh lebih banyak waktu untuk meluncurkan layanan mikro, dan masalah masih sering muncul selama pembuatannya.

Pada akhirnya, kami membangun utilitas CLI sederhana yang mengotomatiskan langkah-langkah dasar saat membuat layanan mikro. Faktanya, ini menggantikan git push pertama. Inilah yang sebenarnya dia lakukan.

— Membuat layanan berdasarkan template — langkah demi langkah, dalam mode "wizard". Kami memiliki template untuk bahasa pemrograman utama di backend Avito: PHP, Golang dan Python.

- Satu perintah pada satu waktu menyebarkan lingkungan untuk pengembangan lokal pada mesin tertentu - Minikube diluncurkan, grafik Helm secara otomatis dibuat dan diluncurkan di kubernet lokal.

— Menghubungkan database yang diperlukan. Pengembang tidak perlu mengetahui IP, login dan kata sandi untuk mendapatkan akses ke database yang dia butuhkan - baik secara lokal, di Stage, atau dalam produksi. Selain itu, database segera diterapkan dalam konfigurasi yang toleran terhadap kesalahan dan dengan penyeimbangan.

— Ia melakukan perakitan langsung sendiri. Katakanlah seorang pengembang memperbaiki sesuatu di layanan mikro melalui IDE-nya. Utilitas melihat perubahan dalam sistem file dan, berdasarkan perubahan tersebut, membangun kembali aplikasi (untuk Golang) dan memulai ulang. Untuk PHP, kita cukup meneruskan direktori di dalam kubus dan disana live-reload diperoleh “secara otomatis”.

— Menghasilkan tes otomatis. Berupa blanko, namun cukup layak digunakan.

• Penerapan layanan mikro.

Menerapkan layanan mikro dulunya merupakan tugas yang sulit bagi kami. Hal-hal berikut ini diperlukan:

I.Dockerfile.

II. Konfigurasi.
AKU AKU AKU. Bagan helm, yang rumit dan mencakup:

— grafiknya sendiri;
— templat;
— nilai spesifik dengan mempertimbangkan lingkungan yang berbeda.

Kami telah bersusah payah mengerjakan ulang manifes Kubernetes sehingga manifes tersebut kini dihasilkan secara otomatis. Namun yang terpenting, mereka menyederhanakan penerapannya hingga batasnya. Mulai sekarang kami memiliki Dockerfile, dan pengembang menulis seluruh konfigurasi dalam satu file app.toml pendek.

Apa yang kita ketahui tentang layanan mikro

Ya, dan di app.toml sendiri tidak ada yang bisa dilakukan selama satu menit pun. Kami menentukan di mana dan berapa banyak salinan layanan yang akan dimunculkan (di server pengembang, di staging, di produksi), dan menunjukkan ketergantungannya. Perhatikan baris size = "small" di blok [engine]. Ini adalah batas yang akan dialokasikan pada layanan melalui Kubernetes.

Kemudian, berdasarkan konfigurasi, semua diagram Helm yang diperlukan dibuat secara otomatis dan koneksi ke database dibuat.

• Validasi dasar. Pemeriksaan semacam itu juga dilakukan secara otomatis.
Perlu melacak:
— apakah ada Dockerfile;
— apakah ada app.toml;
— apakah ada dokumentasi yang tersedia?
— apakah ketergantungannya sudah beres?
— apakah aturan peringatan telah ditetapkan.
Sampai poin terakhir: pemilik layanan sendiri yang menentukan metrik produk mana yang akan dipantau.

• Persiapan dokumentasi.
Masih menjadi area masalah. Tampaknya ini merupakan hal yang paling jelas, namun pada saat yang sama juga merupakan rekor yang “sering dilupakan”, dan oleh karena itu merupakan mata rantai yang rentan dalam rantai tersebut.
Perlu adanya dokumentasi untuk setiap layanan mikro. Ini mencakup blok-blok berikut.

I. Deskripsi singkat tentang layanan. Secara harfiah beberapa kalimat tentang apa yang dilakukannya dan mengapa itu diperlukan.

II. Tautan diagram arsitektur. Penting agar sekilas mudah dipahami, misalnya, apakah Anda menggunakan Redis untuk caching atau sebagai penyimpan data utama dalam mode persisten. Di Avito untuk saat ini, ini adalah tautan ke Confluence.

AKU AKU AKU. Buku Run. Panduan singkat memulai layanan dan seluk-beluk penanganannya.

IV. Pertanyaan Umum, ada baiknya untuk mengantisipasi masalah yang mungkin dihadapi rekan Anda saat bekerja dengan layanan tersebut.

V. Deskripsi titik akhir untuk API. Jika tiba-tiba Anda tidak menentukan tujuannya, kolega yang layanan mikronya terkait dengan Anda hampir pasti akan membayarnya. Sekarang kami menggunakan Swagger dan solusi kami disebut singkat untuk ini.

VI. Label. Atau penanda yang menunjukkan produk, fungsi, atau divisi struktural perusahaan mana yang memiliki layanan tersebut. Mereka membantu Anda dengan cepat memahami, misalnya, apakah Anda mengurangi fungsionalitas yang diluncurkan kolega Anda untuk unit bisnis yang sama seminggu yang lalu.

VII. Pemilik atau pemilik layanan. Dalam kebanyakan kasus, hal tersebut — atau hal tersebut — dapat ditentukan secara otomatis menggunakan PaaS, namun demi amannya, kami mengharuskan pengembang untuk menentukannya secara manual.

Terakhir, merupakan praktik yang baik untuk meninjau dokumentasi, mirip dengan tinjauan kode.

Integrasi berkelanjutan

  • Mempersiapkan repositori.
  • Membuat saluran pipa di TeamCity.
  • Menetapkan hak.
  • Cari pemilik layanan. Ada skema hybrid di sini - penandaan manual dan otomatisasi minimal dari PaaS. Skema yang sepenuhnya otomatis gagal ketika layanan ditransfer untuk dukungan ke tim pengembangan lain atau, misalnya, jika pengembang layanan berhenti.
  • Mendaftarkan layanan di Atlas (Lihat di atas). Dengan semua pemilik dan ketergantungannya.
  • Memeriksa migrasi. Kami memeriksa apakah ada yang berpotensi berbahaya. Misalnya, di salah satunya muncul tabel perubahan atau sesuatu yang lain yang dapat merusak kompatibilitas skema data antara versi layanan yang berbeda. Kemudian migrasi tidak dilakukan, tetapi ditempatkan dalam langganan - PaaS harus memberi sinyal kepada pemilik layanan kapan sudah aman untuk menggunakannya.

Membakar

Tahap selanjutnya adalah pengemasan layanan sebelum penerapan.

  • Membangun aplikasi. Menurut klasik - dalam gambar Docker.
  • Pembuatan diagram Helm untuk layanan itu sendiri dan sumber daya terkait. Termasuk untuk database dan cache. Mereka dibuat secara otomatis sesuai dengan konfigurasi app.toml yang dihasilkan pada tahap CLI-push.
  • Membuat tiket bagi admin untuk membuka port (bila diperlukan).
  • Menjalankan pengujian unit dan menghitung cakupan kode. Jika cakupan kode berada di bawah ambang batas yang ditentukan, kemungkinan besar layanan tidak akan melangkah lebih jauh - ke penerapan. Jika berada di ambang dapat diterima, maka layanan akan diberi koefisien “pesimis”: kemudian, jika tidak ada peningkatan pada indikator dari waktu ke waktu, pengembang akan menerima pemberitahuan bahwa tidak ada kemajuan dalam hal pengujian ( dan sesuatu perlu dilakukan mengenai hal itu).
  • Mempertimbangkan keterbatasan memori dan CPU. Kami terutama menulis layanan mikro di Golang dan menjalankannya di Kubernetes. Oleh karena itu satu kehalusan yang terkait dengan kekhasan bahasa Golang: secara default, ketika memulai, semua inti pada mesin digunakan, jika Anda tidak secara eksplisit mengatur variabel GOMAXPROCS, dan ketika beberapa layanan tersebut diluncurkan pada mesin yang sama, mereka mulai untuk bersaing memperebutkan sumber daya, saling mengganggu. Grafik di bawah menunjukkan bagaimana waktu eksekusi berubah jika Anda menjalankan aplikasi tanpa perselisihan dan dalam mode perlombaan untuk sumber daya. (Sumber grafiknya adalah di sini).

Apa yang kita ketahui tentang layanan mikro

Waktu eksekusi, lebih sedikit lebih baik. Maksimum: 643 md, minimum: 42 md. Foto dapat diklik.

Apa yang kita ketahui tentang layanan mikro

Saatnya untuk operasi, lebih sedikit lebih baik. Maksimum: 14091 ns, minimum: 151 ns. Foto dapat diklik.

Pada tahap persiapan perakitan, Anda bisa mengatur variabel ini secara eksplisit atau Anda bisa menggunakan perpustakaan automaxprocs dari orang-orang dari Uber.

Menyebarkan

• Memeriksa konvensi. Sebelum Anda mulai mengirimkan rakitan layanan ke lingkungan yang Anda inginkan, Anda perlu memeriksa hal berikut:
- Titik akhir API.
— Kepatuhan respons titik akhir API dengan skema.
— Format catatan.
— Mengatur header untuk permintaan ke layanan (saat ini dilakukan oleh netramesh)
— Mengatur token pemilik saat mengirim pesan ke bus acara. Ini diperlukan untuk melacak konektivitas layanan di seluruh bus. Anda dapat mengirim data idempoten ke bus, yang tidak meningkatkan konektivitas layanan (yang bagus), dan data bisnis yang memperkuat konektivitas layanan (yang sangat buruk!). Dan ketika konektivitas ini menjadi masalah, pemahaman siapa yang menulis dan membaca bus membantu memisahkan layanan dengan benar.

Belum banyak konvensi di Avito, namun jumlah konvensinya terus bertambah. Semakin banyak perjanjian tersebut tersedia dalam bentuk yang dapat dipahami dan dipahami oleh tim, semakin mudah menjaga konsistensi antar layanan mikro.

Tes sintetis

• Pengujian loop tertutup. Untuk ini kami sekarang menggunakan open source Hoverfly.io. Pertama, ia mencatat beban sebenarnya pada layanan, kemudian - hanya dalam loop tertutup - ia mengemulasinya.

• Tes Stres. Kami berusaha membawa semua layanan ke kinerja optimal. Dan semua versi dari setiap layanan harus menjalani pengujian beban - dengan cara ini kita dapat memahami kinerja layanan saat ini dan perbedaannya dengan versi sebelumnya dari layanan yang sama. Jika, setelah pembaruan layanan, kinerjanya turun satu setengah kali lipat, ini adalah sinyal yang jelas bagi pemiliknya: Anda perlu menggali kode dan memperbaiki situasinya.
Kami menggunakan data yang dikumpulkan, misalnya, untuk menerapkan penskalaan otomatis dengan benar dan, pada akhirnya, secara umum memahami seberapa skalabel layanan tersebut.

Selama pengujian beban, kami memeriksa apakah konsumsi sumber daya memenuhi batas yang ditetapkan. Dan kami fokus terutama pada hal-hal ekstrem.

a) Kami melihat total beban.
- Terlalu kecil - kemungkinan besar ada yang tidak berfungsi sama sekali jika beban tiba-tiba turun beberapa kali.
- Terlalu besar - diperlukan optimasi.

b) Kita lihat cutoffnya menurut RPS.
Di sini kita melihat perbedaan antara versi saat ini dan versi sebelumnya serta jumlah totalnya. Misalnya, jika suatu layanan menghasilkan 100 rps, maka layanan tersebut ditulis dengan buruk, atau ini adalah kekhususannya, tetapi bagaimanapun juga, ini adalah alasan untuk melihat layanan tersebut dengan cermat.
Sebaliknya, jika ada terlalu banyak RPS, mungkin ada semacam bug dan beberapa titik akhir berhenti mengeksekusi payload, dan titik akhir lainnya terpicu. return true;

Tes kenari

Setelah kami lulus pengujian sintetik, kami menguji layanan mikro pada sejumlah kecil pengguna. Kami memulai dengan hati-hati, dengan pangsa kecil dari audiens yang dituju layanan ini - kurang dari 0,1%. Pada tahap ini, sangat penting untuk menyertakan metrik teknis dan produk yang benar dalam pemantauan sehingga dapat menunjukkan masalah dalam layanan secepat mungkin. Waktu minimum tes kenari adalah 5 menit, yang utama adalah 2 jam. Untuk layanan yang kompleks, kami mengatur waktu secara manual.
Kami menganalisis:
— metrik khusus bahasa, khususnya, pekerja php-fpm;
— kesalahan di Penjaga;
— status respons;
— waktu respons, tepat dan rata-rata;
— latensi;
— pengecualian, diproses dan tidak ditangani;
— metrik produk.

Pengujian Peras

Pengujian Pemerasan juga disebut pengujian “pemerasan”. Nama teknik ini diperkenalkan ke Netflix. Esensinya adalah pertama-tama kita mengisi satu instance dengan lalu lintas nyata hingga titik kegagalan dan kemudian menetapkan batasnya. Kemudian kita menambahkan instance lain dan memuat pasangan ini - lagi secara maksimal; kita melihat langit-langit dan delta mereka dengan “perasan” pertama. Jadi kami menghubungkan satu contoh pada satu waktu dan menghitung pola perubahannya.
Data pengujian melalui "pemerasan" juga mengalir ke database metrik umum, tempat kami memperkaya hasil beban buatan dengan data tersebut, atau bahkan mengganti "sintetis" dengan data tersebut.

Produksi

• Penskalaan. Saat kami meluncurkan layanan ke produksi, kami memantau skalanya. Berdasarkan pengalaman kami, memantau indikator CPU saja tidak efektif. Penskalaan otomatis dengan tolok ukur RPS dalam bentuknya yang murni berfungsi, namun hanya untuk layanan tertentu, seperti streaming online. Jadi pertama-tama kita melihat metrik produk khusus aplikasi.

Hasilnya, saat melakukan penskalaan, kami menganalisis:
- Indikator CPU dan RAM,
— jumlah permintaan dalam antrian,
- waktu merespon,
— perkiraan berdasarkan akumulasi data historis.

Saat menskalakan suatu layanan, penting juga untuk memantau ketergantungannya sehingga kita tidak menskalakan layanan pertama dalam rantai tersebut, dan layanan yang diaksesnya gagal saat dimuat. Untuk menetapkan beban yang dapat diterima untuk seluruh kumpulan layanan, kami melihat data historis dari layanan dependen “terdekat” (berdasarkan kombinasi indikator CPU dan RAM, ditambah dengan metrik khusus aplikasi) dan membandingkannya dengan data historis. layanan inisialisasi, dan seterusnya di seluruh “rantai ketergantungan” ", dari atas ke bawah.

Layanan

Setelah layanan mikro dioperasikan, kita dapat melampirkan pemicu ke dalamnya.

Berikut adalah situasi umum di mana pemicu terjadi.
— Migrasi yang berpotensi berbahaya terdeteksi.
— Pembaruan keamanan telah dirilis.
— Layanannya sendiri sudah lama tidak diperbarui.
— Beban pada layanan telah menurun secara nyata atau beberapa metrik produknya berada di luar kisaran normal.
— Layanan ini tidak lagi memenuhi persyaratan platform baru.

Beberapa pemicu bertanggung jawab atas stabilitas operasi, beberapa - sebagai fungsi pemeliharaan sistem - misalnya, beberapa layanan sudah lama tidak digunakan dan gambar dasarnya tidak lagi lolos pemeriksaan keamanan.

Dasbor

Singkatnya, dashboard adalah panel kontrol seluruh PaaS kami.

  • Satu titik informasi tentang layanan, dengan data tentang cakupan pengujian, jumlah gambar, jumlah salinan produksi, versi, dll.
  • Alat untuk memfilter data berdasarkan layanan dan label (penanda milik unit bisnis, fungsionalitas produk, dll.)
  • Alat untuk integrasi dengan alat infrastruktur untuk penelusuran, pencatatan, dan pemantauan.
  • Satu titik dokumentasi layanan.
  • Satu sudut pandang dari semua peristiwa di seluruh layanan.

Apa yang kita ketahui tentang layanan mikro
Apa yang kita ketahui tentang layanan mikro
Apa yang kita ketahui tentang layanan mikro
Apa yang kita ketahui tentang layanan mikro

Total

Sebelum memperkenalkan PaaS, pengembang baru dapat menghabiskan waktu beberapa minggu untuk memahami semua alat yang diperlukan untuk meluncurkan layanan mikro dalam produksi: Kubernetes, Helm, fitur internal TeamCity kami, menyiapkan koneksi ke database dan cache dengan cara yang toleran terhadap kesalahan, dll. memerlukan waktu beberapa jam untuk membaca mulai cepat dan membuat layanan itu sendiri.

Saya memberikan laporan tentang topik ini untuk HighLoad++ 2018, Anda dapat menontonnya Video и presentasi dari.

Bonus lagu bagi yang membaca sampai akhir

Kami di Avito menyelenggarakan pelatihan internal selama tiga hari untuk pengembang Chris Richardson, seorang ahli dalam arsitektur layanan mikro. Kami ingin memberikan kesempatan untuk berpartisipasi di dalamnya kepada salah satu pembaca postingan ini. Di sini Program pelatihan telah diposting.

Pelatihan akan berlangsung dari tanggal 5 hingga 7 Agustus di Moskow. Ini adalah hari kerja yang akan terisi penuh. Makan siang dan pelatihan akan diadakan di kantor kami, dan peserta terpilih akan membayar sendiri biaya perjalanan dan akomodasinya.

Anda dapat mengajukan permohonan untuk berpartisipasi dalam formulir Google ini. Dari Anda - jawaban atas pertanyaan mengapa Anda perlu mengikuti pelatihan dan informasi tentang cara menghubungi Anda. Jawab dalam bahasa Inggris, karena Chris akan memilih sendiri peserta yang akan mengikuti pelatihan tersebut.
Kami akan mengumumkan nama peserta pelatihan dalam pembaruan postingan ini dan di jejaring sosial Avito untuk pengembang (AvitoTech in Фейсбуке, VKontakte, Twitter) paling lambat tanggal 19 Juli.

Sumber: www.habr.com

Tambah komentar