PostgreSQL
 sql >> база данни >  >> RDS >> PostgreSQL

Производителност на базирано на PostgreSQL приложение:латентност и скрити забавяния

Goldfields Pipeline, от SeanMac (Wikimedia Commons)

Ако се опитвате да оптимизирате производителността на вашето базирано на PostgreSQL приложение, вероятно се фокусирате върху обичайните инструменти:ОБЯСНЯВАНЕ (БУФЕРИ, АНАЛИЗ) , pg_stat_statements , auto_explain , min_duration_log_statement и др.

Може би търсите спор за заключване с log_lock_waits , наблюдение на ефективността на вашата контролна точка и др.

Но помислихте ли за латентността на мрежата ? Геймърите знаят за латентността на мрежата, но смятате ли, че това има значение за вашия сървър на приложения?

Закъснението има значение

Типичните латентности на мрежата за двупосочно пътуване на клиент/сървър могат да варират от 0.01ms (localhost) до ~0.5ms на комутирана мрежа, 5ms WiFi, 20ms ADSL, 300ms междуконтинентално маршрутизиране и дори повече за неща като сателитни и WWAN връзки .

Тривиален SELECT може да отнеме от порядъка на 0,1 ms за изпълнение от страна на сървъра. Тривиален INSERT може да отнеме 0,5 мс.

Всеки път, когато вашето приложение изпълнява заявка, то трябва да изчака сървърът да отговори с успех/неуспех и евентуално набор от резултати, метаданни на заявка и т.н. Това води до поне едно закъснение за двупосочно пътуване до мрежата.

Когато работите с малки, прости заявки, забавянето на мрежата може да бъде значително по отношение на времето за изпълнение на вашите заявки, ако вашата база данни не е на същия хост като приложението ви.

Много приложения, особено ORM, са много склонни да изпълняват много на доста прости запитвания. Например, ако вашето приложение Hibernate извлича обект с лениво извлечен @OneToMany връзка с 1000 дъщерни елемента вероятно ще направи 1001 заявки благодарение на проблема за избор n+1, ако не и повече. Това означава, че вероятно изразходва 1000 пъти латентността ви за двупосочно пътуване на мрежата само в изчакване . Можете да извличате вляво за да избегнете това... но след това прехвърляте родителския обект 1000 пъти в присъединяването и трябва да го дедуплицирате.

По същия начин, ако попълвате базата данни от ORM, вероятно правите стотици хиляди тривиални INSERT s... и чакам след всеки един сървър да потвърди, че е наред.

Лесно е да се опитате да се съсредоточите върху времето за изпълнение на заявката и да опитате да го оптимизирате, но можете да направите толкова много с едно тривиално INSERT INTO ...VALUES ... . Изхвърлете някои индекси и ограничения, уверете се, че е групирано в транзакция и сте почти готови.

Какво ще кажете за премахването на всички чакания в мрежата? Дори в LAN те започват да събират над хиляди заявки.

КОПИРАНЕ

Един от начините да избегнете забавяне е да използвате COPY . За да използвате поддръжката на COPY на PostgreSQL, вашето приложение или драйвер трябва да създаде набор от редове, подобен на CSV, и да ги предава поточно към сървъра в непрекъсната последователност. Или сървърът може да бъде помолен да изпрати на вашето приложение поток, подобен на CSV.

Така или иначе приложението не може да преплита COPY с други заявки и вмъкванията за копиране трябва да бъдат заредени директно в таблицата на местоназначението. Често срещан подход е КОПИРАНЕ във временна таблица, след което от там направете INSERT INTO ... SELECT ... , АКТУАЛИЗИРАНЕ ... ОТ .... , ИЗТРИВАНЕ ОТ ... ИЗПОЛЗВАНЕ... , и т.н., за да използвате копираните данни за промяна на основните таблици с една операция.

Това е удобно, ако пишете директно свой собствен SQL, но много рамки на приложения и ORM не го поддържат, плюс това може директно да замени просто INSERT . Вашето приложение, рамка или клиентски драйвер трябва да се справят с преобразуването за специалното представяне, необходимо от COPY , потърсете самите метаданни за необходимия тип и т.н.

(Забележителни драйвери, които правят поддържа КОПИРАНЕ включват libpq, PgJDBC, psycopg2 и Pg gem... но не непременно рамките и ORM, изградени върху тях.)

PgJDBC – пакетен режим

JDBC драйверът на PostgreSQL има решение за този проблем. Той разчита на поддръжка, присъстваща в сървърите на PostgreSQL от 8.4 и на пакетните функции на JDBC API за изпращане на партида на заявки към сървъра, след което изчакайте само веднъж за потвърждение, че цялата партида е работила добре.

Е, на теория. В действителност някои предизвикателства при внедряване ограничават това, така че партидите могат да се извършват само на парчета от няколкостотин заявки в най-добрия случай. Драйверът може също да изпълнява заявки, които връщат редове с резултати в пакетни парчета, ако може да разбере колко големи ще бъдат резултатите преди време. Въпреки тези ограничения, използвайте Statement.executeBatch() може да предложи огромен тласък на производителността на приложения, които изпълняват задачи като групови данни, зареждащи отдалечени екземпляри на база данни.

