Ачааллж байна...
Өмнөх хоёр хичээлд бид бараг бүх query-г ижил хэлбэрээр бичсэн: with engine.connect() as connection: эсвэл with engine.begin() as connection:. Бид энэ хэлбэрийг "танил хэв маяг" гэж хүлээн авсан ч түүний цаана юу болж байгааг тайлбарлаагүй. Холболт гэж яг юу вэ? Яагаад with заавал хэрэгтэй вэ? Хэрэв хэн нэгэн with-гүйгээр холболт нээгээд хаахаа мартвал юу болох вэ?
Эдгээр асуулт эхлээд техникийн жижиг зүйл мэт санагдаж магадгүй. Гэвч бодит вэб програмд холболтын буруу зохицуулалт бол хамгийн түгээмэл, бас хамгийн нууцлаг асуудлын нэг: програм хөгжүүлэгчийн компьютер дээр төгс ажиллаж, жинхэнэ хэрэглэгчид ирсэн өдөр нэг цагийн дараа гэнэт "гацдаг". Шалтгаан нь ихэвчлэн нэг л зүйл — хаагдаагүй холболтууд хуримтлагдаж, server-ийн хязгаарт хүрсэн.
Энэ хичээлд бид холболтын үнийг хэмжиж, engine холболтуудыг хэрхэн дахин ашигладаг (pooling) болохыг өөрийн нүдээр харж, хаагдаагүй холболт хэрхэн бүх програмыг гацаадгийг аюулгүй орчинд үзүүлнэ. Python-ы гүнзгий түвшний сургалтад context manager — with хэллэгийн цаадах __enter__ ба __exit__ — -ийн тухай сурсан бол тэр мэдлэг энд бүрэн хэрэглэгдэнэ. Сураагүй бол санаа зоволтгүй: хэрэгтэй бүхнийг энд тайлбарлана.
Архивын албанд утсаар хүсэлт өгдөг гэж бодъё. Утасны дуудлага бүр үнэтэй: дугаар залгах, хариу хүлээх, өөрийгөө танилцуулах, нууц үгээ хэлж баталгаажуулах — зөвхөн үүний дараа л жинхэнэ асуултаа асууна. Хэрэв та өдөрт хэдэн зуун асуулт асуудаг бол асуулт бүрт шинээр залгах нь цагийн ихэнхийг танилцуулга, баталгаажуулалтад зарцуулна.
Ухаалаг байгууллага архивтай хэд хэдэн тогтмол шугам нээж, тэдгээрийг ажилтнуудын дунд хуваалцдаг. Ажилтан асуулт асуух бол сул шугамыг авч, асуултаа асууж, дуусмагц утсаа тавина — шугам тасрахгүй, дараагийн ажилтанд бэлэн хүлээнэ. Шугам бүр нэг удаа л баталгаажуулагддаг. Энэ бол pooling буюу холболтын сан: engine нь хэдэн нээлттэй холболтыг хадгалж, тэдгээрийг дахин дахин зээлүүлдэг.
Энэ зүйрлэлд аюул ч бий. Хэрэв ажилтан асуултаа дуусгаад утсаа тавихаа мартвал шугам эзэлсэн хэвээр үлдэнэ. Шугам цөөн тул хэдэн ажилтан мартахад бусад нь сул шугам олохгүй, хүлээж эхэлнэ — бүх байгууллага гацна. Мөн архивын тал дээр шугам бүрт тусдаа ажилтан сууж байдаг: хэт олон шугам нээвэл архив ажилтангүй болж, шинэ дуудлагыг хүлээж авахаа болино.
Бүлэг 10-т PostgreSQL-ийг тусдаа server гэж сурсан. Python програм server-тэй ярихын тулд холболт (connection) нээх ёстой: сүлжээгээр хаяг ба портод хүрэх, хэрэглэгчийн нэр ба нууц үгээ баталгаажуулах, өгөгдлийн сан сонгох. PostgreSQL-ийн онцлог нь шинэ холболт бүрт server талд тусдаа процесс — зүйрлэлийн "шугам бүрт тусдаа ажилтан" — эхлүүлдэгт оршино. Энэ нь холболтыг үнэтэй болгодог: нээхэд хугацаа, нээлттэй байхад server-ийн санах ой зарцуулдаг.
Ийм учраас server нийт холболтын тоонд хязгаар тавьдаг — PostgreSQL-ийн анхдагч хязгаар 100. Энэ нь их санагдаж болох ч олон хуулбартай вэб програм (Бүлэг 10-ын Жишээ 1), хэд хэдэн хөгжүүлэгч, мониторингийн хэрэгслүүд хамт ажиллахад хурдан дүүрдэг. Интернэтэд байршуулсан удирдлагатай PostgreSQL үйлчилгээнүүд (Neon гэх мэт) ихэвчлэн илүү бага хязгаартай, заримдаа холболтын тоогоор төлбөр тооцдог.
Нээлттэй холболтуудыг server-ээс өөрөөс нь харж болно. psql-д library өгөгдлийн санд холбогдоод:
sql
SELECT pid, application_name, state
FROM pg_stat_activity
WHERE datname = 'library';Үр дүн:
pid | application_name | state
-------+------------------+--------
14820 | psql | active
(1 row)Энэ query юу гэж хэлж байна вэ гэвэл: “server-ийн одоогийн үйл ажиллагааны жагсаалтаас library өгөгдлийн сантай холбогдсон холболтуудын процессын дугаар, програмын нэр, төлөвийг харуул.” pg_stat_activity бол PostgreSQL-ийн өөрийн тухай мэдээлэл агуулсан тусгай view (хүснэгт шиг уншигддаг объект). Одоогоор нэг холболт — таны psql. pid нь server талын процессын дугаар — зүйрлэлийн ажилтан. Энэ psql цонхыг нээлттэй үлдээгээрэй — доорх жишээнүүдэд дахин ашиглана.
Холболт үнэтэй гэдгийг хэмжилтээр нотолъё. pg-practice хавтсанд pool_timing.py:
python
import os
import time
from sqlalchemy import create_engine, text
from sqlalchemy.pool import NullPool
url = os.environ["DATABASE_URL"]
pooled_engine = create_engine(url)
no_pool_engine = create_engine(url, poolclass=NullPool)
def run_queries(engine, count):
start = time.perf_counter()
for _ in range(count):
with engine.connect() as connection:
connection.execute(text("SELECT 1"))
return time.perf_counter() - start
print(f"Pool-гүй: {run_queries(no_pool_engine, 50):.3f} сек")
print(f"Pool-той: {run_queries(pooled_engine, 50):.3f} сек")Кодыг уншъя. Хоёр engine үүсгэсэн: эхнийх нь анхдагч — pool-той; хоёр дахь нь NullPool-той — холболт бүрийг шинээр нээж, хэрэглэсний дараа бүрмөсөн хаадаг. run_queries функц 50 удаа давтаж, удаа бүр холболт аваад хамгийн энгийн query (SELECT 1 — “1-ийг буцаа”) ажиллуулна. time.perf_counter() нь өндөр нарийвчлалтай цаг хэмжигч. Query өөрөө бараг ажил хийдэггүй тул хугацааны бараг бүхэлд нь холболтод зарцуулагдана.
Ажиллуулна:
powershell
python pool_timing.pyҮр дүн (жишээ):
Pool-гүй: 0.640 сек
Pool-той: 0.031 секТаны тоонууд өөр байх ч харьцаа ижил байна: pool-гүй хувилбар хэдэн арав дахин удаан. Pool-гүй үед 50 удаа шинээр залгаж, нууц үг баталгаажуулж, server шинэ процесс эхлүүлсэн. Pool-той үед engine эхний удаа нэг холболт нээж, with блок дуусахад түүнийг хаалгүйгээр сандаа буцааж тавьсан; дараагийн 49 удаа тэр холболтыг шууд дахин зээлсэн. Секунд тутам олон хүсэлт хүлээн авдаг вэб програмд энэ ялгаа "хурдан сайт" ба "удаан сайт" хоёрын ялгаа.
Энэ хэмжилтээс хамгийн чухал дүгнэлт: with engine.connect() блокийн төгсгөлд холболт хаагддаггүй — pool руу буцдаг. "Холболт хаах" гэж бид ярьдаг ч pool-той engine-д энэ нь "сан руу буцааж өгөх" гэсэн утгатай. Эхний хичээлийн echo гаралтын төгсгөлд гарч байсан ROLLBACK бол буцаахаас өмнөх цэвэрлэгээ байсан: дараагийн зээлэгч өмнөх хүний дуусаагүй transaction-тэй холболт авахгүйн тулд.
Жишээ 1: pool-ийг server талаас харах
Pool дахь холболтууд server дээр бодитоор нээлттэй байгааг харъя. pool_watch.py:
python
import os
from sqlalchemy import create_engine, text
engine = create_engine(os.environ["DATABASE_URL"])
connections = [engine.connect() for _ in range(3)]
for connection in connections:
connection.execute(text("SELECT 1"))
print(engine.pool.status())
input("Гурван холболт нээлттэй. psql-д pg_stat_activity-г шалгаад Enter дар...")
for connection in connections:
connection.close()
print(engine.pool.status())
input("Холболтууд pool руу буцлаа. Дахин шалгаад Enter дар...")Энэ код санаатайгаар with-гүйгээр гурван холболт аваад, тэдгээрийг гараар хаадаг — гэхдээ хоёр шатанд түр зогсож, бид server-ийг шалгах боломжтой. Ажиллуулахад:
Pool size: 5 Connections in pool: 0 Current Overflow: -2 Current Checked out connections: 3
Гурван холболт нээлттэй. psql-д pg_stat_activity-г шалгаад Enter дар...engine.pool.status() нь pool-ийн одоогийн байдлыг хэлж байна: “pool-ийн хэмжээ 5; санд хүлээж буй холболт 0; гараар зээлсэн холболт 3.” Нээлттэй байгаа psql цонхондоо дахин SELECT pid, application_name, state FROM pg_stat_activity WHERE datname = 'library'; ажиллуулахад дөрвөн мөр гарна: таны psql ба Python-ы гурван холболт — тус бүр өөрийн pid-тэй, өөрөөр хэлбэл server дээр гурван тусдаа процесс.
Python-ы цонхонд Enter дарахад:
Pool size: 5 Connections in pool: 3 Current Overflow: -2 Current Checked out connections: 0
Холболтууд pool руу буцлаа. Дахин шалгаад Enter дар...close() дуудсан ч "санд хүлээж буй холболт 3" болсныг анзаараарай. psql-д дахин шалгавал гурван Python холболт байсаар байгаа — state нь idle (сул). Тэд хаагдаагүй, pool руу буцаж, дараагийн зээлэгчийг хүлээж байна. Энэ бол Анхны жишээний хурдны нууц. Enter дарж скриптийг дуусгахад Python-ы процесс төгсөж, бүх холболт бодитоор хаагдана — psql-д дахин шалгахад зөвхөн psql үлдэнэ.
Жишээ 2: хаагдаагүй холболт ба гацсан програм
Одоо зүйрлэлийн "утсаа тавихаа мартсан ажилтан"-ыг харъя. SQLAlchemy-ийн анхдагч pool 5 холболтыг байнга хадгалж, хэрэгцээ гарвал нэмэлт 10 хүртэлх түр холболт (overflow) нээдэг — нийт 15. Хэрэв холболтууд буцаагдахгүй бол 16 дахь нь сул холболт хүлээнэ, анхдагчаар 30 секунд. Хүлээлтийг хурдасгахын тулд 3 секунд болгоё. leak_demo.py:
python
import os
from sqlalchemy import create_engine, text
engine = create_engine(os.environ["DATABASE_URL"], pool_timeout=3)
leaked = []
for number in range(1, 17):
connection = engine.connect()
connection.execute(text("SELECT 1"))
leaked.append(connection)
print(f"{number}-р холболт авлаа")Энэ код юу гэж хэлж байна вэ гэвэл: “16 удаа холболт аваад, query ажиллуулаад, хэзээ ч хаахгүйгээр жагсаалтад хадгал.” Энэ бол хаахаа мартсан кодын санаатай загвар — бодит алдаатай кодонд холболт жагсаалтад биш, хаа нэгтээ мартагдсан хувьсагчид үлддэг. Ажиллуулахад 15 мөр хурдан хэвлэгдээд, гурван секунд зогсож:
15-р холболт авлаа
Traceback (most recent call last):
...
sqlalchemy.exc.TimeoutError: QueuePool limit of size 5 overflow 10 reached, connection timed out, timeout 3.00 (Background on this error at: https://sqlalche.me/e/20/3o7r)Энэ мессеж юу гэж хэлж байна вэ гэвэл: “pool-ийн хязгаар — үндсэн 5, нэмэлт 10 — дүүрлээ; холболт хүлээх хугацаа (3 секунд) дууслаа.” Арван тав дахь холболтын дараа сул холболт үлдээгүй, хэн ч буцааж өгөхгүй тул 16 дахь хүлээлт хугацаандаа дуусав. Бодит вэб програмд энэ нь хэрэглэгч бүр хуудас нээхэд 30 секунд хүлээгээд алдаа авна гэсэн үг — програм "гацсан" мэт харагдана. Хамгийн хэцүү нь: алдаа гарсан мөр (16 дахь engine.connect()) буруутан биш — буруутан нь өмнө хаа нэгтээ холболтоо буцаагаагүй код.
Server талын хязгаар ч бий. Хэрэв pool-гүй, эсвэл олон програм тус бүр том pool-той ажиллаж нийт холболт PostgreSQL-ийн 100-гийн хязгаарт хүрвэл server шинэ холболтыг татгалзаж, FATAL: sorry, too many clients already — "уучлаарай, хэт олон client холбогдсон байна" — гэсэн алдаа өгнө. Энэ үед зөвхөн таны програм биш, тэр server-тэй холбогддог бүх хүн — psql-ээр орох гэсэн администратор хүртэл — нэвтэрч чадахгүй.
Жишээ 3: with яагаад хамгаалдаг вэ
Жишээ 2-ын код санаатайгаар close()-ийг алгассан. Гэвч close() бичсэн код ч холболт алдаж болно. Дараах кодыг харъя:
python
connection = engine.connect()
connection.execute(text("SELECT * FROM bookz")) # үсгийн алдаатай хүснэгт
connection.close()Хоёр дахь мөр relation "bookz" does not exist алдаа шиднэ. Exception шидэгдэх мөчид Python гурав дахь мөрийг огт ажиллуулахгүй — функцээс гарч, алдааг дээш дамжуулна. close() дуудагдаагүй, холболт буцаагдаагүй. Вэб програмд ийм алдаа хүсэлт бүрт гарвал Жишээ 2-ын гацалт хэдэн арван хүсэлтийн дараа ирнэ.
Python-ы гүнзгий түвшний сургалтад сурсан context manager энэ асуудлыг шийддэг. with engine.connect() as connection: гэж бичихэд Python блок эхлэхэд холболтын __enter__ аргыг дуудаж (холболт зээлэх), блок дуусахад __exit__ аргыг дуудна (холболт буцаах) — блок хэвийн дууссан ч, exception-ээр тасарсан ч. Энэ нь доорх try/finally-тэй ижил:
python
connection = engine.connect()
try:
connection.execute(text("SELECT * FROM bookz"))
finally:
connection.close()finally блок exception гарсан ч, гараагүй ч ажилладаг. with бол энэ бүтцийн товч, мартагдашгүй хэлбэр — нэг мөрөөр нээж, хаалтыг Python-д даатгана. Дүрэм: холболт, session-ийг үргэлж with-ээр нээ. Жишээ 2, 3-т with-гүй код бичсэн нь зөвхөн асуудлыг харуулахын тулд л.
Жишээ 4: connect() ба begin()-ийн ялгаа
Өмнөх хичээлд "уншихад connect(), өгөгдөл өөрчлөхөд begin()" гэсэн дүрэм тавьсан. Одоо ялгааг нь харъя. Хоёулаа холболт зээлж, блокийн төгсгөлд буцаадаг. Ялгаа нь transaction-ийг хэрхэн дуусгахад оршино:
python
with engine.begin() as connection:
connection.execute(text("UPDATE books SET copies = copies + 1 WHERE id = 1"))
connection.execute(text("SELECT * FROM bookz")) # алдааbegin() блок хэвийн дуусвал өөрчлөлтийг хадгална (COMMIT); exception гарвал бүх өөрчлөлтийг цуцална (ROLLBACK). Энд хоёр дахь мөр алдаа шидсэн тул эхний мөрийн UPDATE ч цуцлагдана — "Монголын нууц товчоо"-ны хувийн тоо өөрчлөгдөхгүй. psql-ээр SELECT copies FROM books WHERE id = 1; шалгавал 2 хэвээр байна. connect() блок харин үргэлж ROLLBACK-аар дуусдаг — та connection.commit()-ийг өөрөө дуудаагүй бол. Тиймээс connect() нь уншихад тохиромжтой (хадгалах зүйл байхгүй), begin() нь өөрчлөхөд (бүгд амжилттай бол хадгал, үгүй бол юуг ч бүү хадгал). Энэ "бүгд-эсвэл-юу-ч-үгүй" зан бол дараагийн бүлгийн гол сэдэв — transaction.
Жишээ 5: FastAPI програм дахь engine ба session
FastAPI сургалтад та ойролцоогоор ийм кодтой байсан:
python
engine = create_engine(DATABASE_URL)
def get_session():
with Session(engine) as session:
yield session
@app.get("/books")
def list_books(session: Session = Depends(get_session)):
return session.exec(select(Book)).all()Одоо энэ кодын мөр бүр танд шинэ утгатай болно. engine модулийн түвшинд, програм эхлэхэд нэг удаа үүснэ — нэг engine, нэг pool, бүх хүсэлтэд хуваалцагдана. get_session бол хүсэлт бүрт ажилладаг FastAPI-ийн dependency: with Session(engine) нь pool-оос холболт зээлэх session нээж, yield нь түүнийг endpoint-д өгч, endpoint дуусахад with блок хаагдаж холболт pool руу буцна. SQLModel-ийн Session бол холболтын дээр суусан илүү ухаалаг давхарга — тэр холболтыг зээлэхээс гадна таны Python объектуудын өөрчлөлтийг хянаж, session.commit() дуудах үед тэдгээрийг SQL болгон илгээдэг. Гэвч холболтын үүднээс зарчим ижил: хүсэлт бүр нэг холболт зээлж, дуусахад буцаадаг. Хэдэн зуун хүсэлт ирсэн ч pool-ийн хэдэн холболт тэднийг ээлжлэн үйлчилнэ.
Энэ кодын хамгийн түгээмэл алдаа нь engine-ийг функц дотор үүсгэх:
python
def get_session():
engine = create_engine(DATABASE_URL) # БУРУУ
with Session(engine) as session:
yield sessionЭнэ нь хүсэлт бүрт шинэ engine — шинэ pool, шинэ холболт — үүсгэнэ. Анхны жишээний pool-гүй удаашрал буцаж ирэхээс гадна хуучин engine-үүдийн холболтууд хаагдахгүй үлдэж, server-ийн хязгаарт хурдан хүрнэ. Дүрэм: програмд нэг engine, хүсэлт бүрт нэг session.
Хэрэв сонирхож байвал Жишээ 2-ын leak_demo.py-г засаж, давталт бүрт with ашигладаг болгоод, 16 биш 100 удаа давтаж үзээрэй — алдаа гарахгүй, учир нь холболт бүр буцаагддаг тул pool хэзээ ч дүүрэхгүй. Мөн Жишээ 1-ийн скриптэд create_engine(url, pool_size=2) гэж pool-ийн хэмжээг өөрчилж, engine.pool.status()-ийн гаралт хэрхэн өөрчлөгдөхийг хараарай. Эдгээр нь зөвхөн санал бөгөөд дараагийн хичээлүүд тэдгээрийг хийсэн гэж тооцохгүй.
Pool дүүрэх. sqlalchemy.exc.TimeoutError: QueuePool limit of size 5 overflow 10 reached, connection timed out нь бараг үргэлж холболт буцаагдахгүй байгааг илэрхийлнэ — програм хэт ачаалалтай байгааг биш. Pool-ийн хэмжээг томруулах нь асуудлыг хойшлуулна, шийдэхгүй. Кодоос with-гүй engine.connect() эсвэл Session(engine)-г хайж ол.
Хаагдсан холболтыг ашиглах. with блокийн гадна холболтын хувьсагчийг ашиглавал sqlalchemy.exc.ResourceClosedError: This Connection is closed гарна. Энэ мессеж юу гэж хэлж байна вэ гэвэл: "энэ холболт хаагдсан." Үр дүнг блок дотор уншиж дуусга — жишээ нь rows = list(connection.execute(...)) гэж жагсаалт болгоод блокийн гадна ашигла.
Engine-ийг давтан үүсгэх. Функц, endpoint, давталт дотор create_engine дуудах нь pool-ийн бүх ашгийг устгаж, холболтуудыг хуримтлуулна. Алдааны мессеж шууд гарахгүй — програм аажмаар удааширч, эцэст нь too many clients already гарна. Engine-ийг модулийн түвшинд нэг удаа үүсгэ.
Session-д commit хийхээ мартах. with Session(engine) as session: блок дотор session.add(...) хийгээд session.commit()-гүйгээр блокийг дуусгавал алдаа гарахгүй — гэвч өөрчлөлт хадгалагдахгүй, учир нь connect()-ийн нэгэн адил session хаагдахдаа хадгалаагүй өөрчлөлтийг цуцалдаг. FastAPI сургалтад endpoint бүр session.commit()-тэй байсан нь үүнээс шалтгаалсан.
too many clients already. psql эсвэл Python холбогдох гэхэд энэ FATAL гарвал server-ийн холболтын хязгаар дүүрсэн. Хамгийн ойр шийдэл: холболтуудыг эзэлж буй програмуудыг хаах (Python скриптүүд, нэмэлт pgAdmin цонхнууд). psql-ээр орж чадвал pg_stat_activity-ээс ямар програм олон холболт эзэлж байгааг олж болно. Урт хугацааны шийдэл үргэлж ижил: холболт бүрийг with-ээр нээж, програм бүрт нэг engine.
Энэ хичээлд та холболтыг зохицуулах аргыг ойлголоо. PostgreSQL-ийн холболт бүр server дээр тусдаа процесс бөгөөд нээхэд үнэтэй тул server нийт тоонд (анхдагчаар 100) хязгаар тавьдаг; pg_stat_activity нээлттэй холболтуудыг харуулдаг. Engine холболтуудын сан (pool) хадгалж, with блок дуусахад холболтыг хаалгүйгээр сан руу буцаадаг — хэмжилтээр pool-тэй хувилбар хэдэн арав дахин хурдан байсан. Буцаагдаагүй холболтууд pool-ийг дүүргэж, програмыг QueuePool limit ... reached алдаагаар гацаадаг бөгөөд exception гарахад close()-ийг алгасдаг код ч ийм алдаа үүсгэдэг; with (context manager) нь холболтыг ямар ч тохиолдолд буцаахыг баталгаажуулдаг. connect() блок ROLLBACK-аар, begin() блок амжилттай бол COMMIT-оор, алдаатай бол ROLLBACK-аар дуусдаг. FastAPI програмд нэг engine модулийн түвшинд, хүсэлт бүрт нэг session with-ээр.
Ингээд Бүлэг 11 дууслаа — Python програм тань PostgreSQL-тэй аюулгүй, үр дүнтэй ярьж чадна. Энэ бүлгийн турш хэд хэдэн удаа BEGIN, COMMIT, ROLLBACK гэсэн үгстэй тааралдсан бөгөөд "дараагийн бүлэгт тайлбарлана" гэж хойшлуулсаар ирсэн. Бүлэг 12 бүхэлдээ тэдэнд зориулагдсан: transaction — олон алхамт өөрчлөлтийг "бүгд-эсвэл-юу-ч-үгүй" гэрээ болгох арга. Бид банкны шилжүүлгийн сонгодог түүхээр эхэлж, transaction яагаад хэрэгтэй, тэд таны кодонд хаана аль хэдийн нуугдаж байсныг харна.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.