
Infrastruktur web modern terdiri dari banyak komponen untuk berbagai keperluan, dengan jelas dan tidak terlalu saling berhubungan. Ini menjadi sangat jelas ketika mengoperasikan aplikasi yang menggunakan tumpukan perangkat lunak yang berbeda, yang dengan munculnya layanan microsoft mulai terjadi secara harfiah pada setiap langkah. Faktor-faktor eksternal (API pihak ketiga, layanan, dll.) Ditambahkan ke "kesenangan" umum, yang memperumit gambaran yang sudah sulit.
Secara umum, bahkan jika aplikasi ini akan disatukan oleh ide-ide dan solusi arsitektur umum, untuk menghilangkan masalah yang tidak biasa di dalamnya sering harus mengarungi alam liar yang tidak dikenal berikutnya. Apakah masalah seperti itu terjadi hanya masalah waktu. Ini adalah contoh dari praktik terbaru kami yang ditujukan untuk artikel ini. Pemain: Golang, Sentry, RabbitMQ, nginx, PostgreSQL dan lainnya.
Sejarah No. 1. Golang dan HTTP / 2
Menjalankan tolok ukur yang melakukan banyak permintaan HTTP ke aplikasi web telah menghasilkan hasil yang tidak terduga. Aplikasi Go sederhana dalam proses benchmark digunakan untuk aplikasi Go lain yang terletak di belakang masuknya / openresty. Ketika HTTP / 2 diaktifkan, kami mendapatkan kesalahan dengan kode 400 untuk beberapa permintaan. Untuk memahami alasan perilaku ini, kami menghapus aplikasi Go di ujung dari rantai dan membuat lokasi sederhana di Ingress, yang selalu mengembalikan 200. Perilaku tersebut tidak berubah!
Kemudian diputuskan untuk mereproduksi skrip di luar lingkungan Kubernetes - pada perangkat keras yang berbeda. Hasilnya adalah Makefile, dengan bantuan dua wadah diluncurkan: dalam satu - tolok ukur yang menuju ke nginx, di yang lain - di Apache. Keduanya mendengarkan HTTP / 2 dengan sertifikat yang ditandatangani sendiri. Waktu operasi terakhir lihat di
repositori ini .
Jalankan benchmark dengan
concurrency=200
:
1.1. Nginx:
Completed 0 requests Completed 1000 requests Completed 2000 requests Completed 3000 requests Completed 4000 requests Completed 5000 requests Completed 6000 requests Completed 7000 requests Completed 8000 requests Completed 9000 requests ----- Bench results begin ----- Requests per second: 10336.09 Failed requests: 1623 ----- Bench results end -----
1.2. Apache:
… ----- Bench results begin ----- Requests per second: 11427.60 Failed requests: 0 ----- Bench results end -----
Kami berasumsi bahwa intinya di sini adalah implementasi HTTP / 2 yang kurang ketat di Apache.
Mari kita coba dengan
concurrency=1000
:
2.1. Nginx:
… ----- Bench results begin ----- Requests per second: 11274.92 Failed requests: 4205 ----- Bench results end -----
2.2. Apache:
… ----- Bench results begin ----- Requests per second: 11211.48 Failed requests: 5 ----- Bench results end -----
Pada saat yang sama, kami mencatat bahwa hasilnya
tidak direproduksi setiap waktu : beberapa peluncuran berlalu tanpa masalah.
Pencarian untuk masalah pada github proyek Golang mengarah ke
# 25009 dan
# 32441 . Melalui mereka kami pergi ke
PR 903 : menonaktifkan HTTP / 2 di Go secara default!
Menafsirkan hasil benchmark tanpa menyelam jauh ke dalam arsitektur server web di atas cukup sulit. Dalam kasus tertentu, itu cukup untuk menonaktifkan HTTP / 2 untuk layanan yang ditentukan.
Sejarah No. 2. Simfoni dan penjaga tua
Dalam salah satu proyek, versi kerangka PHP symfony yang sangat lama (v2.3) masih berfungsi. Klien Raven tua dan kelas tulisan-sendiri dalam PHP terlampir padanya “dalam kit”, yang sedikit menyulitkan untuk debugging.
Setelah transfer salah satu layanan di Kubernetes ke Sentry, yang digunakan untuk melacak kesalahan dalam aplikasi proyek ini, peristiwa tiba-tiba berhenti datang. Untuk mereproduksi perilaku ini, kami menggunakan contoh dari situs web Sentry, mengambil dua opsi dan menyalin DSN dari pengaturan Sentry. Secara visual, semuanya berfungsi: pesan kesalahan (diduga) dikirim satu demi satu.
Opsi pemeriksaan JavaScript:
<!DOCTYPE html> <html> <body> <script src="https://browser.sentry-cdn.com/5.6.3/bundle.min.js" integrity="sha384-/Cqa/8kaWn7emdqIBLk3AkFMAHBk0LObErtMhO+hr52CntkaurEnihPmqYj3uJho" crossorigin="anonymous"> </script> <h2>JavaScript in Body</h2> <p id="demo">A Paragraph.</p> <button type="button" onclick="myFunction()">Try it</button> <script> Sentry.init({ dsn: 'http://33dddd76e9f0c4ddcdb51@sentry.kube-dev.test//12' }); try { throw new Error('Caught'); } catch (err) { Sentry.captureException(err); } </script> </body> </html>
Demikian pula dalam Python:
from sentry_sdk import init, capture_message init("http://33dddd76e9f0c4ddcdb51@sentry.kube-dev.test//12") capture_message("Hello World")
Namun, mereka tidak masuk ke Sentry. Saat mengirim pesan, ilusi dibuat bahwa pesan itu dikirim, karena klien segera menghasilkan hash untuk masalah ini.
Akibatnya, masalah diselesaikan dengan sangat sederhana: pengiriman acara pergi ke HTTP, dan layanan Sentry hanya mendengarkan HTTPS. Arahan ulang dari HTTP ke HTTPS disediakan, tetapi klien lama (kode di sisi symfony) tidak dapat mengikuti arahan ulang, yang tidak diharapkan secara default hari ini.
Sejarah No. 3. RabbitMQ dan proksi pihak ketiga
Dalam satu proyek, cloud Evotor digunakan untuk menghubungkan register kas. Bahkan, ini berfungsi sebagai proksi: permintaan POST dari Evotor langsung ke RabbitMQ - melalui
plugin STOMP diimplementasikan melalui koneksi WebSocket.
Salah satu pengembang membuat permintaan pengujian menggunakan Postman dan menerima respons yang diharapkan dari
200 OK
, namun, permintaan melalui cloud menyebabkan
405 Method Not Allowed
.
200 OK source: kubernetes namespace: kube-nginx-ingress host: kube-node-2 pod_name: nginx-2bpt7 container_name: nginx stream: stdout app: nginx controller-revision-hash: 5bdbfd564 pod-template-generation: 25 time: 2019-09-10T09:42:50+00:00 request_id: 1271dba228f0943ab2df0196ff0d7f67 user: client address: 100.200.300.400 protocol: HTTP/1.1 scheme: http method: POST host: rmq-review.kube-dev.client.domain path: /api/queues/vhost/queue.gen.eeeeffff111:1.onlinecassa:55556666/get request_query: referrer: user_agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36 content_kind: cacheable namespace: review ingress: stomp-ws service: rabbitmq service_port: stats vhost: rmq-review.kube-dev.client.domain location: / nginx_upstream_addr: 10.127.1.1:15672 nginx_upstream_bytes_received: 2538 nginx_upstream_response_time: 0.008 nginx_upstream_status: 200 bytes_received: 757 bytes_sent: 1254 request_time: 0 status: 200 upstream_response_time: 0 upstream_retries: 0
405 Metode Tidak Diizinkan source: kubernetes namespace: kube-nginx-ingress host: kube-node-1 pod_name: nginx-4xx6h container_name: nginx stream: stdout app: nginx controller-revision-hash: 5bdbfd564 pod-template-generation: 25 time: 2019-09-10T09:46:26+00:00 request_id: b8dd789604864c95b4af499ed6805630 user: client address: 200.100.300.400 protocol: HTTP/1.1 scheme: http method: POST host: rmq-review.kube-dev.client.domain path: /api/queues/vhost/queue.gen.ef7fb93387ca9b544fc1ecd581cad4a9:1.onlinecassa:55556666/get request_query: referrer: user_agent: ru.evotor.proxy/37 content_kind: cache-headers-not-present namespace: review ingress: stomp-ws service: rabbitmq service_port: stats vhost: rmq-review.kube-dev.client.domain location: / nginx_upstream_addr: 10.127.1.1:15672 nginx_upstream_bytes_received: 134 nginx_upstream_response_time: 0.004 nginx_upstream_status: 405 bytes_received: 878 bytes_sent: 137 request_time: 0 status: 405 upstream_response_time: 0 upstream_retries: 0
Permintaan tcpdump dari Evotor 200.100.300.400.21519 > 100.200.400.300: Flags [P.], cksum 0x8e29 (correct), seq 1:879, ack 1, win 221, options [nop,nop,TS val 2313007107 ecr 79097074], length 878: HTTP, length: 878 POST /api/queues//vhost/queue.gen.ef7fb93387ca9b544fc1ecd581cad4a9:1.onlinecassa:55556666/get HTTP/1.1 device-model: ST-5 device-os: android Accept-Encoding: gzip content-type: application/json; charset=utf-8 connection: close accept: application/json x-original-forwarded-for: 10.11.12.13 originhost: rmq-review.kube-dev.client.domain x-original-uri: /api/v2/apps/e114-aaef-bbbb-beee-abadada44ae/requests x-scheme: https accept-encoding: gzip user-agent: ru.evotor.proxy/37 Authorization: Basic X-Evotor-Store-Uuid: 20180417-73DC-40C9-80B7-00E990B77D2D X-Evotor-Device-Uuid: 20190909-A47B-40EA-806A-F7BC33833270 X-Evotor-User-Id: 01-000000000147888 Content-Length: 58 Host: rmq-review.kube-dev.client.domain {"count":1,"encoding":"auto","ackmode":"ack_requeue_true"}[!http] 12:53:30.095385 IP (tos 0x0, ttl 64, id 5512, offset 0, flags [DF], proto TCP (6), length 52) 100.200.400.300:80 > 200.100.300.400.21519: Flags [.], cksum 0xfa81 (incorrect -> 0x3c87), seq 1, ack 879, win 60, options [nop,nop,TS val 79097122 ecr 2313007107], length 0 12:53:30.096876 IP (tos 0x0, ttl 64, id 5513, offset 0, flags [DF], proto TCP (6), length 189) 100.200.400.300:80 > 200.100.300.400.21519: Flags [P.], cksum 0xfb0a (incorrect -> 0x03b9), seq 1:138, ack 879, win 60, options [nop,nop,TS val 79097123 ecr 2313007107], length 137: HTTP, length: 137 HTTP/1.1 405 Method Not Allowed Date: Tue, 10 Sep 2019 10:53:30 GMT Content-Length: 0 Connection: close allow: HEAD, GET, OPTIONS
Permintaan tcpdump dibuat oleh curl 777.10.74.11.61211 > 100.200.400.300:80: Flags [P.], cksum 0x32a8 (correct), seq 1:397, ack 1, win 2052, options [nop,nop,TS val 734012594 ecr 4012360530], length 396: HTTP, length: 396 POST /api/queues/%2Fvhost/queue.gen.ef7fb93387ca9b544fc1ecd581cad4a9:1.onlinecassa:55556666/get HTTP/1.1 Host: rmq-review.kube-dev.client.domain User-Agent: curl/7.54.0 Authorization: Basic = Content-Type: application/json Accept: application/json Content-Length: 58 {"count":1,"ackmode":"ack_requeue_true","encoding":"auto"}[!http] 12:40:11.001442 IP (tos 0x0, ttl 64, id 50844, offset 0, flags [DF], proto TCP (6), length 52) 100.200.400.300:80 > 777.10.74.11.61211: Flags [.], cksum 0x2d01 (incorrect -> 0xfa25), seq 1, ack 397, win 59, options [nop,nop,TS val 4012360590 ecr 734012594], length 0 12:40:11.017065 IP (tos 0x0, ttl 64, id 50845, offset 0, flags [DF], proto TCP (6), length 2621) 100.200.400.300:80 > 777.10.74.11.61211: Flags [P.], cksum 0x370a (incorrect -> 0x6872), seq 1:2570, ack 397, win 59, options [nop,nop,TS val 4012360605 ecr 734012594], length 2569: HTTP, length: 2569 HTTP/1.1 200 OK Date: Tue, 10 Sep 2019 10:40:11 GMT Content-Type: application/json Content-Length: 2348 Connection: keep-alive Vary: Accept-Encoding cache-control: no-cache vary: accept, accept-encoding, origin
Mata seorang insinyur yang terlatih segera melihat perbedaannya:
- curl:
POST /api/queues/%2Fclient…
- Evotor:
POST /api/queues//client…
Masalahnya adalah bahwa dalam satu kasus, suatu hal yang tidak dapat dipahami (untuk RabbitMQ)
//vhost
, dan dalam kasus lain -
%2Fvhost
, yang merupakan perilaku yang diharapkan ketika:
# rabbitmqctl list_vhosts Listing vhosts ... /vhost
Dalam
masalah proyek RabbitMQ tentang topik ini, pengembang menjelaskan:
Kami tidak akan mengganti% -encoding. Ini cara standar penyandian jalur URL dan telah berlangsung lama. Dengan asumsi bahwa% -encoding dalam alat berbasis HTTP akan hilang karena bahkan kerangka kerja yang paling populer dengan asumsi jalur URL seperti itu "jahat" adalah picik dan naif. Nama host virtual default dapat diubah ke nilai apa pun (seperti nilai yang tidak menggunakan garis miring atau karakter lain yang memerlukan%-encoding) dan setidaknya dengan rilis BOSH Penting dari RabbitMQ, host virtual default tetap dihapus pada waktu penyebaran. .
Masalahnya diselesaikan tanpa keterlibatan lebih lanjut dari teknisi kami (di sisi Evotor setelah menghubungi mereka).
Sejarah No. 4. Gen dalam PostgreSQL
PostgreSQL memiliki indeks yang sangat berguna, yang sering dilupakan. Kisah ini dimulai dengan keluhan tentang rem dalam aplikasi. Dalam
artikel terbaru, kami telah memberikan contoh perkiraan alur kerja saat menganalisis situasi seperti itu. Dan di sini APM kami -
Atatus - menunjukkan gambar berikut:

Pada jam 10 pagi ada peningkatan waktu yang dihabiskan aplikasi untuk bekerja dengan database. Seperti yang diharapkan, alasannya terletak pada respons lambat dari DBMS. Bagi kami, menganalisis kueri, mengidentifikasi area masalah, dan indeks "menggantung" adalah rutin yang dapat dipahami.
Okmeter yang kami gunakan
banyak membantu
di dalamnya : ada dua panel standar untuk memantau status server dan kemampuan untuk
dengan cepat membangun server kami sendiri - dengan output metrik yang bermasalah:

Grafik memuat CPU menunjukkan bahwa salah satu basis data dimuat 100%. Mengapa Panel PostgreSQL baru akan meminta:

Penyebab masalah segera terlihat - konsumen CPU utama:
SELECT u.* FROM users u WHERE u.id = ? & u.field_1 = ? AND u.field_2 LIKE '%somestring%' ORDER BY u.id DESC LIMIT ?
Mempertimbangkan rencana kerja kueri yang bermasalah, kami menemukan bahwa pemfilteran menurut bidang tabel yang diindeks memberi terlalu banyak pilihan: database menerima lebih dari 70 ribu baris oleh
id
dan
field_1
, dan kemudian mencari substring di antara mereka. Ternyata
LIKE
pada substring iterates atas sejumlah besar data teks, yang mengarah ke perlambatan serius dalam eksekusi permintaan dan peningkatan beban CPU.
Di sini Anda dapat dengan benar melihat bahwa masalah arsitektur tidak dikesampingkan (koreksi logika aplikasi atau bahkan mesin teks lengkap diperlukan ...), tetapi tidak ada waktu untuk pengerjaan ulang, tetapi seharusnya bekerja dengan cepat 15 menit yang lalu. Pada saat yang sama, kata pencarian sebenarnya adalah pengidentifikasi (dan mengapa tidak di bidang yang terpisah? ..), yang menghasilkan satuan garis. Bahkan, jika kita dapat membuat indeks pada bidang teks ini, semua yang lain akan menjadi tidak perlu.Solusi terakhir saat ini adalah menambahkan indeks GIN untuk
field_2
. Itu pahlawan hari itu - "jin" yang sama. Singkatnya, GIN adalah semacam indeks yang berkinerja sangat baik dalam pencarian teks lengkap, mempercepatnya secara kualitatif. Anda dapat membaca lebih lanjut tentang itu, misalnya, dalam
materi yang luar biasa ini .

