-Jep, JNI va GIL-
1. Kirish: Geterogen runtime'larni integratsiya qilishdagi muammolar
Zamonaviy backend arxitekturalarida servis mantiqi tobora kamroq hollarda bitta tilda tuzilmoqda. Numpy va Scipy yetakchiligidagi Python ekotizimi sonli tahlil va mashinaviy o‘rganish inferensiyasi uchun amalda de-fakto standartga aylangan bo‘lsa-da, JVM asosidagi steklar tranzaksiyalarni boshqarish va ekspluatatsion barqarorlikda hamon ustunlikni saqlab qolmoqda. Natijada backend muhandislari ikki xil runtime'ning kuchli jihatlarini yagona servis oqimiga integratsiya qilish muammosiga duch kelmoqda.
Kema ma’lumotlari platformasi ham xuddi shunday muammoga duch keldi. Dvigatel va generator holati, tanklardagi sathlar hamda korpusning holati haqidagi ma’lumotlar dengizdagi kemalardan har daqiqada kelib turadi, biroq platformaning asosiy vazifasi bu ma’lumotlarni saqlash emas. Uning vazifasi korpus harakatini talqin qiluvchi hisob-kitoblarni bajarish va odatiy ko‘rsatkichlarga nisbatan degradatsiya darajasini hisoblashdan iborat. Muammo shundaki, bu hisob-kitoblar predmet sohasi ekspertlari tomonidan Python’da yozilgan va tasdiqlangan kemasozlik formulalaridan tashkil topgan edi. Ular oldindan o‘qitilgan mashinaviy o‘rganish modellari va C kengaytma kutubxonalariga asoslangan bo‘lib, ularning mos Java implementatsiyalari hatto mavjud emas edi. Bundan tashqari, Class tasdig‘idan o‘tishi kerak bo‘lgan formulalar mavjud natijalarga kasr xonalarigacha mos keladigan natijalarni berishi lozim edi.
Ushbu texnik hujjatda Java Embedded Python (bundan buyon Jep) yordamida JVM jarayoni ichiga Python interpretatori joylashtirilgan holat tasvirlanadi. Avval dastlabki implementatsiyada uchragan ikkita muammo va ularni bartaraf etish usullari bayon qilinadi, so‘ngra bu muammolar nima sababdan strukturaviy jihatdan muqarrar bo‘lganini batafsil tushuntirish uchun Jep'ning ichki ishlashi JNI va GIL nuqtayi nazaridan tahlil qilinadi.
2. Texnologiyani tanlash asoslari: Jarayonlarni ajratish va jarayon ichida joylashtirish o‘rtasidagi muvozanat
Tasdiqlangan Python aktivlarini servisga integratsiya qilishning ikkita asosiy yondashuvi mavjud. Ularni mustaqil Python servisi sifatida ajratish nosozliklarni izolyatsiya qilish nuqtayi nazaridan afzallik beradi, biroq har bir chaqiruv uchun tarmoq bo‘ylab borib-kelish va serializatsiya xarajatlarini keltirib chiqaradi. Jarayon ichida joylashtirish esa, aksincha, ma’lumotlarni ayni jarayonning umumiy xotira maydonida bevosita almashishga imkon beradi.
Hal qiluvchi mezon hisob-kitob so‘rovlarni qayta ishlash oqimi ichida joylashgan-joylashmaganidan iborat edi. Platformadagi hisob-kitoblarning aksariyati har daqiqada keladigan sensor ma’lumotlari qabul qilinishi bilanoq bajarilishi va keyin domen obyektlari sifatida saqlanishi kerak edi. Shu sababli ushbu yo‘lga HTTP orqali borib-kelishlar va serializatsiyani kiritish amaliy foyda bermaydi, degan xulosaga kelib, jarayon ichidagi yondashuvni tanladik. Buni amalga oshiradigan kutubxona — Jep.
Jython'dan farqli ravishda, Jep JVM ustiga Python'ni qayta implementatsiya qilmaydi. Buning o‘rniga u JNI (Java Native Interface) orqali C tilida yozilgan asl Python interpretatorini chaqiradi. Numpy va Scipy asosan C kengaytma modullaridan iborat bo‘lib, Jython muhitida ishlamaydi. C kengaytmalari bilan moslik muhim talab bo‘lganligi sababli, Jep amalda yagona tanlov edi.
try (Interpreter interp = new SharedInterpreter()) {
interp.exec("import numpy as np");
interp.set("xs", new double[]{1.0, 2.0, 3.0});
interp.exec("result = float(np.mean(np.array(xs)))");
double mean = (Double) interp.getValue("result");
}
Interfeys uchta fe’ldan iborat. set() Java obyektini Python global o‘zgaruvchisiga bog‘laydi, exec() kodni bajaradi, getValue() esa natijani ajratib oladi. Python dict obyektlari avtomatik ravishda Java Map obyektlariga, list'lar esa List obyektlariga, jumladan ichma-ich tuzilmalarga aylantiriladi. Agar jarayonlar ajratilganida, JSON sxemasi ta’riflari va serializatsiya kodi aynan shu nuqtada qo‘shilgan bo‘lardi.
3. Dastlabki implementatsiyaning cheklovlari va texnik muammoli jihatlar
Dastlabki implementatsiya sodda tuzilishga ega edi: interpretatorlarni yaratish xarajatini kamaytirish uchun qayta foydalaniladigan bitta Jep instansiyasi yaratilgan va native kutubxona yo‘li konteyner muhitining standart qidiruv qoidalariga topshirilgan. Nuqson validatsiya muhitida ko‘rinmagan bo‘lsa-da, ishlab chiqarish muhitida parallellik va infratuzilmadagi o‘zgarishlar joriy etilgach, tabiati turlicha bo‘lgan ikkita muammo yuzaga keldi.
3.1. Instansiyadan qayta foydalanish dizayni: Oqimga bog‘liqlik cheklovini e’tibordan chetda qoldirish
Dastlabki implementatsiyada interpretatorlarni yaratish xarajatini kamaytirish uchun Jep instansiyasi muayyan klass maydonida saqlangan va bir nechta chaqiruvlar davomida qayta ishlatilgan. Bu ulanishlar pulini boshqarish nuqtayi nazaridan tabiiy dizayn edi va bir oqimli validatsiya vaqtida to‘g‘ri ishladi.
Biroq Jep instansiyasi shu tarzda ulashish mumkin bo‘lgan obyekt emas edi. Batafsil asos 4.4-bo‘limda bayon qilinadi, ammo sabab shundaki, Jep obyekti ichida saqlanadigan tstate maydoni har bir oqim uchun bir martadan mavjud bo‘lishi kerak bo‘lgan native PyThreadState ko‘rsatkichini o‘z ichiga oladi. Muammo instansiyani yaratgan oqimdan boshqa worker oqimi unga murojaat qilganida yuzaga chiqdi.
Bunga ikkita chora ko‘rildi. Birinchidan, interpretator implementatsiyasi SharedInterpreter'ga almashtirildi. Avvalgi implementatsiyada yana bir muammo mavjud edi: u Numpy kabi C kengaytma modullari bilan mos kelmasdi (4.3-bo‘lim). Ikkinchidan, oqimga bog‘liqlik muammosining o‘zini bartaraf etish uchun instansiyani maydonda saqlashdan voz kechildi va tuzilma try-with-resources bayonoti ichida foydalanish nuqtasida yaratib, darhol bo‘shatadigan qilib o‘zgartirildi. Keyingi o‘zgarish oqimlar muammosini hal qildi.
try (SharedInterpreter jep = new SharedInterpreter()) {
// 사용하는 스레드에서 직접 생성되고, 블록을 벗어나면 해제됨
}
Bu o‘zgarish oqimga bog‘liqlik muammosini hal qildi, biroq yangi muvozanatni yuzaga keltirdi: modullarni import qilish xarajati har bir chaqiruvda qaytadan to‘lanishi kerak bo‘ldi. Buning uchun kiritilgan tuzatish 5.2-bo‘limda muhokama qilinadi.
3.2. Bilvosita kutubxona qidiruvi: Infratuzilmadagi o‘zgarishlarga zaiflik
Ikkinchi muammo dastur kodida emas, balki joylashtirish konfiguratsiyasi qatlamida yuzaga keldi. Dastlab Jep native kutubxonasi va Python runtime'ining joylashuvlarini aniqlashda konteyner muhitining standart qidiruv qoidalariga tayandik. Tizim hech qanday qo‘shimcha konfiguratsiyasiz odatdagidek ishlagani sababli, aniq deklaratsiyalar zarurligini anglamagan edik.
Muammo bajaruvchi tugundagi Linux yadrosi versiyasi yangilangandan keyin yuzaga chiqdi. Runtime kutubxona yo‘lini aniqlay olmadi va dastur ishga tushish vaqtida ishdan chiqdi. Ilova kodining biror qatori o‘zgarmagan bo‘lsa-da, servis faqat infratuzilma qatlamidagi o‘zgarish sababli ishga tushmay qoldi.
Chora sifatida -Djava.library.path JVM opsiyasi yordamida native kutubxona yo‘li aniq deklaratsiya qilindi. Ushbu opsiyani konteyner image'i ichidagi kutubxona katalogiga yo‘l (masalan, native-libs) bilan birga Deployment manifest'ining spec.template.spec.containers[].args qismiga to‘g‘ridan-to‘g‘ri qo‘shdik. Shu orqali runtime'ning qidiruv qoidalariga tayanish o‘rniga aniq deklaratsiyaga o‘tildi.
# deployment.yml
spec:
template:
spec:
containers:
- args:
- "-Djava.library.path=/app/native-libs"
Bu holatdan kelib chiqadigan xulosa aniq. Native bog‘liqliklarga ega kutubxona uchun “alohida konfiguratsiyasiz ishlash” holati barqarorlik ta’minlanganini anglatmaydi; bu faqat muhit hali o‘zgarmaganini bildiradi.
4. Jep'ning ichki ishlashini qismlarga ajratib tahlil qilish
3-bo‘limdagi ikkala nosozlik ham Jep'ning ichki tuzilishidan kelib chiqadigan muqarrar oqibatlar edi. Quyidagi beshta mexanizm Jep asosidagi tizimlarning ishlashi va cheklovlarini belgilaydi.
4.1. Jarayon tuzilishi: Bitta jarayonda ikkita mustaqil runtime
Ichki jarayon tuzilishi
1-rasm. JVM sohasi va Python sohasi JNI chegarasi orqali bitta OS jarayoni ichida birga mavjud bo‘lgan tuzilma
Bitta OS jarayoni ichida uchta soha birga mavjud. JVM sohasi (thread pool, Jep instansiyalari va JVM Heap) to‘liq GC tomonidan boshqariladi, Python sohasi esa (PyThreadState, sys.modules/sys.path, C kengaytma .so fayllari va ndarray buferlari) GC tomonidan boshqarilmaydi. Bundan tashqari, jarayon tugaguniga qadar saqlanib qoladigan JepMainInterpreter'ga bag‘ishlangan oqim ham mavjud.
Eng muhim fakt shuki, Python xotirasi JVM heap'idan tashqarida ajratiladi. Numpy ndarray obyektlari, pandas DataFrame obyektlari va joblib modellari JVM uchun noma’lum bo‘lgan native xotira sohalarida joylashadi; -Xmx sozlamalari va GC bu sohalarga ta’sir qilmaydi.
4.2. Initsializatsiya ketma-ketligi: Lazy initsializatsiya va kutubxona qidiruvi
// jep/MainInterpreter.java
protected static synchronized MainInterpreter getMainInterpreter() throws Error {
if (null == instance) {
instance = new MainInterpreter();
instance.initialize();
}
...
}
Bu synchronized kalit so‘zi va lazy initialization pattern'ining kombinatsiyasidir. Python dastur ishga tushganda emas, balki new SharedInterpreter() birinchi marta chaqirilganda initsializatsiya qilinadi va bu jarayon hayotiy sikl davomida faqat bir marta sodir bo‘ladi. Keyingi new SharedInterpreter() chaqiruvlari faqat PyThreadState obyektlarini yaratish uchun javob beradi.
initialize() funksiyasining birinchi bosqichi System.loadLibrary("jep") orqali native kutubxonani yuklashdan iborat bo‘lib, 3.2-bo‘limda tasvirlangan xatolik manbai shudir. U navbatma-navbat -Djava.library.path va LD_LIBRARY_PATH'ni tekshiradi; ikkalasi ham muvaffaqiyatsiz bo‘lsa, LibraryLocator site-packages tarkibini aylanib chiqadi. Yo‘l aniq ko‘rsatilmagan holatda ham tizim ishlashi mumkin, biroq bunday xatti-harakat to‘liq bajarilish muhitining standart sozlamalariga bog‘liq.
Shundan so‘ng Py_Initialize() JepMainInterpreter deb ataladigan alohida oqimda bajariladi. Manba kodidagi izohlarga ko‘ra, buning sababi subinterpreter asosiy interpretator bilan bir oqimda joylashganda yuzaga keladigan GIL muammolarining oldini olishdir. Ushbu oqim cheksiz siklda saqlanadi va tugamaydi. Agar boshqa oqim Python sohasida ishlayotgan paytda bu oqim tugasa, holat buzilishi mumkin. Shu sababli bu oqim Jep ishlatilayotgan har qanday JVM'da jarayon tugaguniga qadar mavjud bo‘ladi; demak, thread-dump tahlili vaqtida uni sizib chiqish deb noto‘g‘ri aniqlamaslik uchun buni oldindan hisobga olish kerak.
4.3. Interpretator modelini tanlash: SharedInterpreter va global holatni ulashish
|
SubInterpreter |
SharedInterpreter |
|
|---|---|---|
|
Asoslangan |
Py_NewInterpreter() |
Asosiy interpretator bilan ulashadi |
|
sys.modules |
Izolyatsiyalangan |
Umumiy |
|
C kengaytma modullari bilan moslik |
Beqaror (ko‘plab Numpy/Scipy komponentlari qo‘llab-quvvatlanmaydi) |
Barqaror |
|
Global holatning ifloslanishi |
Mavjud emas |
Mavjud |
Subinterpreter — bir nechta interpretator holatlarini yaratadigan imkoniyatdir. Biroq C kengaytma moduli global statik o‘zgaruvchilardan foydalanganda, bu holat interpretatorlar bo‘yicha ajratilmaydi. Numpy kabi yirik C kengaytmalari aynan shunday tuzilishga ega, shuning uchun ular subinterpreter muhitida noto‘g‘ri ishlashi yoki jarayonni tugatishi mumkin. PEP 554 va PEP 684 orqali takomillashtirish ishlari olib borilayotgan bo‘lsa-da, ularni joriy stekka qo‘llash qiyin, degan xulosaga kelib, 3.1-bo‘limda SharedInterpreter'ga o‘tdik.
Biroq bu tanlovning o‘z xarajati bor va Javadoc ham bu haqda ogohlantiradi. Modul xatti-harakatini o‘zgartiradigan har qanday amal butun SharedInterpreter'ga ta’sir qiladi; odatiy misollar qatoriga sys.path'ni o‘zgartirish yoki Numpy.seterr()'ni chaqirish kiradi. sys.modules ulashilgani sababli barcha interpretatorlar bir xil sys.path ro‘yxatiga murojaat qiladi va sys.path.append(...) funksiyasini shartni tekshirmasdan takroran chaqirish yo‘llarning to‘planib borishiga olib keladi. Modul yo‘llarini kod ichida emas, balki muhit o‘zgaruvchilari orqali ro‘yxatdan o‘tkazish xavfsizroqdir.
4.4. Thread Affinity: tstate tomonidan qo‘llanadigan cheklovlar
// jep/Jep.java
public void isValidThread() throws JepException {
if (this.thread != Thread.currentThread())
throw new JepException("Invalid thread access.");
...
}
Jep nusxalaridan faqat ular yaratilgan oqimda foydalanish mumkin va barcha ommaviy metodlar kirish vaqtida buni tekshiradi. Ushbu tekshiruv mantiqi 3.1-bo‘limda bayon qilingan nusxalarni qayta ishlatish dizaynining muvaffaqiyatsiz bo‘lishiga bevosita sabab bo‘ladi.
Python interpretatori ichida “ushbu oqim bajarilishda hozir qanchalik ilgarilaganini” saqlaydigan ma’lumotlar tuzilmasi mavjud. Unda bajarilayotgan kodning joylashuvi, istisnolarni qayta ishlash holati va chaqiruvlar steki kabi ma’lumotlar saqlanadi. Bu tuzilma PyThreadState deb ataladi. Muhim jihati shundaki, har bir oqim uchun aynan bittadan shunday tuzilma mavjud bo‘lishi kerak.
Java tomonida Jep ushbu tuzilmaning xotira manzilini maydonda saqlaydi.
private long tstate; // 실제로는 PyThreadState의 메모리 주소
tstate — ushbu maydonning nomi bo‘lib, thread state iborasining qisqartmasidir. Java tilida C ko‘rsatkichiga teng tushuncha mavjud bo‘lmagani sababli, u faqat manzil qiymatini long turi sifatida saqlaydi.
Ushbu qiymatning oqimga xos ekanligi 3.1-bo‘limda tasvirlangan muvaffaqiyatsizlikning asosiy sababidir. Agar boshqa oqim o‘ziga tegishli bo‘lmagan tstate yordamida Python kodini bajarishga urinsa, interpretatorning ichki holati buziladi. Buning oldini olish uchun Jep har bir metodga kirishda chaqiruvchi oqim nusxa yaratilgan oqim bilan mos kelishini tekshiradi. Javadoc hujjatida, shuningdek, bitta oqimda bir nechta interpretatorni bir vaqtning o‘zida faollashtirish imkonsiz ekani aniq ko‘rsatilgan.
Ikki cheklovni — har bir oqimga bittadan va faqat shu oqimda foydalanish — qanoatlantirishning eng sodda usuli nusxani foydalanish joyida yaratib, darhol bo‘shatadigan try-with-resources tuzilmasidan foydalanishdir.
4.5. Global Interpreter Lock (GIL) orqali ketma-ketlashtirish
Jep GIL'ni olib tashlamaydi. Java oqimi jep.eval(...) ni chaqirganda, u GIL'ni egallaydi va qaytishda uni bo‘shatadi. Shuning uchun o‘nlab Java oqimlari bir vaqtning o‘zida uni chaqirsa ham, Python bayt-kodini amalda bir vaqtda faqat bitta oqim bajaradi.
GIL orqali ketma-ketlashtirish xronologiyasi
2-rasm. Worker-1 operatsiyani bajarayotganda, Worker-2 GIL'ni kutadi va natijada bajarilish ketma-ket amalga oshadi
Biroq Numpy'ning ko‘plab og‘ir operatsiyalari GIL'ni ichki darajada bo‘shatadi. Sof Python sikllari to‘liq ketma-ket bajariladi, ammo haqiqiy parallel qayta ishlash ndarray operatsiyalari davomida yuz beradi. Shu sababli hisoblash mantiqini imkon qadar Numpy qatlamiga topshirish maqsadga muvofiq.
Shu bois Python bajarilish yo‘li uchun oqimlar havzasini kengaytirish o‘tkazuvchanlikni oshirmaydi. GIL yuqori chegarani belgilaydi, har bir oqim uchun PyThreadState va globals dict yaratish esa faqat native xotira sarfini oshiradi. CPU ga bog‘liq ishlarni mazmunli parallel bajarish uchun jarayonlarni ajratish talab etiladi.
5. Tuzilmaviy cheklovlardan kelib chiqadigan uchta ekspluatatsion tamoyil
5.1. Konteyner xotirasi byudjetini qayta hisoblash
4.1-bo‘limda ko‘rilganidek, Python qatlami foydalanadigan xotira JVM heap'idan tashqarida joylashadi. -XX:MaxRAMPercentage=80 odatiy Spring Boot konteyneri uchun maqbul, ammo Jep xizmatida bu Python interpretatori, modullar, ndarray obyektlari va ML modellari qolgan 20% ichiga sig‘ishi kerakligini anglatadi. Byudjet oshib ketsa, OutOfMemoryError yuzaga kelishining o‘rniga yadroning OOM Killer mexanizmi konteynerni to‘xtatadi va heap dump'siz faqat 137 chiqish kodi kuzatiladi. Mos qiymat Python tomoni amalda qancha xotiradan foydalanishiga bog‘liq, shuning uchun yagona maqbul yondashuv monitoring vositalari yordamida konteynerning haqiqiy xotira sarfini bevosita o‘lchash va shu asosda teskari hisoblashdir.
5.2. Interpretator ishlash muddatini ish birligi bilan moslashtirish
3.1-bo‘limda tasvirlangan yondashuv oqimga bog‘liqlik muammosini hal qildi, ammo yana bir muammo qoldi: har bir chaqiruv uchun interpretator yaratish modul importi xarajatining har bir chaqiruvda takrorlanishiga olib keladi. Interpretatorning ishlash muddatini alohida chaqiruv bilan emas, balki mantiqiy Unit of Work bilan moslashtirish maqsadga muvofiq.
// getJep(): SharedInterpreter 생성 + 모듈 import
try (SharedInterpreter jep = this.getJep()) {
flowA.createBaseline(jep, targetId, param);
flowB.createBaseline(jep, targetId, param);
flowC.createBaseline(jep, targetId, param);
}
Nusxa maydonda saqlanmagani sababli oqimga bog‘liqlik cheklovi saqlanadi, boshlang‘ich sozlash xarajati esa paket bo‘ylab taqsimlanadi.
5.3. Zararning tarqalish doirasini oldindan loyihalash
Agar C kengaytmasi kodi yaroqsiz xotira sohasiga murojaat qilsa — bu holat odatda segfault deb ataladi — Java istisnosi yuzaga kelishining o‘rniga jarayonning o‘zi to‘xtaydi. Bu holatni try-catch yordamida tutib bo‘lmaydi va JVM qatlamida tiklanishning hech qanday usuli mavjud emas. Python chaqiruvlarini bajaradigan xizmatni sof domen xizmatlaridan ajratish orqali bunday hodisaning ta’sir doirasini shu xizmat bilan cheklash mumkin.
6. Xulosa: Embedding emas, balki birgalikda ishlashdan olingan saboqlar
Ushbu joriy etish jarayonida olingan eng qimmatli saboq shuki, “Embedding atamasi bir runtime ikkinchisini boshqaradigan tuzilmani anglatadigandek tuyulsa-da, amalda bu bir jarayon ichida ikkita mustaqil runtime birgalikda mavjud bo‘lishini anglatadi”.
Ushbu faktni anglamasdan qabul qilingan ikki qaror 3-bo‘limda tasvirlangan muvaffaqiyatsizliklarga olib keldi. Jep nusxalarini connection-pool obyektlari kabi qayta ishlatish native ko‘rsatkichlarni saqlovchi obyektlarga JVM obyektlarining ishlash muddati haqidagi yondashuvni bevosita tatbiq etish natijasi bo‘ldi. Kutubxona yo‘lini muhitdagi standart qiymatga topshirish esa native bog‘liqliklar JVM ilovasining odatiy joylashtirish chegarasidan tashqarida ekanini e’tibordan chetda qoldirish natijasi bo‘ldi. Har ikkala holatda ham loyihalash vaqtida noto‘g‘ri dizaynni to‘xtatadigan hech narsa mavjud emas edi; ogohlantiruvchi belgilar faqat noto‘g‘ri implementatsiya yaratilgandan so‘ng, runtime vaqtida paydo bo‘ldi.
Men Jep'ni qabul qilishning o‘zi to‘g‘ri tanlov bo‘lgan degan xulosaga keldim. Ularni qayta yozmasdan, sinalgan Python aktivlarini integratsiya qilish va domen mutaxassislari formulalarga kiritgan o‘zgartirishlarni darhol aks ettirish mumkin bo‘lgan tuzilmani saqlab qolish imkonini berdi. Biroq buning evaziga xotira, oqimlar va joylashtirish modellari boshidan qayta loyihalanishi kerak bo‘ldi. Ushbu tajribadan kelib chiqadigan xulosa shuki, ishlab chiqarishdagi nosozliklar sabablarini aniqlash uchun qulaylik ortidagi ekspluatatsion mexanizmlarni avvalambor tushunish muhimdir.
Pancake Maker