| | 1 | == Други теми - перформанси и безбедност |
| | 2 | |
| | 3 | == Мерна поставеност |
| | 4 | |
| | 5 | Анализата на перформанси не може да се направи врз официјалните тест податоци од |
| | 6 | data_load.sql, бидејќи тие се премногу мали - 8 корисници и 20 предмети. Врз |
| | 7 | табела со 20 редови PostgreSQL речиси секогаш избира секвенцијално читање дури и |
| | 8 | кога постои совршен индекс, затоа што читањето на 20 редови е поевтино од |
| | 9 | пребарување низ индекс. Мерењето врз такви податоци не би покажало ништо. |
| | 10 | |
| | 11 | Затоа е креирана посебна шема '''perf''' со иста структура како project, но со |
| | 12 | генерирани податоци од реален обем: |
| | 13 | |
| | 14 | || '''Табела''' || '''Редови''' || |
| | 15 | || users || 4.120 (4.000 студенти, 120 професори) || |
| | 16 | || subjects || 300 || |
| | 17 | || active_semesters || 20 || |
| | 18 | || major_subjects || 600 || |
| | 19 | || professour_subjects || 6.000 || |
| | 20 | || enrolled_semesters || 16.000 || |
| | 21 | || semesters_subjects || 80.000 || |
| | 22 | || passed_subjects || 56.000 || |
| | 23 | || payment || 16.000 || |
| | 24 | |
| | 25 | Упатството за Фаза 2 дозволува креирање дополнителни шеми за експерименти и |
| | 26 | тестови, па официјалната шема project останува недопрена. |
| | 27 | |
| | 28 | Сите мерења се направени со {{{EXPLAIN (ANALYZE, BUFFERS)}}} и со {{{ANALYZE}}} |
| | 29 | извршен пред секое мерење, за статистиките на планерот да бидат свежи. |
| | 30 | |
| | 31 | '''Забелешка за околината:''' мерењата се извршени врз локална инстанца |
| | 32 | PostgreSQL 18, а не врз доделената проектна база (PostgreSQL 17.11). Причината е |
| | 33 | што проектната база е достапна само преку SSH тунел кој во текот на работата |
| | 34 | постојано се прекинуваше, а мерење на времиња низ тунел и онака би ја мерело |
| | 35 | мрежата наместо базата. Плановите за извршување се споредливи, бидејќи истата |
| | 36 | шема, истите барања и истите податоци се употребени во двата случаја. |
| | 37 | |
| | 38 | == SQL перформанси |
| | 39 | |
| | 40 | === Предложени индекси |
| | 41 | |
| | 42 | PostgreSQL автоматски креира индекс за примарен клуч и за секое UNIQUE |
| | 43 | ограничување, но '''никогаш''' не креира индекс за колоната која е надворешен |
| | 44 | клуч. Освен тоа, сложен индекс помага само на барања кои ја користат неговата |
| | 45 | '''прва''' колона. |
| | 46 | |
| | 47 | Според тоа, во шемата недостасуваат индекси на следните места: |
| | 48 | |
| | 49 | {{{#!sql |
| | 50 | CREATE INDEX ix_semesters_subjects_subject ON semesters_subjects (subjects_id); |
| | 51 | CREATE INDEX ix_semesters_subjects_professor ON semesters_subjects (professor_id); |
| | 52 | CREATE INDEX ix_enrolled_semesters_semester ON enrolled_semesters (semester_id); |
| | 53 | CREATE INDEX ix_enrolled_semesters_major ON enrolled_semesters (major_id); |
| | 54 | CREATE INDEX ix_payment_enrollment ON payment (enrollment_id); |
| | 55 | CREATE INDEX ix_major_subjects_subject ON major_subjects (subject_id); |
| | 56 | CREATE INDEX ix_professour_subjects_semester ON professour_subjects (active_semester_id, subject_id); |
| | 57 | CREATE INDEX ix_user_documents_document ON user_documents (document_id); |
| | 58 | CREATE INDEX ix_token_user ON token (user_id); |
| | 59 | CREATE INDEX ix_passed_subjects_enrolled_grade ON passed_subjects (enrolled_id, grade); |
| | 60 | }}} |
| | 61 | |
| | 62 | Колоните semesters_subjects.enrolled_semesters_id, enrolled_semesters.user_id, |
| | 63 | passed_subjects.enrolled_id, high_school.user_id и contact.user_id '''не''' се на |
| | 64 | списокот, бидејќи веќе се покриени - тие се првата колона на постоечко UNIQUE |
| | 65 | ограничување. |
| | 66 | |
| | 67 | === Резултати пред и по индексите |
| | 68 | |
| | 69 | || '''Барање''' || '''Пред''' || '''По''' || '''Промена''' || |
| | 70 | || Извештај 1 - проодност по предмет || 146,0 ms || 156,0 ms || нема || |
| | 71 | || Извештај 2 - досие на сите студенти || 188,5 ms || 179,4 ms || нема || |
| | 72 | || Извештај 3 - најуспешен студент по програма || 107,3 ms || 94,0 ms || нема || |
| | 73 | || Извештај 4 - најоптоварен професор || 121,9 ms || 124,6 ms || нема || |
| | 74 | || Извештај 5 - промена на активноста || 124,0 ms || 138,1 ms || нема || |
| | 75 | || Досие на '''еден''' студент || 2,19 ms || '''1,07 ms''' || '''2,0 пати побрзо''' || |
| | 76 | || Дневник на професор за еден професор || 16,3 ms || '''7,6 ms''' || '''2,1 пати побрзо''' || |
| | 77 | |
| | 78 | === Зошто петте извештаи не станаа побрзи |
| | 79 | |
| | 80 | Ова не е неуспех на индексите, туку очекувано однесување. Петте извештаи од Фаза |
| | 81 | 6 се агрегатни - тие читаат '''сè''' од semesters_subjects, passed_subjects и |
| | 82 | enrolled_semesters, за да пресметаат проодност, просек или ранг. Кога барањето и |
| | 83 | онака мора да ги прочита сите редови, секвенцијалното читање е побрзо од |
| | 84 | пребарување низ индекс, бидејќи чита последователни блокови од дискот наместо да |
| | 85 | скока по индексот и потоа по табелата. |
| | 86 | |
| | 87 | Дека разликите во табелата се шум, а не забавување, се гледа од повторените |
| | 88 | мерења на истото барање со исти индекси: |
| | 89 | |
| | 90 | {{{ |
| | 91 | run 1: 164,0 ms |
| | 92 | run 2: 151,5 ms |
| | 93 | run 3: 162,7 ms |
| | 94 | run 4: 179,9 ms |
| | 95 | }}} |
| | 96 | |
| | 97 | Распонот од околу 28 ms меѓу четири последователни извршувања е поголем од сите |
| | 98 | разлики „пред/по“ во табелата. Значи индексите за овие барања не менуваат ништо - |
| | 99 | ниту на добро, ниту на лошо. |
| | 100 | |
| | 101 | === Каде индексите навистина помогнаа |
| | 102 | |
| | 103 | Кај двете '''селективни''' барања - оние кои бараат податоци за еден студент или |
| | 104 | за еден професор - разликата е јасна и се гледа во самиот план. |
| | 105 | |
| | 106 | '''Досие на еден студент''' (2,19 ms → 1,07 ms). Планот пред индексите содржеше: |
| | 107 | |
| | 108 | {{{ |
| | 109 | Seq Scan on payment |
| | 110 | }}} |
| | 111 | |
| | 112 | а по индексите: |
| | 113 | |
| | 114 | {{{ |
| | 115 | Index Scan using ix_payment_enrollment |
| | 116 | }}} |
| | 117 | |
| | 118 | '''Дневник на професор''' (16,3 ms → 7,6 ms). Пред индексот: |
| | 119 | |
| | 120 | {{{ |
| | 121 | Seq Scan on semesters_subjects (80.000 реда прочитани, филтрирани на ~660) |
| | 122 | }}} |
| | 123 | |
| | 124 | По индексот: |
| | 125 | |
| | 126 | {{{ |
| | 127 | Bitmap Heap Scan on semesters_subjects |
| | 128 | -> Bitmap Index Scan using ix_semesters_subjects_professor |
| | 129 | }}} |
| | 130 | |
| | 131 | Наместо да ги прочита сите 80.000 реда и да ги отфрли 99%, базата сега оди |
| | 132 | директно до редовите на тој професор. |
| | 133 | |
| | 134 | === Дали индексите навистина се употребени |
| | 135 | |
| | 136 | Бројачите од pg_stat_user_indexes по сите мерења: |
| | 137 | |
| | 138 | || '''Индекс''' || '''Големина''' || '''Пати употребен''' || |
| | 139 | || ix_passed_subjects_enrolled_grade || 1248 kB || 1410 || |
| | 140 | || ix_payment_enrollment || 368 kB || 8 || |
| | 141 | || ix_semesters_subjects_subject || 584 kB || 8 || |
| | 142 | || ix_semesters_subjects_professor || 576 kB || 1 || |
| | 143 | || ix_enrolled_semesters_major || 128 kB || '''0''' || |
| | 144 | || ix_enrolled_semesters_semester || 136 kB || '''0''' || |
| | 145 | || ix_major_subjects_subject || 32 kB || '''0''' || |
| | 146 | || ix_professour_subjects_semester || 152 kB || '''0''' || |
| | 147 | || ix_token_user || 8 kB || '''0''' || |
| | 148 | || ix_user_documents_document || 48 kB || '''0''' || |
| | 149 | |
| | 150 | Од десет предложени индекси, '''четири''' се навистина употребени, а '''шест''' |
| | 151 | ниту еднаш. Тоа е важен резултат: индекс кој не се употребува не е бесплатен - |
| | 152 | зазема простор и го забавува секој INSERT, UPDATE и DELETE врз таа табела, |
| | 153 | бидејќи и индексот мора да се ажурира. |
| | 154 | |
| | 155 | === Заклучок |
| | 156 | |
| | 157 | * Индексите врз надворешни клучеви '''не ги забрзуваат''' агрегатните извештаи од Фаза 6, бидејќи тие и онака ја читаат целата табела. Секвенцијалното читање таму е правилниот избор на планерот. |
| | 158 | * Истите индекси ги забрзуваат '''селективните''' барања - оние што ги користи апликацијата - околу '''два пати''', со видлива промена на планот од Seq Scan во Index Scan. |
| | 159 | * Бидејќи апликацијата работи со селективни барања (еден студент, еден професор), а извештаите се пуштаат ретко, индексите се исплатливи. |
| | 160 | * Се задржуваат само четирите употребени индекси. Останатите шест се отстрануваат, со една резерва: индекс врз надворешен клуч помага и при бришење во родителската табела, па ix_enrolled_semesters_semester и ix_major_subjects_subject може да се задржат ако се предвидуваат такви операции. |
| | 161 | * Мерењето врз 20 реда не докажува ништо. Секое тврдење за перформанси мора да се прави врз податоци со реален обем. |
| | 162 | |
| | 163 | == Безбедносни мерки |
| | 164 | |
| | 165 | === Во апликацијата |
| | 166 | |
| | 167 | '''Спречување на SQL injection.''' Апликацијата '''воопшто не составува SQL од |
| | 168 | текст'''. Проверено врз целиот изворен код - нема ниту еден повик на !FromSqlRaw, |
| | 169 | !ExecuteSqlRaw, !NpgsqlCommand ниту рачно поставен !CommandText. Целиот пристап оди |
| | 170 | преку Entity Framework Core, кој секоја вредност ја испраќа како параметар. Единственото |
| | 171 | место со напишан SQL е проверката на врската: |
| | 172 | |
| | 173 | {{{#!sql |
| | 174 | .SqlQuery<string>($"SELECT version() AS \"Value\"") |
| | 175 | }}} |
| | 176 | |
| | 177 | Тоа е интерполиран стринг во C#, но EF Core таквите изрази ги претвора во |
| | 178 | параметризирани наредби, а не во спојување на текст - вредностите никогаш не |
| | 179 | влегуваат во телото на барањето. |
| | 180 | |
| | 181 | '''Лозинки.''' Не се чуваат во читлива форма. При регистрација се хешираат со |
| | 182 | BCrypt, а при најава се споредуваат со Verify: |
| | 183 | |
| | 184 | {{{ |
| | 185 | PasswordHash = BCrypt.Net.BCrypt.HashPassword(registerDto.Password) |
| | 186 | BCrypt.Net.BCrypt.Verify(loginDto.Password, user.PasswordHash) |
| | 187 | }}} |
| | 188 | |
| | 189 | BCrypt носи сопствена сол и намерно е бавен, што го отежнува пробувањето лозинки. |
| | 190 | |
| | 191 | '''Автентикација и авторизација.''' Пристапот е со JWT токен со краток рок; |
| | 192 | подолгата сесија се одржува со refresh токен кој се чува во табелата token и се |
| | 193 | поништува при одјава (is_valid = FALSE) наместо да се брише, за да остане трага. |
| | 194 | Улогата се чита '''од потпишаниот токен''', не од барањето, па повикувачот не |
| | 195 | може да си ја додели. |
| | 196 | |
| | 197 | '''Авторизацијата е во самото барање, не покрај него.''' Наместо прво да се |
| | 198 | провери правото, па потоа да се изврши барањето, условот е дел од наредбата: |
| | 199 | |
| | 200 | {{{#!sql |
| | 201 | INSERT INTO passed_subjects (enrolled_id, grade, date_passed) |
| | 202 | SELECT ss.id, '9', now() |
| | 203 | FROM semesters_subjects ss |
| | 204 | JOIN enrolled_semesters es ON es.id = ss.enrolled_semesters_id |
| | 205 | WHERE ss.professor_id = 4 AND ss.subjects_id = 2 AND es.user_id = 1; |
| | 206 | }}} |
| | 207 | |
| | 208 | Ако професорот не го предава тој предмет на тој студент, наредбата не менува |
| | 209 | ниту еден ред. Нема временски прозорец меѓу проверката и дејството. |
| | 210 | |
| | 211 | Проверено со барања од туѓа улога: студент кон професорски краен точки враќа |
| | 212 | '''403''', професор кон административни '''403''', барање без токен '''401''', а |
| | 213 | професор кој се обидува да оцени предмет што не го предава добива одбивање без |
| | 214 | никаква измена во базата. |
| | 215 | |
| | 216 | === Во базата |
| | 217 | |
| | 218 | '''Правила кои важат и при директен пристап.''' Проверките од Фаза 7 се тригери |
| | 219 | во базата, а не само во апликацијата: предметот мора да е во студиската програма, |
| | 220 | професорот мора да го предава тој семестар, предусловите мора да се положени, |
| | 221 | оценка не смее да има датум во иднина. Кој и да пишува во базата - апликацијата, |
| | 222 | скрипта или човек преку DBeaver - правилата важат. |
| | 223 | |
| | 224 | '''Домени.''' Форматите се дел од типот на колоната, па невалидна е-пошта или ЕМБГ |
| | 225 | не може да влезе ниту преку директен INSERT. |
| | 226 | |
| | 227 | '''Изолација на податоци меѓу факултети.''' Со политиките за безбедност на ниво |
| | 228 | на редица (Фаза 7), барањата автоматски се ограничуваат на тековниот факултет. |
| | 229 | |
| | 230 | '''Отворено прашање.''' Апликацијата се поврзува со корисникот кој е сопственик |
| | 231 | на шемата, а сопственикот ги '''заобиколува''' политиките за безбедност на ниво |
| | 232 | на редица, освен ако не се употреби FORCE ROW LEVEL SECURITY. Значи политиките |
| | 233 | во моментов се напишани и точни, но не се активни за апликацијата. Правилното |
| | 234 | решение е посебна улога со ограничени права: |
| | 235 | |
| | 236 | {{{#!sql |
| | 237 | CREATE ROLE iknow_app LOGIN PASSWORD '...'; |
| | 238 | GRANT USAGE ON SCHEMA project TO iknow_app; |
| | 239 | GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA project TO iknow_app; |
| | 240 | -- без DROP, без ALTER, без права врз други шеми |
| | 241 | }}} |
| | 242 | |
| | 243 | Ова не е спроведено, бидејќи доделениот кориснички налог на проектната база нема |
| | 244 | право да креира нови улоги. |
| | 245 | |
| | 246 | '''Лозинки во конфигурација.''' Лозинката за базата не е во складиштето на кодот |
| | 247 | - таа е во appsettings.Development.json, кој е во .gitignore, а во складиштето |
| | 248 | постои само appsettings.Development.json.example со празно место. |
| | 249 | |
| | 250 | == Други развојни теми |
| | 251 | |
| | 252 | '''Материјализиран преглед.''' Статистиката по предмети (Фаза 7) е пресметка врз |
| | 253 | целата база - точно оној вид агрегатно барање кое, како што покажа мерењето |
| | 254 | погоре, не може да се забрза со индекс. Затоа е материјализирана и се освежува |
| | 255 | еднаш дневно, со уникатен индекс кој овозможува освежување со CONCURRENTLY, |
| | 256 | односно без заклучување за читање. |
| | 257 | |
| | 258 | '''Pool на конекции.''' Документиран во Фаза 8; тука е релевантен затоа што |
| | 259 | базата е зад SSH тунел, па секоја нова конекција е нов канал низ тунелот и |
| | 260 | отворањето е поскапо од вообичаеното. |
| | 261 | |
| | 262 | == Користење на вештачка интелигенција |
| | 263 | |
| | 264 | [wiki:OtherTopicsAIUsage OtherTopicsAIUsage] |
| | 265 | |
| | 266 | == Историјат |
| | 267 | |
| | 268 | '''Верзија 1''' - Прва верзија: мерење на перформансите на петте извештаи од Фаза |
| | 269 | 6 пред и по воведување индекси врз шема со 80.000 запишани предмети, анализа на |
| | 270 | плановите за извршување, проверка кои индекси навистина се употребуваат, и |
| | 271 | документирање на безбедносните мерки во апликацијата и во базата. |
| | 272 | |
| | 273 | == Статус |
| | 274 | |
| | 275 | ''' [[span(style=color: #FF8000, Во тек )]] ''' |