Seperti yang Anda lihat, operasi sederhana ini memungkinkan untuk menghapus beban tambahan, dan dengan itu - dan menghemat uang untuk klien.
Sejarah No. 5. Caching s3 di nginx
Penyimpanan awan yang kompatibel dengan S3 telah lama berada dalam daftar teknologi yang digunakan oleh banyak proyek. Jika Anda memerlukan penyimpanan gambar yang andal untuk situs Anda atau untuk data jaringan saraf, Amazon S3 adalah pilihan yang bagus. Keandalan penyimpanan dan ketersediaan data yang tinggi (dan kurangnya kebutuhan untuk "pagar taman") sangat menawan.
Namun, terkadang untuk menghemat uang - karena biasanya pembayaran untuk S3 berlaku untuk permintaan dan lalu lintas - solusi yang baik adalah dengan menginstal server proxy caching di depan penyimpanan. Metode ini akan mengurangi biaya ketika datang, misalnya, ke avatar pengguna, yang ada banyak di setiap halaman.
Tampaknya lebih mudah daripada mengambil nginx dan mengatur proxy dengan caching, validasi ulang, pembaruan latar belakang, dan blackjack lainnya? Namun, seperti di tempat lain, ada beberapa nuansa ...
Konfigurasi perkiraan proxy seperti itu dengan caching tampak seperti ini:
proxy_cache_key $uri; proxy_cache_methods GET HEAD; proxy_cache_lock on; proxy_cache_revalidate on; proxy_cache_background_update on; proxy_cache_use_stale updating error timeout invalid_header http_500 http_502 http_503 http_504; proxy_cache_valid 200 1h; location ~ ^/(?<bucket>avatars|images)/(?<filename>.+)$ { set $upstream $bucket.s3.amazonaws.com; proxy_pass http://$upstream/$filename; proxy_set_header Host $upstream; proxy_cache aws; proxy_cache_valid 200 1h; proxy_cache_valid 404 60s; }
Dan secara umum, ini berhasil: gambar ditampilkan, semuanya baik-baik saja dengan cache ... namun, masalah dengan klien AWS S3 muncul. Secara khusus, klien dari
aws-sdk-php berhenti bekerja. Analisis nginx log menunjukkan bahwa upstream mengembalikan kode 403 untuk permintaan HEAD, dan responsnya mengandung kesalahan spesifik:
SignatureDoesNotMatch
. Ketika kami melihat bahwa nginx membuat permintaan GET ke hulu, semuanya jatuh ke tempatnya.
Faktanya adalah bahwa klien S3 menandatangani setiap permintaan, dan server memeriksa tanda tangan ini. Dalam hal proxy sederhana, semuanya berfungsi dengan baik: permintaan mencapai server tidak berubah. Namun, ketika caching diaktifkan, nginx mulai mengoptimalkan kerja dengan backend dan menggantikan permintaan HEAD dengan GET. Logikanya sederhana: lebih baik untuk mengambil dan menyimpan seluruh objek, dan kemudian semua permintaan HEAD dari cache juga. Namun, dalam kasus kami, permintaan tidak dapat diubah, karena ditandatangani.
Pada dasarnya ada dua solusi:
- Jangan mengarahkan klien S3 melalui proxy;
- jika "perlu", matikan opsi
proxy_cache_convert_head
dan tambahkan $request_method
ke kunci caching. Dalam hal ini, nginx mengirimkan permintaan HEAD "apa adanya" dan menyimpan responsnya secara terpisah.
Sejarah No. 6. DDoS dan Konten Pengguna Google
Minggu malam tidak menandakan masalah sampai - tiba-tiba! - Antrian cache invalidation pada edge server belum bertambah, yang memberikan traffic ke pengguna nyata. Ini adalah gejala yang sangat aneh: lagipula, cache diimplementasikan dalam memori dan tidak terikat ke hard drive. Pembilasan cache dalam arsitektur yang digunakan adalah operasi yang murah, jadi kesalahan ini hanya dapat muncul jika bebannya sangat tinggi. Ini dikonfirmasi oleh fakta bahwa server yang sama mulai memberi tahu kemunculan 500 kesalahan
(garis merah pada grafik di bawah) .

Lonjakan yang tajam menyebabkan overruns CPU:

Analisis cepat menunjukkan bahwa permintaan tidak datang ke domain utama, tetapi dari log menjadi jelas bahwa mereka berada di vhost default. Sepanjang jalan, ternyata banyak pengguna Amerika datang ke sumber daya Rusia. Keadaan seperti itu selalu segera menimbulkan pertanyaan.
Setelah mengumpulkan data dari log nginx, kami mengungkapkan bahwa kami berurusan dengan botnet tertentu:
35.222.30.127 US [15/Sep/2019:21:40:00 +0300] GET "http://example.ru/?ITPDH=XHJI" HTTP/1.1 301 178 "http://example.ru/ORQHYGJES" "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 3.5.30729)" "-" "upcache=-" "upaddr=-" "upstatus=-" "uplen=-" "uptime=-" spdy="" "loc=wide-closed.example.ru.undef" "rewrited=/?ITPDH=XHJI" "redirect=http://www.example.ru/?ITPDH=XHJI" ancient=1 cipher=- "LM=-;EXP=-;CC=-" 107.178.215.0 US [15/Sep/2019:21:40:00 +0300] GET "http://example.ru/?REVQSD=VQPYFLAJZ" HTTP/1.1 301 178 "http://www.usatoday.com/search/results?q=MLAJSBZAK" "Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)" "-" "upcache=-" "upaddr=-" "upstatus=-" "uplen=-" "uptime=-" spdy="" "loc=wide-closed.example.ru.undef" "rewrited=/?REVQSD=VQPYFLAJZ" "redirect=http://www.example.ru/?REVQSD=VQPYFLAJZ" ancient=1 cipher=- "LM=-;EXP=-;CC=-" 107.178.215.0 US [15/Sep/2019:21:40:00 +0300] GET "http://example.ru/?MPYGEXB=OMJ" HTTP/1.1 301 178 "http://engadget.search.aol.com/search?q=MIWTYEDX" "Mozilla/5.0 (Windows; U; Windows NT 6.1; en; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 3.5.30729)" "-" "upcache=-" "upaddr=-" "upstatus=-" "uplen=-" "uptime=-" spdy="" "loc=wide-closed.example.ru.undef" "rewrited=/?MPYGEXB=OMJ" "redirect=http://www.example.ru/?MPYGEXB=OMJ" ancient=1 cipher=- "LM=-;EXP=-;CC=-"
Pola yang dapat dimengerti dilacak dalam log:
- agen-pengguna sejati;
- permintaan ke root URL dengan argumen GET acak untuk menghindari masuk ke cache;
- perujuk menunjukkan bahwa permintaan tersebut berasal dari mesin pencari.
Kami mengumpulkan alamat dan memverifikasi afiliasinya - semuanya milik
googleusercontent.com
, dengan dua subnet (107.178.192.0/18 dan 34.64.0.0/10). Subnet ini berisi mesin virtual GCE dan berbagai layanan, seperti terjemahan halaman.
Untungnya, serangan itu tidak berlangsung lama (sekitar satu jam) dan secara bertahap menurun. Tampaknya algoritme pelindung di dalam Google telah berfungsi, sehingga masalahnya diselesaikan "dengan sendirinya."
Serangan ini tidak merusak, tetapi menimbulkan pertanyaan berguna untuk masa depan:
- Mengapa anti-ddos tidak berfungsi? Layanan eksternal digunakan, yang kami kirimkan permintaan yang sesuai. Namun, ada banyak alamat ...
- Bagaimana melindungi diri Anda dari ini di masa depan? Dalam kasus kami, bahkan opsi untuk menutup akses berdasarkan geografis dimungkinkan.
PS
Baca juga di blog kami: