Halo Nama Panggilan! Jika Anda seorang programmer dan bekerja dengan arsitektur microservice, maka bayangkan Anda perlu mengkonfigurasi interaksi layanan A Anda dengan beberapa layanan baru dan masih belum dikenal B. Apa yang akan Anda lakukan pertama kali?
Jika Anda mengajukan pertanyaan ini kepada 100 pemrogram dari perusahaan yang berbeda, kemungkinan besar kami akan mendapatkan 100 jawaban yang berbeda. Seseorang menjelaskan kontrak dengan sombong, seseorang di gRPC hanya membuat klien untuk layanan mereka tanpa menjelaskan kontrak. Dan seseorang bahkan menyimpan JSON di googleok: D. Sebagian besar perusahaan mengembangkan pendekatan mereka sendiri untuk interaksi antar-layanan berdasarkan beberapa faktor historis, kompetensi, tumpukan teknologi, dan sebagainya. Saya ingin memberi tahu Anda bagaimana layanan di Delivery Club berkomunikasi satu sama lain dan mengapa kami membuat pilihan seperti itu. Dan yang terpenting, cara kami memastikan relevansi dokumentasi dari waktu ke waktu. Akan ada banyak kode!
Halo lagi! Nama saya Sergey Popov, saya adalah ketua tim dari tim yang bertanggung jawab atas hasil pencarian restoran di aplikasi dan situs web Delivery Club, dan juga anggota aktif dari serikat pengembangan internal kami di Go (kita mungkin membicarakannya nanti, tetapi tidak sekarang).
Saya akan segera membuat reservasi, kami terutama akan berbicara tentang layanan yang tertulis di Go. Kami belum mengimplementasikan pembuatan kode untuk layanan PHP, meskipun kami mencapai keseragaman dalam pendekatan di sana dengan cara yang berbeda.
Apa yang ingin kami akhiri:
- Pastikan kontrak layanan adalah yang terbaru. Ini harus mempercepat pengenalan layanan baru dan memfasilitasi komunikasi antar tim.
- Datang ke metode interaksi terpadu melalui HTTP antar layanan (kami tidak akan mempertimbangkan interaksi melalui antrian dan streaming acara untuk saat ini).
- Untuk membakukan pendekatan untuk bekerja dengan kontrak layanan.
- Gunakan satu repositori kontrak agar tidak mencari dok untuk semua jenis pertemuan.
- Idealnya, buat klien untuk berbagai platform.
Dari semua hal di atas, Protobuf muncul di benak sebagai cara terpadu untuk menggambarkan kontrak. Ini memiliki alat yang bagus dan dapat menghasilkan klien untuk platform yang berbeda (klausul 5 kami). Namun ada juga kelemahan yang jelas: bagi banyak orang, gRPC tetap merupakan sesuatu yang baru dan tidak diketahui, dan ini akan sangat mempersulit penerapannya. Faktor penting lainnya adalah bahwa perusahaan telah lama mengadopsi pendekatan "spesifikasi pertama", dan dokumentasi sudah ada untuk semua layanan dalam bentuk deskripsi sombong atau RAML.
Sombong
Secara kebetulan, pada saat yang sama, kami mulai mengadaptasi Go di perusahaan. Oleh karena itu, kandidat pertimbangan kami berikutnya adalah go-swagger - alat yang memungkinkan Anda menghasilkan kode klien dan server dari spesifikasi yang tinggi. Kerugian yang jelas adalah hanya menghasilkan kode untuk Go. Faktanya, ini menggunakan pembuatan kode gosh, dan go-swagger memungkinkan kerja yang fleksibel dengan template, jadi secara teori ini dapat digunakan untuk menghasilkan kode PHP, tetapi kami belum mencobanya.
Go-swagger tidak hanya tentang pembuatan lapisan transportasi. Sebenarnya, ini menghasilkan kerangka aplikasi, dan di sini saya ingin menyebutkan sedikit tentang budaya pengembangan di DC. Kami memiliki Sumber Dalam, yang berarti bahwa setiap pengembang dari tim mana pun dapat membuat permintaan tarik ke layanan apa pun yang kami miliki. Untuk membuat skema ini berfungsi, kami mencoba untuk membakukan pendekatan dalam pengembangan: kami menggunakan terminologi umum, pendekatan tunggal untuk logging, metrik, bekerja dengan dependensi dan, tentu saja, ke struktur proyek.
Karenanya, dengan menerapkan go-swagger, kami memperkenalkan standar untuk mengembangkan layanan kami di Go. Ini adalah langkah lain menuju tujuan kami, yang awalnya tidak kami harapkan, tetapi penting untuk pembangunan secara umum.
Langkah pertama
Jadi, go-swagger ternyata menjadi kandidat menarik yang sepertinya bisa memenuhi sebagian besar kebutuhan yang kita
Catatan: semua kode lebih lanjut relevan untuk versi 0.24.0, petunjuk penginstalan dapat dilihat di repositori kami dengan contoh , dan situs web resmi memiliki petunjuk untuk menginstal versi saat ini.Mari kita lihat apa yang bisa dia lakukan. Mari kita ambil spesifikasi yang tinggi dan menghasilkan layanan:
> goswagger generate server \
--with-context -f ./swagger-api/swagger.yml \
--name example1
Kami mendapat yang berikut:
Makefile dan go.mod Saya sudah membuat sendiri.
Faktanya, kami berakhir dengan layanan yang memproses permintaan yang dijelaskan dengan sombong.
> go run cmd/example1-server/main.go
2020/02/17 11:04:24 Serving example service at http://127.0.0.1:54586
> curl http://localhost:54586/hello -i
HTTP/1.1 501 Not Implemented
Content-Type: application/json
Date: Sat, 15 Feb 2020 18:14:59 GMT
Content-Length: 58
Connection: close
"operation hello HelloWorld has not yet been implemented"
Langkah kedua. Memahami template
Jelas, kode yang kita buat jauh dari apa yang ingin kita lihat dalam operasi.
Apa yang kami inginkan dari struktur aplikasi kami:
- Mampu mengkonfigurasi aplikasi: mentransfer pengaturan untuk menghubungkan ke database, menentukan port koneksi HTTP, dan sebagainya.
- Pilih objek aplikasi yang akan menyimpan status aplikasi, koneksi database, dan sebagainya.
- Jadikan fungsi penangan aplikasi kita, ini harus menyederhanakan pekerjaan dengan kode.
- Inisialisasi dependensi di file utama (dalam contoh kami ini tidak akan terjadi, tetapi kami masih menginginkan ini.
Untuk mengatasi masalah baru, kami dapat mengganti beberapa templat. Untuk melakukan ini, kami akan menjelaskan file-file berikut, seperti yang saya lakukan ( Github ):
Kami perlu menjelaskan file template (
`*.gotmpl`) dan file untuk konfigurasi ( `*.yml`) untuk menghasilkan layanan kami.
Selanjutnya secara berurutan kita akan menganalisa template yang saya buat. Saya tidak akan mendalami bekerja dengan mereka, karena dokumentasi go-swagger cukup rinci, misalnya, berikut adalah deskripsi file konfigurasi. Saya hanya akan mencatat bahwa Go-templating digunakan, dan jika Anda sudah memiliki pengalaman dalam hal ini atau harus menjelaskan konfigurasi HELM, maka tidak akan sulit untuk mengetahuinya.
Konfigurasi Aplikasi
config.gotmpl berisi struktur sederhana dengan satu parameter - port yang akan didengarkan aplikasi untuk permintaan HTTP yang masuk. Saya juga membuat fungsi
InitConfigyang akan membaca variabel lingkungan dan mengisi struktur ini. Saya akan menyebutnya dari main.go, jadi saya InitConfigmenjadikannya fungsi publik.
package config
import (
"github.com/pkg/errors"
"github.com/vrischmann/envconfig"
)
// Config struct
type Config struct {
HTTPBindPort int `envconfig:"default=8001"`
}
// InitConfig func
func InitConfig(prefix string) (*Config, error) {
config := &Config{}
if err := envconfig.InitWithPrefix(config, prefix); err != nil {
return nil, errors.Wrap(err, "init config failed")
}
return config, nil
}
Untuk template ini yang akan digunakan saat membuat kode, itu harus ditentukan di konfigurasi YML :
layout:
application:
- name: cfgPackage
source: serverConfig
target: "./internal/config/"
file_name: "config.go"
skip_exists: false
Saya akan memberi tahu Anda sedikit tentang parameter:
name- memiliki fungsi informatif murni dan tidak mempengaruhi generasi.source- sebenarnya jalur ke file templat di camelCase, mis. serverConfig sama dengan ./server/config.gotmpl .target- direktori tempat kode yang dihasilkan akan disimpan. Di sini Anda dapat menggunakan template untuk membuat jalur secara dinamis ( contoh ).file_name- nama file yang dihasilkan, di sini Anda juga dapat menggunakan template.skip_exists- tanda bahwa file hanya akan dibuat sekali dan tidak akan menimpa file yang sudah ada. Ini penting bagi kami, karena file konfigurasi akan berubah seiring pertumbuhan aplikasi dan tidak bergantung pada kode yang dihasilkan.
Dalam konfigurasi pembuatan kode, Anda perlu menentukan semua file, dan bukan hanya yang ingin kita timpa. Untuk file yang kita tidak berubah, dalam arti
sourcetitik keluar asset:< >, misalnya, di sini : asset:serverConfigureapi. Ngomong-ngomong, jika Anda tertarik melihat templat aslinya, mereka ada di sini .
Objek aplikasi dan penangan
Saya tidak akan menjelaskan objek aplikasi untuk menyimpan negara, koneksi database dan hal-hal lain, semuanya mirip dengan konfigurasi yang baru saja dibuat. Tetapi dengan penangan, semuanya menjadi sedikit lebih menarik. Tujuan utama kami adalah membuat fungsi rintisan di file terpisah saat kami menambahkan URL ke spesifikasi, dan yang terpenting, server kami memanggil fungsi ini untuk memproses permintaan.
Mari kita gambarkan template fungsi dan stub:
package app
import (
api{{ pascalize .Package }} "{{.GenCommon.TargetImportPath}}/{{ .RootPackage }}/operations/{{ .Package }}"
"github.com/go-openapi/runtime/middleware"
)
func (srv *Service){{ pascalize .Name }}Handler(params api{{ pascalize .Package }}.{{ pascalize .Name }}Params{{ if .Authorized }}, principal api{{ .Package }}.{{ if not ( eq .Principal "interface{}" ) }}*{{ end }}{{ .Principal }}{{ end }}) middleware.Responder {
return middleware.NotImplemented("operation {{ .Package }} {{ pascalize .Name }} has not yet been implemented")
}
Mari kita lihat contoh sedikit:
pascalize- sejalan dengan CamelCase (deskripsi fungsi lain di sini )..RootPackage- paket server web yang dihasilkan..Package- nama paket dalam kode yang dihasilkan, yang menjelaskan semua struktur yang diperlukan untuk permintaan dan tanggapan HTTP, mis. struktur. Misalnya, struktur untuk isi permintaan atau struktur respons..Name- nama pawang. Ini diambil dari operationID dalam spesifikasi, jika ditentukan. Saya sarankan selalu menentukanoperationIDuntuk hasil yang lebih jelas.
Konfigurasi untuk pawang adalah sebagai berikut:
layout:
operations:
- name: handlerFns
source: serverHandler
target: "./internal/app"
file_name: "{{ (snakize (pascalize .Name)) }}.go"
skip_exists: true
Seperti yang Anda lihat, kode penangan tidak akan ditimpa (
skip_exists: true), dan nama file akan dibuat dari nama penangan.
Oke, ada fungsi stub, tetapi web server belum mengetahui bahwa fungsi ini harus digunakan untuk memproses permintaan. Saya memperbaikinya di main.go (saya tidak akan memberikan seluruh kode, versi lengkap dapat ditemukan di sini ):
package main
{{ $name := .Name }}
{{ $operations := .Operations }}
import (
"fmt"
"log"
"github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/generated/restapi"
"github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/generated/restapi/operations"
{{range $index, $op := .Operations}}
{{ $found := false }}
{{ range $i, $sop := $operations }}
{{ if and (gt $i $index ) (eq $op.Package $sop.Package)}}
{{ $found = true }}
{{end}}
{{end}}
{{ if not $found }}
api{{ pascalize $op.Package }} "{{$op.GenCommon.TargetImportPath}}/{{ $op.RootPackage }}/operations/{{ $op.Package }}"
{{end}}
{{end}}
"github.com/go-openapi/loads"
"github.com/vrischmann/envconfig"
"github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/app"
)
func main() {
...
api := operations.New{{ pascalize .Name }}API(swaggerSpec)
{{range .Operations}}
api.{{ pascalize .Package }}{{ pascalize .Name }}Handler = api{{ pascalize .Package }}.{{ pascalize .Name }}HandlerFunc(srv.{{ pascalize .Name }}Handler)
{{- end}}
...
}
Kode yang diimport terlihat rumit, meskipun pada kenyataannya hanya Go-templating dan struktur dari repositori go-swagger. Dan dalam sebuah fungsi,
mainkami cukup menetapkan fungsi yang dihasilkan ke penangan.
Tetap menghasilkan kode yang menunjukkan konfigurasi kita:
> goswagger generate server \
-f ./swagger-api/swagger.yml \
-t ./internal/generated -C ./swagger-templates/default-server.yml \
--template-dir ./swagger-templates/templates \
--name example2
Hasil akhirnya dapat dilihat di repositori kami .
Apa yang kami dapatkan:
- Kita dapat menggunakan struktur kita untuk aplikasi, konfigurasi dan apapun yang kita inginkan. Yang terpenting, cukup mudah untuk menyematkan kode yang dihasilkan.
- Kami dapat secara fleksibel mengelola struktur proyek, hingga ke nama file individual.
- Membuat template terlihat rumit dan membutuhkan waktu untuk membiasakan diri, tetapi secara keseluruhan ini adalah alat yang sangat kuat.
Langkah ketiga. Menghasilkan klien
Go-swagger juga memungkinkan kami membuat paket klien untuk layanan kami yang dapat digunakan oleh layanan Go lainnya. Di sini saya tidak akan membahas pembuatan kode secara detail, pendekatannya persis sama dengan saat membuat kode sisi server.
Untuk proyek Go, biasanya memasukkan paket publik
./pkg, kami akan melakukan hal yang sama: masukkan klien untuk layanan kami di pkg, dan buat kode itu sendiri sebagai berikut:
> goswagger generate client -f ./swagger-api/swagger.yml -t ./pkg/example3
Contoh kode yang dihasilkan ada di sini .
Sekarang semua konsumen layanan kami dapat mengimpor klien ini untuk diri mereka sendiri, misalnya, dengan tag (untuk contoh saya, tag akan menjadi
example3/pkg/example3/v0.0.1).
Template klien dapat disesuaikan, misalnya, mengalir
open tracing iddari konteks ke header.
kesimpulan
Secara alami, implementasi internal kami berbeda dari kode yang ditampilkan di sini terutama karena penggunaan paket internal dan pendekatan ke CI (menjalankan berbagai pengujian dan linter). Dalam kode yang dihasilkan di luar kotak, kumpulan metrik teknis, bekerja dengan konfigurasi, dan logging dikonfigurasi. Kami telah menstandarkan semua alat umum. Karena itu, kami menyederhanakan pengembangan secara umum dan peluncuran layanan baru secara khusus, memastikan bagian yang lebih cepat dari daftar periksa layanan sebelum menerapkan ke produk.
Mari kita periksa apakah kita telah mencapai tujuan awal kita:
- Pastikan relevansi kontrak yang dijelaskan untuk layanan, ini harus mempercepat pengenalan layanan baru dan menyederhanakan komunikasi antar tim - Ya .
- HTTP ( event streaming) — .
- , .. Inner Source — .
- , — ( — Bitbucket).
- , — ( , , ).
- Go — ( ).
Pembaca yang penuh perhatian mungkin telah menanyakan pertanyaan: bagaimana file template masuk ke proyek kita? Kami sekarang menyimpannya di setiap proyek kami. Ini menyederhanakan pekerjaan sehari-hari, memungkinkan Anda menyesuaikan sesuatu untuk proyek tertentu. Tetapi ada sisi lain dari koin: tidak ada mekanisme untuk pembaruan templat dan pengiriman fitur-fitur baru yang terpusat, terutama yang terkait dengan CI.
NB Jika Anda menyukai materi ini, maka kedepannya kami akan menyiapkan artikel tentang arsitektur standar layanan kami, kami akan memberi tahu Anda prinsip apa yang kami gunakan saat mengembangkan layanan di Go.