Оригиналът е на Nitzan Raz
В предишната си работа поддържах онлайн услуга на Java, която осигуряваше необходимата функционалност на отдалечен потребителски интерфейс, подобна на RDP или Citrix. Тази услуга се базираше на сесии, състоящи се от взаимосвързани Java обекти, които трябваше да бъдат изчистени или когато потребителят излезе от системата, или когато изтече определено време.
На етапа на планиране на натоварването открихме значителни загуби на памет, причините за които бих искал да разгледам в тази статия.
Планиране на натоварването
Част от ежедневната ми работа с екипа бе да планирам работното натоварване за следващата година. Анализирайки показателите за използване, моделите на растеж и проучванията на населението, нашите специалисти по данни можеха да предвидят колко потребители ще имаме през следващата година.
За да определим инфраструктурата, необходима за поддържане на очакваната потребителска база, използвахме следната „изключително сложна“ формула:
По този начин определихме броя на сървърите, които ще са ни необходими през следващата година.
На една от срещите за планиране на натоварването стана ясно, че поради огромната популярност на услугата очакваме значително увеличение на броя на потребителите. Изчисленията ни показаха, че за да отговорим на повишеното търсене, ще ни трябват повече сървъри, отколкото имаме. Така че бяхме изправени пред предизвикателството да разберем как да поберем колкото се може повече потребители на един и същ сървър, за да поддържаме очакваната потребителска база.
В какво сме ограничени?
Чрез измерване на натоварването успяхме да определим тясното място в системата, което в случая се оказа паметта. Когато към сървъра бяха добавени нови потребители, системата започна да се срива при повишеното натоварване и в крайна сметка паметта свършваше. Разбирането, че имаме недостиг на памет, бе от решаващо значение, защото насочи усилията ни към намаляване на потреблението на памет.
Проучване на използването на паметта
Изчислихме приблизителна оценка на потреблението на памет за всеки потребител, като използвахме следната формула:
Използвайки числата от нашия анализ получихме следното:
Това означава, че всеки потребител се нуждае от приблизително 300 MB памет. За да разберем как да намалим това число, направихме сериозни измервания на използваната памет.
За да идентифицираме потенциалните подобрения, започнахме с анализ на дъмповете на паметта на Java. Първоначално изследвахме дъмповете ръчно, но поради големия брой сървъри се наложи да разработим скрипт за оптимизиране на процеса. С помощта на този скрипт успяхме да идентифицираме обектите, които разхищават твърде много памет, свързани с конкретни сесии. Чрез откриването на подобни проблеми успяхме да се избавим от ненужното разхищение и да оптимизираме използването на паметта в нашата система.
Може би в друга публикация ще разкажа за скрипта и анализа, но засега бих искал да разгледам по-отблизо тази съвсем лесна победа, която ни даде анализът на паметта.
Много големият низ
Започнахме с проучването на хилядите дъмпове на паметта, търсейки много големи обекти. Най-големият „кит“ се оказа низ от 1,5 GB. Той изглеждаше по следния начин:
Както можете да видите на изображението, низът съдържа много символи с обратна наклонена черта. Намерихме още много подобни низове с по-малък размер, но този бе най-големият.
Докато изучавах предназначението на този низ забелязах, че имаме класове, които изглеждат по следния начин:
class Screen {
//...
private Screen previous;
public String toJson() {
JSONObject jo = new JSONObject();
//...
if (previous != null) {
jo.put("previous", previous.toJson());
}
//...
return jo.toString();
}
}
class Session {
//...
String currentScreen;
public void setUrl(Screen s) {
currentScreen = s.toJson();
}
}
Това означава, че всеки програмен прозорец има предишен прозорец, който е посетен от потребителя. Това дава възможност на потребителя да се върне назад точно към прозореца, на който се е намирал преди това (със запазени позиция, състояние, място на скролването и т.н.). Също така потребителската сесия има и текущ прозорец, на който се намира потребителят и ако той се опита повторно да се присъедини към вече съществуваща сесия, той може да се върне към този прозорец, в който в момента се намира.
Тук възникват две бих казал, архитектурни проблеми:
- Стекът на предишните прозорци може да се счита за на практика неограничен – тоест, ние генерираме и записваме все повече данни, докато сървърът не гръмне
- Изпълнявайки jo.put(„previous“, previous.toJson());, ние преобразуваме JSON речника в низ. И тъй като JSON полетата съдържат кавички , а тези кавички при запис във вид на низ е необходимо да бъдат съчетани със символа за преход, то те се записват като \“. Този обратен слеш се съчетава със символа за преход, който си е същият символ и по този начин получаваме \\\“. Само след две подобни повторения вече имаме \\\\\\\\\\\\\\\\“.
Оказа се, че потребител със сесия, в който има множество програмни прозорци, създава String currentScreen с огромни пропорции.
Решението на проблема
Разделихме проблема на бързо и дългосрочно решения:
Бързото решение бе обикновеното орязване на низовете от предишните програмни прозорци в случай на превишаване на определен, определен от нас брой символи – например 100 MB. Въпреки че това решение е непълно има вероятност да влоши UX, неговата реализация е лесна и бърза, освен това е съвсем лесно за тестване. Това веднага повиши надеждността на нашата услуга, като изцяло предотврати ситуацията, в която дадена сесия може да използва прекалено много памет и да претовари сървъра.
Дългосрочното решение бе да пренапишем изцяло решението за стека на предишните екрани: създадохме отделен реален стек, който имаше вътрешни ограничения за размера и собствена отчетност. Написването и тестването му отнеха повече време, както и пускането му в експлоатация, но то предотврати разхищението на памет, вместо просто да скрие „китовите“ низове като друг тип памет (т.е. много дълбоки JSON обекти).
Епилог
Продължихме да използваме инструмента за анализ на дъмповете на паметта и открихме други проблеми, но нито един от тях не беше решен толкова лесно, колкото този.
За мен основният извод от тази история е, че понякога проверката на подробностите за използването на ресурсите на дадена програма (напр. преглед на дъмповете памет, а не само измерване на използването на паметта) е от решаващо значение за успеха и дава възможност за незабавни подобрения и облаги.
Всичко важно от света на технологиите, директно в пощата ти.
С абонирането приемате нашите Условия и Политика за поверителност. Може да се отпишете с един клик по всяко време.
Коментирайте статията в нашите Форуми. За да научите първи най-важното, харесайте страницата ни във Facebook, и ни последвайте в Google Новини, TikTok, Telegram и Viber или изтеглете приложението на Kaldata.com за Android, iPhone, Huawei, Google Chrome, Microsoft Edge и Opera!