Тъй като това е стандартен API, той може да се използва от приложения, които работят в множество машини за бази данни. Hibernate, например, може да използва пакетиране на JDBC, въпреки че не го прави по подразбиране.

libpq и пакетиране

Повечето (всички?) други PostgreSQL драйвери нямат поддръжка за пакетиране. PgJDBC внедрява протокола PostgreSQL напълно независимо, докато повечето други драйвери вътрешно използват C библиотеката libpq който се предоставя като част от PostgreSQL.

libpq не поддържа пакетиране. Той има асинхронен неблокиращ API, но клиентът все още може да има само една заявка „в полет“ наведнъж. Трябва да изчака, докато бъдат получени резултатите от тази заявка, преди да може да изпрати друга.

PostgreSQL сървърът поддържа пакетиране много добре и PgJDBC вече го използва. Затова написах пакетна поддръжка за libpq и го представи като кандидат за следващата версия на PostgreSQL. Тъй като променя само клиента, ако се приеме, пак ще ускори нещата при свързване към по-стари сървъри.

Наистина ще се интересувам от обратна връзка от автори и напреднали потребители на libpq -базирани клиентски драйвери и разработчици на libpq -базирани приложения. Пачът се прилага добре върху PostgreSQL 9.6beta1, ако искате да го изпробвате. Документацията е подробна и има изчерпателна примерна програма.

Ефективност

Мислех, че хоствана услуга за база данни като RDS или Heroku Postgres ще бъде добър пример за това къде този вид функционалност би била полезна. По-специално, достъпът до тях от нашата собствена мрежа наистина показва колко много латентност може да навреди.

При ~320ms мрежово забавяне:

  • 500 вмъквания без пакетиране:167.0s
  • 500 вложки с пакетиране:1.2s

… което е над 120 пъти по-бързо.

Обикновено няма да изпълнявате приложението си през междуконтинентална връзка между сървъра на приложения и базата данни, но това служи за подчертаване на въздействието на латентността. Дори през unix сокет към localhost видях над 50% подобрение на производителността за 10 000 вмъквания.

Пакетиране в съществуващи приложения

За съжаление не е възможно автоматично да се активира групирането за съществуващи приложения. Приложенията трябва да използват малко по-различен интерфейс, където изпращат поредица от заявки и едва след това искат резултатите.

Би трябвало да е сравнително лесно адаптирането на приложения, които вече използват асинхронния libpq интерфейс, особено ако използват неблокиращ режим и select() /анкета() /epoll() /WaitForMultipleObjectsEx цикъл. Приложения, които използват синхронен libpq интерфейсите ще изискват повече промени.

Пакетиране в други клиентски драйвери

По същия начин клиентските драйвери, рамки и ORM обикновено ще се нуждаят от интерфейс и вътрешни промени, за да позволят използването на пакети. Ако вече използват цикъл за събития и неблокиращ вход/изход, те трябва да бъдат сравнително лесни за промяна.

Бих искал да видя потребителите на Python, Ruby и т.н., които имат достъп до тази функционалност, така че съм любопитен да видя кой се интересува. Представете си, че можете да направите това:

import psycopg2
conn = psycopg2.connect(...)
cur = conn.cursor()

# this is just an idea, this code does not work with psycopg2:
futures = [ cur.async_execute(sql) for sql in my_queries ]
for future in futures:
    result = future.result  # waits if result not ready yet
    ... process the result ...
conn.commit()

Асинхронното групово изпълнение не трябва да бъде сложно на ниво клиент.

COPY е най-бързо

Където практически клиентите трябва да предпочитат COPY . Ето някои резултати от моя лаптоп:

inserting 1000000 rows batched, unbatched and with COPY
batch insert elapsed:      23.715315s
sequential insert elapsed: 36.150162s
COPY elapsed:              1.743593s
Done.

Групирането на работата осигурява изненадващо голям тласък на производителността дори при връзка с локален unix сокет.... но КОПИРАНЕ оставя и двата отделни подхода далеч зад себе си в праха.

Използвайте COPY .

Изображението

Изображението за тази публикация е на тръбопровода Goldfields Water Supply Scheme от Mundaring Weir близо до Пърт в Западна Австралия до вътрешните (пустини) златни находища. Той е уместен, защото отне толкова време за завършване и беше подложен на толкова интензивна критика, че неговият дизайнер и основен поддръжник, C. Y. O’Connor, се самоуби 12 месеца преди да бъде пуснат в експлоатация. Местните хора често (неправилно) казват, че е починал след тръбопроводът е построен, когато не е текла вода – защото просто е отнело толкова време, всички предполагат, че проектът за тръбопровода е провален. След това седмици по-късно водата се изля.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Какъв е правилният индекс за запитване на структури в масиви в Postgres jsonb?

  2. Как да конвертирате Postgres база данни в sqlite

  3. Вземете името на изходната таблица на реда, когато отправяте заявка към родителя, от който той наследява

  4. Кръстосана таблица с голям или неопределен брой категории

  5. Нулирайте стойността на последователността като 1