Իմ ամենավերջին աշխատանքային հարցազրույցներից մեկից հետո ես զարմացա, երբ հասկացա, որ ընկերությունը, որի համար ես դիմել էի, դեռ օգտագործում էր Laravel՝ PHP շրջանակ, որը ես փորձեցի մոտ մեկ տասնամյակ առաջ: Ժամանակի համար դա պարկեշտ էր, բայց եթե տեխնոլոգիայի և նորաձևության մեջ կա մեկ հաստատուն, դա ոճերի և գաղափարների շարունակական փոփոխությունն ու վերարտադրումը: Եթե դուք JavaScript ծրագրավորող եք, հավանաբար ծանոթ եք այս հին կատակին.
Ծրագրավորող 1. «Ինձ դուր չի գալիս այս նոր JavaScript շրջանակը:«
Ծրագրավորող 2. «Անհանգստանալու կարիք չկա: Պարզապես սպասեք վեց ամիս, և կլինի ևս մեկը, որը կփոխարինի այն:"
Հետաքրքրությունից դրդված որոշեցի տեսնել, թե կոնկրետ ինչ է տեղի ունենում, երբ փորձության ենք ենթարկում հինն ու նորը: Իհարկե, համացանցը լցված է հենանիշերով և պահանջներով, որոնցից ամենահայտնին, հավանաբար, այն է TechEmpower Web Framework-ի հենանիշերն այստեղ . Այնուամենայնիվ, մենք չենք պատրաստվում անել այնպիսի բարդ բան, որքան նրանք այսօր: Մենք կպահենք ամեն ինչ գեղեցիկ և պարզ, որպեսզի այս հոդվածը չվերածվի. Պատերազմ և խաղաղություն , և որ դուք մի փոքր հնարավորություն կունենաք արթուն մնալու մինչև կարդալը ավարտեք: Կիրառվում են սովորական նախազգուշացումները. սա կարող է նույնը չաշխատել ձեր մեքենայի վրա, ծրագրաշարի տարբեր տարբերակները կարող են ազդել աշխատանքի վրա, և Շրյոդինգերի կատուն իրականում դարձել է զոմբի կատու, որը կիսով չափ կենդանի էր և կիսով չափ մեռած:
Այս թեստի համար ես կօգտագործեմ իմ նոութբուքը, որը զինված է Manjaro Linux-ով աշխատող մանր i5-ով, ինչպես ցույց է տրված այստեղ:
╰─➤ uname -a
Linux jimsredmi 5.10.174-1-MANJARO #1 SMP PREEMPT Tuesday Mar 21 11:15:28 UTC 2023 x86_64 GNU/Linux
╰─➤ cat /proc/cpuinfo
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 126
model name : Intel(R) Core(TM) i5-1035G1 CPU @ 1.00GHz
stepping : 5
microcode : 0xb6
cpu MHz : 990.210
cache size : 6144 KB
Մեր կոդը յուրաքանչյուր հարցման համար կունենա երեք պարզ առաջադրանք.
Ինչպիսի՞ հիմար թեստ է, կարող եք հարցնել: Դե, եթե նայեք այս էջի ցանցային հարցումներին, ապա կնկատեք մեկին, որը կոչվում է sessionvars.js, որն անում է ճիշտ նույն բանը:
Տեսեք, ժամանակակից վեբ էջերը բարդ արարածներ են, և ամենատարածված խնդիրներից մեկը բարդ էջերի քեշավորումն է՝ տվյալների բազայի սերվերի վրա ավելորդ բեռից խուսափելու համար:
Եթե մենք վերաարտադրենք բարդ էջը ամեն անգամ, երբ օգտատերը դա պահանջում է, ապա մենք կարող ենք սպասարկել վայրկյանում ընդամենը 600 օգտվողի:
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1/system/index.en.html
Running 10s test @ http://127.0.0.1/system/index.en.html
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 186.83ms 174.22ms 1.06s 81.16%
Req/Sec 166.11 58.84 414.00 71.89%
6213 requests in 10.02s, 49.35MB read
Requests/sec: 619.97
Transfer/sec: 4.92MB
Բայց եթե այս էջը քեշավորենք որպես ստատիկ HTML ֆայլ և թույլ տանք, որ Nginx-ը արագորեն պատուհանից դուրս նետի այն օգտագործողին, ապա մենք կարող ենք վայրկյանում սպասարկել 32000 օգտատերերի՝ բարձրացնելով արդյունավետությունը 50 անգամ:
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1/system/index.en.html
Running 10s test @ http://127.0.0.1/system/index.en.html
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 3.03ms 511.95us 6.87ms 68.10%
Req/Sec 8.20k 1.15k 28.55k 97.26%
327353 requests in 10.10s, 2.36GB read
Requests/sec: 32410.83
Transfer/sec: 238.99MB
Ստատիկ index.en.html-ն այն մասն է, որը գնում է բոլորին, և միայն այն մասերը, որոնք տարբերվում են ըստ օգտվողի, ուղարկվում են sessionvars.js-ում: Սա ոչ միայն նվազեցնում է տվյալների բազայի ծանրաբեռնվածությունը և ավելի լավ փորձ է ստեղծում մեր օգտատերերի համար, այլ նաև նվազեցնում է քվանտային հավանականությունը, որ մեր սերվերը ինքնաբերաբար գոլորշիացվելու է աղավաղված միջուկի խախտման ժամանակ, երբ Klingons հարձակմանը:
Յուրաքանչյուր շրջանակի համար վերադարձված կոդը կունենա մեկ պարզ պահանջ՝ օգտագործողին ցույց տալ, թե քանի անգամ է նա թարմացրել էջը՝ ասելով «Հաշիվը x է»: Ամեն ինչ պարզ պահելու համար մենք առայժմ հեռու կմնանք Redis-ի հերթերից, Kubernetes-ի բաղադրիչներից կամ AWS Lambdas-ից:
Յուրաքանչյուր օգտվողի սեսիայի տվյալները կպահվեն PostgreSQL տվյալների բազայում:
Եվ այս տվյալների բազայի աղյուսակը կկտրվի յուրաքանչյուր թեստից առաջ:
Պարզ, բայց արդյունավետ է Pafera-ի կարգախոսը... ամենևին էլ ամենամութ ժամանակացույցից դուրս...
Լավ, այնպես որ հիմա մենք վերջապես կարող ենք սկսել կեղտոտել մեր ձեռքերը: Մենք բաց կթողնենք Laravel-ի կարգավորումները, քանի որ այն ընդամենը կոմպոզիտորների և արհեստավորների մի խումբ է: հրամաններ.
Նախ, մենք կկարգավորենք մեր տվյալների բազայի կարգավորումները .env ֆայլում
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=sessiontest
DB_USERNAME=sessiontest
DB_PASSWORD=sessiontest
Այնուհետև մենք կսահմանենք մեկ հետադարձ երթուղի, որը յուրաքանչյուր հարցում կուղարկի մեր վերահսկիչին:
Route::fallback(SessionController::class);
Եվ դրեք կարգավորիչը, որպեսզի ցուցադրի հաշվարկը: Laravel-ը լռելյայնորեն պահում է նիստերը տվյալների բազայում: Այն նաև ապահովում է session()
մեր սեսիայի տվյալների հետ ինտերֆեյսի գործառույթը, այնպես որ ընդամենը մի քանի տող կոդ պահանջվեց մեր էջը մատուցելու համար:
class SessionController extends Controller
{
public function __invoke(Request $request)
{
$count = session('count', 0);
$count += 1;
session(['count' => $count]);
return 'Count is ' . $count;
}
}
php-fpm և Nginx-ը կարգավորելուց հետո մեր էջը բավականին լավ տեսք ունի...
╰─➤ php -v
PHP 8.2.2 (cli) (built: Feb 1 2023 08:33:04) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.2.2, Copyright (c) Zend Technologies
with Xdebug v3.2.0, Copyright (c) 2002-2022, by Derick Rethans
╰─➤ sudo systemctl restart php-fpm
╰─➤ sudo systemctl restart nginx
Համենայն դեպս մինչև մենք իրականում տեսնենք թեստի արդյունքները...
PHP/Laravel
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1
Running 10s test @ http://127.0.0.1
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.08s 546.33ms 1.96s 65.71%
Req/Sec 12.37 7.28 40.00 56.64%
211 requests in 10.03s, 177.21KB read
Socket errors: connect 0, read 0, write 0, timeout 176
Requests/sec: 21.04
Transfer/sec: 17.67KB
Ոչ, դա տառասխալ չէ։ Մեր թեստային մեքենան վայրկյանում 600 հարցումից՝ բարդ էջի արտապատկերման համար... մինչև 21 հարցում/վայրկյանում՝ «Հաշիվը 1 է»:
Այսպիսով, ինչն է սխալվել: Ինչ-որ բան այն չէ՞ մեր PHP տեղադրման հետ: Արդյո՞ք Nginx-ը ինչ-որ կերպ դանդաղում է php-fpm-ի հետ շփվելիս:
Եկեք վերամշակենք այս էջը մաքուր PHP կոդով:
<?php
// ====================================================================
function uuid4()
{
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
// ====================================================================
function Query($db, $query, $params = [])
{
$s = $db->prepare($query);
$s->setFetchMode(PDO::FETCH_ASSOC);
$s->execute(array_values($params));
return $s;
}
// ********************************************************************
session_start();
$sessionid = 0;
if (isset($_SESSION['sessionid']))
{
$sessionid = $_SESSION['sessionid'];
}
if (!$sessionid)
{
$sessionid = uuid4();
$_SESSION['sessionid'] = $sessionid;
}
$db = new PDO('pgsql:host=127.0.0.1 dbname=sessiontest user=sessiontest password=sessiontest');
$data = 0;
try
{
$result = Query(
$db,
'SELECT data FROM usersessions WHERE uid = ?',
[$sessionid]
)->fetchAll();
if ($result)
{
$data = json_decode($result[0]['data'], 1);
}
} catch (Exception $e)
{
echo $e;
Query(
$db,
'CREATE TABLE usersessions(
uid TEXT PRIMARY KEY,
data TEXT
)'
);
}
if (!$data)
{
$data = ['count' => 0];
}
$data['count']++;
if ($data['count'] == 1)
{
Query(
$db,
'INSERT INTO usersessions(uid, data)
VALUES(?, ?)',
[$sessionid, json_encode($data)]
);
} else
{
Query(
$db,
'UPDATE usersessions
SET data = ?
WHERE uid = ?',
[json_encode($data), $sessionid]
);
}
echo 'Count is ' . $data['count'];
Այժմ մենք օգտագործել ենք կոդերի 98 տող՝ անելու այն, ինչ արեցին կոդերի չորս տողերը (և կազմաձևման մի ամբողջ փունջ) Laravel-ում: (Իհարկե, եթե մենք սխալների ճիշտ մշակում և օգտատերերի առջև ծառացած հաղորդագրություններ անեինք, դա մոտավորապես երկու անգամ ավելի կկազմի տողերի քանակից:) Միգուցե մենք կարող ենք վայրկյանում հասցնել 30 հարցում:
PHP/Pure PHP
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1
Running 10s test @ http://127.0.0.1
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 140.79ms 27.88ms 332.31ms 90.75%
Req/Sec 178.63 58.34 252.00 61.01%
7074 requests in 10.04s, 3.62MB read
Requests/sec: 704.46
Transfer/sec: 369.43KB
Ո՜վ Կարծես թե, ի վերջո, մեր PHP տեղադրման մեջ ոչ մի վատ բան չկա: Մաքուր PHP տարբերակը վայրկյանում կատարում է 700 հարցում:
Եթե PHP-ում սխալ բան չկա, միգուցե մենք սխալ կազմաձևել ենք Laravel-ը:
Համացանցը կազմաձևման խնդիրների և կատարողականի վերաբերյալ խորհուրդներ փնտրելուց հետո ամենահայտնի մեթոդներից երկուսը կազմաձևման և երթուղու տվյալների քեշավորումն էր՝ խուսափելու համար դրանք մշակելուց յուրաքանչյուր հարցում: Հետևաբար, մենք կընդունենք նրանց խորհուրդները և կփորձենք այս խորհուրդները:
╰─➤ php artisan config:cache
INFO Configuration cached successfully.
╰─➤ php artisan route:cache
INFO Routes cached successfully.
Հրամանի տողում ամեն ինչ լավ է թվում: Եկեք վերանայենք հենանիշը:
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1
Running 10s test @ http://127.0.0.1
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.13s 543.50ms 1.98s 61.90%
Req/Sec 25.45 13.39 50.00 55.77%
289 requests in 10.04s, 242.15KB read
Socket errors: connect 0, read 0, write 0, timeout 247
Requests/sec: 28.80
Transfer/sec: 24.13KB
Դե, մենք այժմ ավելացրել ենք կատարումը 21.04-ից մինչև 28.80 հարցում վայրկյանում, ինչը կտրուկ բարձրացում է գրեթե 37%: Սա բավականին տպավորիչ կլիներ ցանկացած ծրագրային փաթեթի համար... բացառությամբ այն փաստի, որ մենք դեռ կատարում ենք մաքուր PHP տարբերակի հարցումների քանակի միայն 1/24-րդ մասը:
Եթե կարծում եք, որ այս թեստի հետ ինչ-որ բան այն չէ, պետք է խոսեք Lucinda PHP շրջանակի հեղինակի հետ: Իր թեստի արդյունքներում նա ունի Լուսինդան հաղթում է Laravel-ին 36 անգամ HTML հարցումների համար և 90 անգամ՝ JSON հարցումների համար:
Իմ սեփական մեքենայի վրա թե՛ Apache-ի, թե՛ Nginx-ի հետ փորձարկումից հետո ես նրան կասկածելու պատճառ չունեմ: Laravel-ը իսկապես արդար է որ դանդաղ! PHP-ն ինքնին այնքան էլ վատ չէ, բայց երբ ավելացնեք բոլոր լրացուցիչ մշակումները, որոնք Laravel-ը ավելացնում է յուրաքանչյուր հարցում, ապա ես դժվարանում եմ առաջարկել Laravel-ին որպես ընտրություն 2023 թվականին:
PHP/Wordpress հաշիվների համար Համացանցում բոլոր կայքերի մոտ 40%-ը , դարձնելով այն ամենից գերիշխող շրջանակը: Անձամբ ես, սակայն, գտնում եմ, որ ժողովրդականությունը պարտադիր չէ որակի վերածել այլևս, քան ես ինքս ինձ զգում եմ այդ արտասովոր գուրման ուտելիքի հանկարծակի անկառավարելի ցանկությունը: աշխարհի ամենահայտնի ռեստորանը ... McDonald's. Քանի որ մենք արդեն փորձարկել ենք մաքուր PHP կոդը, մենք չենք պատրաստվում փորձարկել Wordpress-ը, քանի որ Wordpress-ի հետ կապված ցանկացած բան, անկասկած, ավելի ցածր կլինի, քան վայրկյանում 700 հարցումը, որը մենք դիտարկել ենք մաքուր PHP-ի դեպքում:
Django-ն ևս մեկ հանրաճանաչ շրջանակ է, որը գոյություն ունի երկար ժամանակ: Եթե նախկինում օգտագործել եք այն, հավանաբար սիրով հիշում եք տվյալների բազայի կառավարման տպավորիչ միջերեսը, ինչպես նաև այն, թե որքան տհաճ էր ամեն ինչ կարգավորելը այնպես, ինչպես ցանկանում էիք: Եկեք տեսնենք, թե որքան լավ է աշխատում Django-ն 2023 թվականին, հատկապես նոր ASGI ինտերֆեյսի հետ, որը նա ավելացրել է 4.0 տարբերակի դրությամբ:
Django-ի կարգավորումը զգալիորեն նման է Laravel-ի տեղադրմանը, քանի որ դրանք երկուսն էլ եղել են այն տարիքից, որտեղ MVC ճարտարապետությունները նորաձև և ճիշտ էին: Մենք բաց կթողնենք ձանձրալի կոնֆիգուրացիան և անմիջապես կանցնենք տեսարանը կարգավորելուն:
from django.shortcuts import render
from django.http import HttpResponse
# =====================================================================
def index(request):
count = request.session.get('count', 0)
count += 1
request.session['count'] = count
return HttpResponse(f"Count is {count}")
Չորս տող կոդը նույնն է, ինչ Laravel տարբերակում: Եկեք տեսնենք, թե ինչպես է այն գործում:
╰─➤ python --version
Python 3.10.9
Python/Django
╰─➤ gunicorn --access-logfile - -k uvicorn.workers.UvicornWorker -w 4 djangotest.asgi
[2023-03-21 15:20:38 +0800] [2886633] [INFO] Starting gunicorn 20.1.0
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1:8000/sessiontest/
Running 10s test @ http://127.0.0.1:8000/sessiontest/
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 277.71ms 142.84ms 835.12ms 69.93%
Req/Sec 91.21 57.57 230.00 61.04%
3577 requests in 10.06s, 1.46MB read
Requests/sec: 355.44
Transfer/sec: 148.56KB
Ամենևին էլ վատ չէ վայրկյանում 355 հարցում: Դա մաքուր PHP տարբերակի միայն կեսն է, բայց նաև 12 անգամ ավելի, քան Laravel տարբերակը: Ջանգոն ընդդեմ Լարավելի, կարծես թե, ընդհանրապես մրցակցություն չկա:
Բացի ամեն ինչից ավելի մեծ շրջանակներից, այդ թվում՝ խոհանոցի լվացարանի շրջանակներից, կան նաև ավելի փոքր շրջանակներ, որոնք պարզապես կատարում են որոշ հիմնական կարգավորումներ՝ միաժամանակ թույլ տալով ձեզ կարգավորել մնացածը: Օգտագործման լավագույններից մեկը Flask-ն է և նրա ASGI գործընկեր Quart-ը: Իմ սեփականը PaferaPy Framework կառուցված է Flask-ի վերևում, ուստի ես լավ ծանոթ եմ, թե որքան հեշտ է անել գործերը՝ պահպանելով կատարումը:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Session benchmark test
import json
import psycopg
import uuid
from flask import Flask, session, redirect, url_for, request, current_app, g, abort, send_from_directory
from flask.sessions import SecureCookieSessionInterface
app = Flask('pafera')
app.secret_key = b'secretkey'
dbconn = 0
# =====================================================================
@app.route('/', defaults={'path': ''}, methods = ['GET', 'POST'])
@app.route('/<path:path>', methods = ['GET', 'POST'])
def index(path):
"""Handles all requests for the server.
We route all requests through here to handle the database and session
logic in one place.
"""
global dbconn
if not dbconn:
dbconn = psycopg.connect('dbname=sessiontest user=sessiontest password=sessiontest')
cursor = dbconn.execute('''
CREATE TABLE IF NOT EXISTS usersessions(
uid TEXT PRIMARY KEY,
data TEXT
)
''')
cursor.close()
dbconn.commit()
sessionid = session.get('sessionid', 0)
if not sessionid:
sessionid = uuid.uuid4().hex
session['sessionid'] = sessionid
cursor = dbconn.execute("SELECT data FROM usersessions WHERE uid = %s", [sessionid])
row = cursor.fetchone()
count = json.loads(row[0])['count'] if row else 0
count += 1
newdata = json.dumps({'count': count})
if count == 1:
cursor.execute("""
INSERT INTO usersessions(uid, data)
VALUES(%s, %s)
""",
[sessionid, newdata]
)
else:
cursor.execute("""
UPDATE usersessions
SET data = %s
WHERE uid = %s
""",
[newdata, sessionid]
)
cursor.close()
dbconn.commit()
return f'Count is {count}'
Ինչպես տեսնում եք, Flask սկրիպտն ավելի կարճ է, քան մաքուր PHP սկրիպտը։ Ես գտնում եմ, որ իմ օգտագործած բոլոր լեզուներից Python-ը, հավանաբար, ամենաարտահայտիչ լեզուն է՝ տպագրված ստեղնաշարերի առումով: Փակագծերի և փակագծերի բացակայությունը, ցուցակների և թելադրանքների ըմբռնումները, և արգելափակումը, որը հիմնված է ոչ թե ստորակետերի, այլ նահանջների վրա, Python-ին դարձնում են բավականին պարզ, բայց հզոր իր հնարավորություններով:
Ցավոք սրտի, Python-ը նաև ամենադանդաղ ընդհանուր նշանակության լեզուն է այնտեղ, չնայած նրանում որքան ծրագրային ապահովում է գրված: Python-ի հասանելի գրադարանների թիվը մոտ չորս անգամ ավելին է, քան նմանատիպ լեզուները և ընդգրկում է հսկայական քանակությամբ տիրույթներ, սակայն ոչ ոք չի ասի, որ Python-ը արագ է և կատարողական է NumPy-ի նման նիշերից դուրս:
Եկեք տեսնենք, թե ինչպես է մեր Flask տարբերակը համեմատվում մեր նախորդ շրջանակների հետ:
Python/Flask
╰─➤ gunicorn --access-logfile - -w 4 flasksite:app
[2023-03-21 15:32:49 +0800] [2856296] [INFO] Starting gunicorn 20.1.0
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1:8000
Running 10s test @ http://127.0.0.1:8000
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 91.84ms 11.97ms 149.63ms 86.18%
Req/Sec 272.04 39.05 380.00 74.50%
10842 requests in 10.04s, 3.27MB read
Requests/sec: 1080.28
Transfer/sec: 333.37KB
Մեր Flask սցենարն իրականում ավելի արագ է, քան մեր մաքուր PHP տարբերակը:
Եթե դա ձեզ զարմացնում է, ապա պետք է հասկանաք, որ մեր Flask հավելվածն անում է իր ամբողջ սկզբնավորումն ու կազմաձևումը, երբ մենք գործարկում ենք Gunicorn սերվերը, մինչդեռ PHP-ն նորից գործարկում է սկրիպտը ամեն անգամ, երբ նոր հարցում է հայտնվում: Դա' Դա հավասարազոր է Ֆլասկին երիտասարդ, եռանդուն տաքսու վարորդին, ով արդեն գործի է դրել մեքենան և սպասում է ճանապարհի կողքին, մինչդեռ PHP-ն ծեր վարորդն է, ով մնում է իր տանը՝ սպասելով ներս մտնելու զանգին և միայն դրանից հետո քշում: քեզ վերցնելու համար: Լինելով հին դպրոցական տղա և գալիս է այն օրերից, երբ PHP-ն հիանալի փոփոխություն էր պարզ HTML և SHTML ֆայլերի համար, մի փոքր տխուր է գիտակցել, թե որքան ժամանակ է անցել, բայց դիզայնի տարբերություններն իսկապես դժվարացնում են PHP-ի համար մրցել Python-ի, Java-ի և Node.js սերվերների դեմ, որոնք պարզապես մնում են հիշողության մեջ և կատարում են հարցումները ձեռնածության հեշտությամբ:
Flask-ը կարող է լինել մեր ամենաարագ շրջանակը մինչ այժմ, բայց այն իրականում բավականին հին ծրագրակազմ է: Python համայնքը մի քանի տարի առաջ անցավ նոր ասիկրոն ASGI սերվերներին, և, իհարկե, ես ինքս անցել եմ նրանց հետ միասին:
Pafera Framework-ի նորագույն տարբերակը, PaferaPyAsync , հիմնված է Starlette-ի վրա: Չնայած կա ASGI Flask-ի տարբերակ, որը կոչվում է Quart, Quart-ի և Starlette-ի միջև կատարողական տարբերությունները բավական էին, որպեսզի ես փոխարենը վերահիմնավորեմ իմ կոդը Starlette-ի վրա:
Asychronous ծրագրավորումը կարող է վախեցնել շատ մարդկանց համար, բայց դա իրականում բարդ հայեցակարգ չէ՝ շնորհիվ Node.js-ի տղաների, որոնք ավելի քան մեկ տասնամյակ առաջ տարածեցին հայեցակարգը:
Մենք պայքարում էինք միաժամանակության դեմ բազմաթելային, բազմամշակման, բաշխված հաշվարկների, խոստումների շղթայական կապի և այն բոլոր զվարճալի ժամանակների հետ, որոնք վաղաժամ ծերացրին և չորացրեցին բազմաթիվ վետերան ծրագրավորողների: Այժմ մենք պարզապես մուտքագրում ենք async
մեր գործառույթների առջեւ եւ await
ցանկացած կոդի դիմաց, որի գործարկումը կարող է որոշ ժամանակ տևել: Այն, իրոք, ավելի մանրամասն է, քան սովորական կոդը, բայց շատ ավելի քիչ նյարդայնացնող է օգտագործել, քան համաժամացման պրիմիտիվների, հաղորդագրությունների փոխանցման և խոստումների լուծման հետ գործ ունենալը:
Մեր Starlette ֆայլն ունի հետևյալ տեսքը.
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Session benchmark test
import json
import uuid
import psycopg
from starlette.applications import Starlette
from starlette.responses import Response, PlainTextResponse, JSONResponse, RedirectResponse, HTMLResponse
from starlette.routing import Route, Mount, WebSocketRoute
from starlette_session import SessionMiddleware
dbconn = 0
# =====================================================================
async def index(R):
global dbconn
if not dbconn:
dbconn = await psycopg.AsyncConnection.connect('dbname=sessiontest user=sessiontest password=sessiontest')
cursor = await dbconn.execute('''
CREATE TABLE IF NOT EXISTS usersessions(
uid TEXT PRIMARY KEY,
data TEXT
)
''')
await cursor.close()
await dbconn.commit()
sessionid = R.session.get('sessionid', 0)
if not sessionid:
sessionid = uuid.uuid4().hex
R.session['sessionid'] = sessionid
cursor = await dbconn.execute("SELECT data FROM usersessions WHERE uid = %s", [sessionid])
row = await cursor.fetchone()
count = json.loads(row[0])['count'] if row else 0
count += 1
newdata = json.dumps({'count': count})
if count == 1:
await cursor.execute("""
INSERT INTO usersessions(uid, data)
VALUES(%s, %s)
""",
[sessionid, newdata]
)
else:
await cursor.execute("""
UPDATE usersessions
SET data = %s
WHERE uid = %s
""",
[newdata, sessionid]
)
await cursor.close()
await dbconn.commit()
return PlainTextResponse(f'Count is {count}')
# *********************************************************************
app = Starlette(
debug = True,
routes = [
Route('/{path:path}', index, methods = ['GET', 'POST']),
],
)
app.add_middleware(
SessionMiddleware,
secret_key = 'testsecretkey',
cookie_name = "pafera",
)
Ինչպես տեսնում եք, այն բավականին շատ պատճենված և տեղադրվել է մեր Flask սցենարից՝ ընդամենը մի քանի երթուղային փոփոխություններով և async/await
հիմնաբառեր.
Որքա՞ն բարելավում կարող է իրականում մեզ տալ պատճենահանված և տեղադրվող կոդը:
Python/Starlette
╰─➤ gunicorn --access-logfile - -k uvicorn.workers.UvicornWorker -w 4 starlettesite:app 130 ↵
[2023-03-21 15:42:34 +0800] [2856220] [INFO] Starting gunicorn 20.1.0
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1:8000
Running 10s test @ http://127.0.0.1:8000
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 21.85ms 10.45ms 67.29ms 55.18%
Req/Sec 1.15k 170.11 1.52k 66.00%
45809 requests in 10.04s, 13.85MB read
Requests/sec: 4562.82
Transfer/sec: 1.38MB
Մենք ունենք նոր չեմպիոն, տիկնայք և պարոնայք: Մեր նախորդ բարձր մակարդակը մեր մաքուր PHP տարբերակն էր վայրկյանում 704 հարցումով, որն այնուհետև գերազանցեց մեր Flask տարբերակը վայրկյանում 1080 հարցումով: Մեր Starlette սկրիպտը ջախջախում է բոլոր նախորդ հավակնորդներին վայրկյանում 4562 հարցումներով, ինչը նշանակում է 6 անգամ բարելավում մաքուր PHP-ի նկատմամբ և 4 անգամ բարելավում Flask-ի նկատմամբ:
Եթե դեռ չեք փոխել ձեր WSGI Python կոդը ASGI-ի, հիմա կարող է լավ ժամանակ լինել սկսելու համար:
Մինչ այժմ մենք ծածկել ենք միայն PHP և Python շրջանակները: Այնուամենայնիվ, աշխարհի մեծ մասը իրականում օգտագործում է Java, DotNet, Node.js, Ruby on Rails և նման այլ տեխնոլոգիաներ իրենց կայքերի համար: Սա ամենևին էլ աշխարհի բոլոր էկոհամակարգերի և բիոմների համապարփակ ակնարկ չէ, ուստի օրգանական քիմիայի ծրագրավորման համարժեքը խուսափելու համար մենք կընտրենք միայն այն շրջանակները, որոնց համար ամենահեշտ է մուտքագրել կոդը: որից Java-ն հաստատ չէ։
Եթե դուք թաքնվել եք K&R C-ի կամ Knuth's-ի ձեր պատճենի տակ Համակարգչային ծրագրավորման արվեստը Վերջին տասնհինգ տարիների ընթացքում դուք հավանաբար լսել եք Node.js-ի մասին: Մեզանից նրանք, ովքեր եղել են JavaScript-ի սկզբից ի վեր, կա՛մ աներևակայելիորեն վախեցած են, կա՛մ զարմացած, կա՛մ երկուսն էլ ժամանակակից JavaScript-ի վիճակով, բայց չի կարելի հերքել, որ JavaScript-ը դարձել է մի ուժ, որի հետ պետք է հաշվի նստել նաև սերվերների վրա: որպես բրաուզերներ: Ի վերջո, մենք նույնիսկ ունենք բնիկ 64 բիթանոց ամբողջ թվեր այժմ լեզվում: Դա շատ ավելի լավ է, քան այն ամենը, ինչ պահվում է 64 բիթանոց լողացողների մեջ:
ExpressJS-ը, հավանաբար, Node.js-ի օգտագործման ամենադյուրին սերվերն է, ուստի մենք կկատարենք արագ և կեղտոտ Node.js/ExpressJS հավելված՝ մեր հաշվիչին սպասարկելու համար:
/**********************************************************************
* Simple session test using ExpressJS.
**********************************************************************/
var L = console.log;
var uuid = require('uuid4');
var express = require('express');
var session = require('express-session');
var MemoryStore = require('memorystore')(session);
var { Client } = require('pg')
var db = 0;
var app = express();
const PORT = 8000;
//session middleware
app.use(
session({
secret: "secretkey",
saveUninitialized: true,
resave: false,
store: new MemoryStore({
checkPeriod: 1000 * 60 * 60 * 24 // prune expired entries every 24h
})
})
);
app.get('/',
async function(req,res)
{
if (!db)
{
db = new Client({
user: 'sessiontest',
host: '127.0.0.1',
database: 'sessiontest',
password: 'sessiontest'
});
await db.connect();
await db.query(`
CREATE TABLE IF NOT EXISTS usersessions(
uid TEXT PRIMARY KEY,
data TEXT
)`,
[]
);
};
var session = req.session;
if (!session.sessionid)
{
session.sessionid = uuid();
}
var row = 0;
let queryresult = await db.query(`
SELECT data::TEXT
FROM usersessions
WHERE uid = $1`,
[session.sessionid]
);
if (queryresult && queryresult.rows.length)
{
row = queryresult.rows[0].data;
}
var count = 0;
if (row)
{
var data = JSON.parse(row);
data.count += 1;
count = data.count;
await db.query(`
UPDATE usersessions
SET data = $1
WHERE uid = $2
`,
[JSON.stringify(data), session.sessionid]
);
} else
{
await db.query(`
INSERT INTO usersessions(uid, data)
VALUES($1, $2)`,
[session.sessionid, JSON.stringify({count: 1})]
);
count = 1;
}
res.send(`Count is ${count}`);
}
);
app.listen(PORT, () => console.log(`Server Running at port ${PORT}`));
Այս կոդը իրականում ավելի հեշտ էր գրել, քան Python-ի տարբերակները, թեև բնիկ JavaScript-ը դառնում է բավականին անգործունակ, երբ հավելվածներն ավելի մեծանում են, և սա ուղղելու բոլոր փորձերը, ինչպիսին է TypeScript-ը, արագ դառնում են ավելի մանրամասն, քան Python-ը:
Եկեք տեսնենք, թե ինչպես է սա կատարում:
Node.js/ExpressJS
╰─➤ node --version v19.6.0
╰─➤ NODE_ENV=production node nodejsapp.js 130 ↵
Server Running at port 8000
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1:8000
Running 10s test @ http://127.0.0.1:8000
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 90.41ms 7.20ms 188.29ms 85.16%
Req/Sec 277.15 37.21 393.00 81.66%
11018 requests in 10.02s, 3.82MB read
Requests/sec: 1100.12
Transfer/sec: 390.68KB
Դուք հավանաբար լսել եք հնագույն (ինտերնետի չափանիշներով հնագույն, այնուամենայնիվ...) ժողովրդական հեքիաթներ Node.js-ի մասին' արագությունը, և այդ պատմությունները հիմնականում ճշմարիտ են շնորհիվ այն տպավորիչ աշխատանքի, որը Google-ը կատարել է V8 JavaScript շարժիչով: Այնուամենայնիվ, այս դեպքում, թեև մեր արագ հավելվածը գերազանցում է Flask սցենարը, դրա մեկ թելային բնույթը տապալվում է չորս համաժամանակյա գործընթացներով, որոնք կիրառում է Starlette Knight-ը, ով ասում է «Ni!»:
Եկեք ավելի շատ օգնություն ստանանք:
╰─➤ pm2 start nodejsapp.js -i 4
[PM2] Spawning PM2 daemon with pm2_home=/home/jim/.pm2
[PM2] PM2 Successfully daemonized
[PM2] Starting /home/jim/projects/paferarust/nodejsapp.js in cluster_mode (4 instances)
[PM2] Done.
┌────┬──────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼──────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ nodejsapp │ default │ N/A │ cluster │ 37141 │ 0s │ 0 │ online │ 0% │ 64.6mb │ jim │ disabled │
│ 1 │ nodejsapp │ default │ N/A │ cluster │ 37148 │ 0s │ 0 │ online │ 0% │ 64.5mb │ jim │ disabled │
│ 2 │ nodejsapp │ default │ N/A │ cluster │ 37159 │ 0s │ 0 │ online │ 0% │ 56.0mb │ jim │ disabled │
│ 3 │ nodejsapp │ default │ N/A │ cluster │ 37171 │ 0s │ 0 │ online │ 0% │ 45.3mb │ jim │ disabled │
└────┴──────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
Լավ! Այժմ դա հավասար պայքար է չորս դեմ չորս: Եկեք նշենք.
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1:8000
Running 10s test @ http://127.0.0.1:8000
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 45.09ms 19.89ms 176.14ms 60.22%
Req/Sec 558.93 97.50 770.00 66.17%
22234 requests in 10.02s, 7.71MB read
Requests/sec: 2218.69
Transfer/sec: 787.89KB
Դեռևս այնքան էլ Starlette-ի մակարդակի վրա չէ, բայց դա վատ չէ հինգ րոպեանոց JavaScript-ի արագ հարձակման համար: Իմ սեփական փորձարկումից այս սկրիպտը իրականում մի փոքր հետ է պահվում տվյալների բազայի ինտերֆեյսի մակարդակում, քանի որ node-postgres-ը ոչ մի տեղ այնքան արդյունավետ չէ, որքան psycopg-ը Python-ի համար: sqlite-ին անցնելը, քանի որ տվյալների բազայի վարորդը մեկ վայրկյանում տալիս է ավելի քան 3000 հարցում նույն ExpressJS կոդի համար:
Հիմնական բանը, որ պետք է նշել, այն է, որ չնայած Python-ի դանդաղ կատարման արագությանը, ASGI շրջանակները կարող են իրականում մրցունակ լինել Node.js լուծումների հետ որոշակի ծանրաբեռնվածության համար:
Այսպիսով, հիմա մենք մոտենում ենք լեռան գագաթին, և լեռ ասելով նկատի ունեմ մկների և տղամարդկանց կողմից գրանցված ամենաբարձր ցուցանիշները:
Եթե նայեք համացանցում առկա շրջանակային հենանիշերի մեծամասնությանը, ապա կնկատեք, որ կան երկու լեզու, որոնք հակված են գերիշխել վերևում՝ C++ և Rust: Ես աշխատել եմ C++-ի հետ 90-ականներից, և ես նույնիսկ ունեի իմ սեփական Win32 C++ շրջանակը, նախքան MFC/ATL-ը, այնպես որ ես լեզվի հետ մեծ փորձ ունեմ: Ինչ-որ բանի հետ աշխատելն այնքան էլ զվարճալի չէ, երբ արդեն գիտես, դրա փոխարեն մենք պատրաստվում ենք անել Rust տարբերակը: ;)
Rust-ը համեմատաբար նոր է ծրագրավորման լեզուների առումով, բայց դա ինձ համար հետաքրքրության առարկա դարձավ, երբ Լինուս Տորվալդսը հայտարարեց, որ ինքը կընդունի Rust-ը որպես Linux միջուկի ծրագրավորման լեզու: Մեզ՝ տարեց ծրագրավորողների համար, դա մոտավորապես նույնն է, ինչ ասենք, որ այս նոր, նոր, նոր դարաշրջանի հիպի-հիպի բանը լինելու է ԱՄՆ Սահմանադրության նոր փոփոխություն:
Այժմ, երբ դուք փորձառու ծրագրավորող եք, դուք հակված չեք ցատկել այնքան արագ, որքան երիտասարդները, այլապես կարող եք այրվել լեզվի կամ գրադարանների արագ փոփոխություններից: (Յուրաքանչյուր ոք, ով օգտագործել է AngularJS-ի առաջին տարբերակը, կիմանա, թե ինչի մասին եմ խոսում:) Rust-ը դեռ որոշ չափով գտնվում է փորձարարական զարգացման փուլում, և ես ծիծաղելի եմ համարում, որ համացանցում շատ կոդի օրինակներ նույնիսկ չեն: այլևս հավաքել փաթեթների ընթացիկ տարբերակներով:
Այնուամենայնիվ, Rust հավելվածների ցուցադրած կատարումը չի կարելի հերքել: Եթե երբեք չեք փորձել ripgrep կամ fd-գտնել դուրս մեծ կոդով ծառերի վրա, դուք անպայման պետք է պտտեք նրանց: Դրանք նույնիսկ հասանելի են Linux բաշխումների մեծ մասի համար պարզապես փաթեթների կառավարիչից: Դուք խոսակցությունը փոխանակում եք Rust-ի հետ կատարման հետ... ա շատ խոսակցությունների համար ա շատ կատարման։
Rust-ի ամբողջական կոդը մի փոքր մեծ է, ուստի մենք պարզապես կնայենք համապատասխան մշակողներին այստեղ.
// =====================================================================
pub async fn RunQuery(
db: &web::Data<Pool>,
query: &str,
args: &[&(dyn ToSql + Sync)]
) -> Result<Vec<tokio_postgres::row::Row>, tokio_postgres::Error>
{
let client = db.get().await.unwrap();
let statement = client.prepare_cached(query).await.unwrap();
client.query(&statement, args).await
}
// =====================================================================
pub async fn index(
req: HttpRequest,
session: Session,
db: web::Data<Pool>,
) -> Result<HttpResponse, Error>
{
let mut count = 1;
if let Some(sessionid) = session.get::<String>("sessionid")?
{
let rows = RunQuery(
&db,
"SELECT data
FROM usersessions
WHERE uid = $1",
&[&sessionid]
).await.unwrap();
if rows.is_empty()
{
let jsondata = serde_json::json!({
"count": 1,
}).to_string();
RunQuery(
&db,
"INSERT INTO usersessions(uid, data)
VALUES($1, $2)",
&[&sessionid, &jsondata]
).await
.expect("Insert failed!");
} else
{
let jsonstring:&str = rows[0].get(0);
let countdata: CountData = serde_json::from_str(jsonstring)?;
count = countdata.count;
count += 1;
let jsondata = serde_json::json!({
"count": count,
}).to_string();
RunQuery(
&db,
"UPDATE usersessions
SET data = $1
WHERE uid = $2
",
&[&jsondata, &sessionid]
).await
.expect("Update failed!");
}
} else
{
let sessionid = Uuid::new_v4().to_string();
let jsondata = serde_json::json!({
"count": 1,
}).to_string();
RunQuery(
&db,
"INSERT INTO usersessions(uid, data)
VALUES($1, $2)",
&[&sessionid, &jsondata]
).await
.expect("Insert failed!");
session.insert("sessionid", sessionid)?;
}
Ok(HttpResponse::Ok().body(format!(
"Count is {:?}",
count
)))
}
Սա շատ ավելի բարդ է, քան Python/Node.js տարբերակները...
Rust/Actix
╰─➤ cargo run --release
[2023-03-21T23:37:25Z INFO actix_server::builder] starting 4 workers
Server running at http://127.0.0.1:8888/
╰─➤ wrk -d 10s -t 4 -c 100 http://127.0.0.1:8888
Running 10s test @ http://127.0.0.1:8888
4 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 9.93ms 3.90ms 77.18ms 94.87%
Req/Sec 2.59k 226.41 2.83k 89.25%
102951 requests in 10.03s, 24.59MB read
Requests/sec: 10267.39
Transfer/sec: 2.45MB
Եվ շատ ավելի կատարողական:
Actix/deadpool_postgres օգտագործող մեր Rust սերվերը հեշտությամբ հաղթում է մեր նախորդ չեմպիոն Starlette-ին +125%-ով, ExpressJS-ին +362%-ով և մաքուր PHP-ին +1366%-ով։ (Ես կթողնեմ կատարման դելտան Laravel տարբերակով որպես վարժություն ընթերցողի համար):
Ես հասկացա, որ Rust լեզուն սովորելն ինքնին ավելի դժվար է եղել, քան մյուս լեզուները, քանի որ այն ունի շատ ավելի շատ բան, քան այն ամենը, ինչ ես տեսել եմ 6502 Assembly-ից դուրս, բայց եթե ձեր Rust սերվերը կարող է ընդունել 14 անգամ ավելի օգտվողները որպես ձեր PHP սերվեր, ապա, ի վերջո, միգուցե ինչ-որ բան կարելի է ձեռք բերել փոխարկման տեխնոլոգիաներով: Ահա թե ինչու Pafera Framework-ի հաջորդ տարբերակը հիմնված կլինի Rust-ի վրա: Ուսուցման կորը շատ ավելի բարձր է, քան սկրիպտային լեզուները, բայց կատարումը արժե այն: Եթե դուք չեք կարող ժամանակ տրամադրել Rust-ին սովորելու համար, ապա ձեր տեխնոլոգիական փաթեթը Starlette-ի կամ Node.js-ի վրա հիմնելը նույնպես վատ որոշում չէ:
Վերջին քսան տարիների ընթացքում մենք էժան ստատիկ հոսթինգ կայքերից անցել ենք LAMP կույտերով համօգտագործվող հոսթինգի մինչև VPS-ներ վարձակալել AWS, Azure և այլ ամպային ծառայություններին: Մեր օրերում, շատ ընկերություններ բավարարված են դիզայնի որոշումներ կայացնելով՝ հիմնվելով նրանց վրա, ում նրանք կարող են գտնել այն հասանելի կամ ամենաէժանը, քանի որ հարմար ամպային ծառայությունների հայտնվելը հեշտացրել է ավելի շատ սարքավորումներ նետել դանդաղ սերվերների և հավելվածների վրա: Սա նրանց մեծ կարճաժամկետ շահույթ է տվել երկարաժամկետ տեխնիկական պարտքի գնով:
70 տարի առաջ Խորհրդային Միության և Միացյալ Նահանգների միջև մեծ տիեզերական մրցավազք էր: Խորհրդային Միությունը հաղթեց վաղ փուլերի մեծ մասը: Նրանք ունեին առաջին արբանյակը Sputnik-ում, առաջին շունը տիեզերքում Լայկայում, առաջին լուսնային տիեզերանավը Լունա 2-ում, առաջին տղամարդն ու կինը տիեզերքում Յուրի Գագարինում և Վալենտինա Տերեշկովայում և այլն...
Բայց կամաց-կամաց տեխնիկական պարտք էին կուտակում։
Չնայած Խորհրդային Միությունը առաջինն էր այս նվաճումներից յուրաքանչյուրին, նրանց ինժեներական գործընթացները և նպատակները ստիպում էին նրանց կենտրոնանալ կարճաժամկետ մարտահրավերների վրա, քան երկարաժամկետ իրագործելիության: Նրանք հաղթում էին ամեն անգամ, երբ ցատկեցին, բայց ավելի հոգնած ու դանդաղ էին դառնում, մինչդեռ մրցակիցները շարունակում էին հետևողական քայլեր կատարել դեպի վերջնագիծը:
Երբ Նիլ Արմսթրոնգը լուսնի վրա իր պատմական քայլերն արեց ուղիղ հեռուստատեսությամբ, ամերիկացիները ստանձնեցին առաջատարը, իսկ հետո մնացին այնտեղ, քանի որ սովետական ծրագիրը խափանում էր: Սա ոչնչով չի տարբերվում այն ընկերություններից, որոնք այսօր կենտրոնացել են հաջորդ մեծ բանի, հաջորդ մեծ վարձատրության կամ հաջորդ մեծ տեխնոլոգիայի վրա՝ չմշակելով համապատասխան սովորություններ և ռազմավարություններ երկարաժամկետ հեռանկարում:
Շուկայում առաջինը լինելը չի նշանակում, որ դուք կդառնաք այդ շուկայում գերիշխող խաղացողը։ Որպես այլընտրանք, ժամանակ հատկացնելն ամեն ինչ ճիշտ անելու համար չի երաշխավորում հաջողությունը, բայց, իհարկե, մեծացնում է երկարաժամկետ ձեռքբերումների ձեր հնարավորությունները: Եթե դուք տեխնոլոգիական առաջատարն եք ձեր ընկերության համար, ընտրեք ճիշտ ուղղությունը և գործիքները ձեր ծանրաբեռնվածության համար: Թույլ մի տվեք, որ ժողովրդականությունը փոխարինի կատարողականությանը և արդյունավետությանը:
Ցանկանու՞մ եք ներբեռնել Rust, ExpressJS, Flask, Starlette և Pure PHP սկրիպտները պարունակող 7z ֆայլ:
Հեղինակի մասին |
|
![]() |
Ջիմը ծրագրավորում է այն պահից, երբ 90-ական թվականներին ստացել է IBM PS/2: Մինչ օրս նա դեռ նախընտրում է HTML և SQL գրել ձեռքով և կենտրոնանում է աշխատանքի արդյունավետության և կոռեկտության վրա։ |