Սպասե՛ք։ Սպասե՛ք։ Ճիշտ է, սա հերթական հոդվածը չէ SQL Server-ի կրկնօրինակումների տեսակների մասին: Ես նույնիսկ չեմ խոսի վերականգնման մոդելների միջև եղած տարբերությունների և գերաճած գերանի հետ վարվելու մասին:
Միգուցե (ուղղակի գուցե), այս գրառումը կարդալուց հետո դուք կկարողանաք համոզվել, որ ստանդարտ միջոցների միջոցով ձեզանից հեռացված կրկնօրինակը կհեռացվի վաղը երեկոյան, լավ, 1.5 անգամ ավելի արագ: Եվ միայն այն բանի շնորհիվ, որ դուք օգտագործում եք մի փոքր ավելի շատ BACKUP DATABASE պարամետրեր:
Եթե գրառման բովանդակությունը ձեզ համար ակնհայտ էր, կներեք։ Ես կարդացի այն ամենը, ինչ Google-ը ստացել է «habr sql սերվերի կրկնօրինակում» արտահայտության համար, և ոչ մի հոդվածում չեմ գտել որևէ հիշատակում այն մասին, որ պահուստավորման ժամանակը կարող է ինչ-որ կերպ ազդել պարամետրերի միջոցով:
Անմիջապես ձեր ուշադրությունը կհրավիրեմ Ալեքսանդր Գլադչենկոյի մեկնաբանության վրա (
Երբեք մի փոխեք BUFFERCOUNT, BLOCKSIZE, MAXTRANSFERSIZE պարամետրերը արտադրության մեջ: Դրանք ստեղծված են միայն նման հոդվածներ գրելու համար։ Գործնականում դուք կարճ ժամանակում կազատվեք հիշողության խնդիրներից։
Իհարկե, լավ կլիներ լինել ամենախելացիը և տեղադրել բացառիկ բովանդակություն, բայց, ցավոք, դա այդպես չէ: Այս թեմային նվիրված կան և՛ անգլերեն, և՛ ռուսալեզու հոդվածներ/գրառումներ (ես միշտ շփոթված եմ, թե ինչպես ճիշտ անվանել դրանք): Ահա դրանցից մի քանիսը, որոնց հանդիպեցի.
Այսպիսով, սկզբից ես կկցեմ մի փոքր հանված ՊԱՀԵԽՆՎԱԾ շարահյուսություն
BACKUP DATABASE { database_name | @database_name_var }
TO <backup_device> [ ,...n ]
<...>
[ WITH { <...>
| <general_WITH_options> [ ,...n ] } ]
[;]
<general_WITH_options> [ ,...n ]::=
<...>
--Media Set Options
<...>
| BLOCKSIZE = { blocksize | @blocksize_variable }
--Data Transfer Options
BUFFERCOUNT = { buffercount | @buffercount_variable }
| MAXTRANSFERSIZE = { maxtransfersize | @maxtransfersize_variable }
<...>
<…> - դա նշանակում է, որ այնտեղ ինչ-որ բան է եղել, բայց ես այն հանել եմ, քանի որ այժմ այն չի առնչվում թեմային:
Ինչպե՞ս եք սովորաբար կրկնօրինակում կատարում: Ինչպե՞ս են նրանք «սովորեցնում» ինչպես կրկնօրինակումներ վերցնել միլիարդավոր հոդվածներից: Ընդհանրապես, եթե ինձ անհրաժեշտ լինի միանվագ կրկնօրինակում կատարել ոչ շատ մեծ տվյալների բազայից, ես ավտոմատ կերպով կգրեմ այսպիսի մի բան.
BACKUP DATABASE smth
TO DISK = 'D:Backupsmth.bak'
WITH STATS = 10, CHECKSUM, COMPRESSION, COPY_ONLY;
--ладно, CHECKSUM я написал только чтобы казаться умнее
Եվ, ընդհանուր առմամբ, այստեղ թվարկված են բոլոր պարամետրերի 75-90%-ը, որոնք սովորաբար նշվում են կրկնօրինակների մասին հոդվածներում։ Դե կա նաև INIT, SKIP։ Դուք այցելե՞լ եք MSDN: Տեսե՞լ եք, որ մեկուկես էկրանի տարբերակներ կան։ Ես էլ տեսա...
Դուք հավանաբար արդեն հասկացել եք, որ հետագայում մենք կխոսենք կոդի առաջին բլոկում մնացած երեք պարամետրերի մասին՝ BLOCKSIZE, BUFFERCOUNT և MAXTRANSFERSIZE: Ահա նրանց նկարագրությունները MSDN-ից.
BLOCKSIZE = { բլոկի չափը | @ blocksize_variable } - ցույց է տալիս ֆիզիկական բլոկի չափը բայթերով: Աջակցվող չափերն են 512, 1024, 2048, 4096, 8192, 16, 384 և 32 բայթ (768 ԿԲ): Կասետային սարքերի համար լռելյայն արժեքը 65 է, իսկ այլ սարքերի համար՝ 536: Սովորաբար այս պարամետրը անհրաժեշտ չէ, քանի որ BACKUP հայտարարությունը ավտոմատ կերպով ընտրում է սարքի համար բլոկի համապատասխան չափը: Բլոկի չափը սահմանելը բացահայտորեն անտեսում է բլոկի չափի ավտոմատ ընտրությունը:
BUFFERCOUNT = { բուֆերային հաշվարկ | @ buffercount_variable } - Սահմանում է I/O բուֆերների ընդհանուր թիվը, որոնք կօգտագործվեն պահուստավորման գործողության համար: Դուք կարող եք նշել ցանկացած դրական ամբողջ արժեք, սակայն մեծ թվով բուֆերներ կարող են առաջացնել հիշողությունից դուրս սխալ՝ Sqlservr.exe գործընթացում վիրտուալ հասցեների չափազանց մեծ տարածության պատճառով:
Բուֆերների կողմից օգտագործվող տարածքի ընդհանուր քանակը որոշվում է հետևյալ բանաձևով.
BUFFERCOUNT * MAXTRANSFERSIZE
.
MAXTRANFERSIZE = { maxtransfersize | @ maxtransfersize_variable } սահմանում է տվյալների փաթեթի ամենամեծ չափը, բայթերով, որպեսզի փոխանակվի SQL Server-ի և պահեստային հավաքածուի մեդիայի միջև: Աջակցվում են 65 բայթ (536 ԿԲ) բազմապատիկ մինչև 64 բայթ (4 ՄԲ):
Երդվում եմ. ես սա կարդացել եմ նախկինում, բայց մտքովս չի անցել, թե որքան մեծ ազդեցություն կարող են ունենալ դրանք արտադրողականության վրա: Ավելին, ըստ երևույթին, ինձ պետք է մի տեսակ «քամինգ աութ» անել և խոստովանել, որ հիմա էլ լրիվ չեմ հասկանում, թե կոնկրետ ինչ են անում։ Հավանաբար, ինձ պետք է ավելին կարդալ բուֆերացված I/O-ի և կոշտ սկավառակի հետ աշխատելու մասին: Մի օր ես դա կանեմ, բայց առայժմ ես պարզապես կարող եմ գրել մի սկրիպտ, որը կստուգի, թե ինչպես են այս արժեքներն ազդում կրկնօրինակի վերցման արագության վրա:
Ես փոքրիկ տվյալների բազա եմ պատրաստել՝ մոտ 10 ԳԲ չափով, դրեցի SSD-ի վրա, իսկ պահուստավորման գրացուցակը դրեցի HDD-ի վրա։
Ես ստեղծում եմ ժամանակավոր աղյուսակ՝ արդյունքները պահելու համար (ես դա ժամանակավոր չունեմ, այնպես որ կարող եմ ավելի մանրամասն ուսումնասիրել արդյունքները, բայց դուք ինքներդ որոշեք).
DROP TABLE IF EXISTS ##bt_results;
CREATE TABLE ##bt_results (
id int IDENTITY (1, 1) PRIMARY KEY,
start_date datetime NOT NULL,
finish_date datetime NOT NULL,
backup_size bigint NOT NULL,
compressed_size bigint,
block_size int,
buffer_count int,
transfer_size int
);
Սկրիպտի սկզբունքը պարզ է՝ տեղադրված հանգույցներ, որոնցից յուրաքանչյուրը փոխում է մեկ պարամետրի արժեքը, տեղադրեք այս պարամետրերը BACKUP հրամանի մեջ, պահպանեք պատմության հետ վերջին գրառումը msdb.dbo.backupset-ից, ջնջեք պահուստային ֆայլը և հաջորդ կրկնությունը։ . Քանի որ կրկնօրինակի կատարման տվյալները վերցված են պահուստային սարքից, ճշգրտությունը որոշ չափով կորել է (վայրկյանների կոտորակներ չկան), բայց մենք կպահպանենք դա:
Սկզբում դուք պետք է միացնեք xp_cmdshell-ը ջնջելու պահուստները (այնուհետև մի մոռացեք անջատել այն, եթե դրա կարիքը չունեք).
EXEC sp_configure 'show advanced options', 1;
EXEC sp_configure 'xp_cmdshell', 1;
RECONFIGURE;
EXEC sp_configure 'show advanced options', 0;
GO
Դե, իրականում.
DECLARE @tmplt AS nvarchar(max) = N'
BACKUP DATABASE [bt]
TO DISK = ''D:SQLServerbackupbt.bak''
WITH
COMPRESSION,
BLOCKSIZE = {bs},
BUFFERCOUNT = {bc},
MAXTRANSFERSIZE = {ts}';
DECLARE @sql AS nvarchar(max);
/* BLOCKSIZE values */
DECLARE @bs int = 4096,
@max_bs int = 65536;
/* BUFFERCOUNT values */
DECLARE @bc int = 7,
@min_bc int = 7,
@max_bc int = 800;
/* MAXTRANSFERSIZE values */
DECLARE @ts int = 524288, --512KB, default = 1024KB
@min_ts int = 524288,
@max_ts int = 4194304; --4MB
SELECT TOP 1
@bs = COALESCE (block_size, 4096),
@bc = COALESCE (buffer_count, 7),
@ts = COALESCE (transfer_size, 524288)
FROM ##bt_results
ORDER BY id DESC;
WHILE (@bs <= @max_bs)
BEGIN
WHILE (@bc <= @max_bc)
BEGIN
WHILE (@ts <= @max_ts)
BEGIN
SET @sql = REPLACE (REPLACE (REPLACE(@tmplt, N'{bs}', CAST(@bs AS nvarchar(50))), N'{bc}', CAST (@bc AS nvarchar(50))), N'{ts}', CAST (@ts AS nvarchar(50)));
EXEC (@sql);
INSERT INTO ##bt_results (start_date, finish_date, backup_size, compressed_size, block_size, buffer_count, transfer_size)
SELECT TOP 1 backup_start_date, backup_finish_date, backup_size, compressed_backup_size, @bs, @bc, @ts
FROM msdb.dbo.backupset
ORDER BY backup_set_id DESC;
EXEC xp_cmdshell 'del "D:SQLServerbackupbt.bak"', no_output;
SET @ts += @ts;
END
SET @bc += @bc;
SET @ts = @min_ts;
WAITFOR DELAY '00:00:05';
END
SET @bs += @bs;
SET @bc = @min_bc;
SET @ts = @min_ts;
END
Եթե հանկարծ պարզաբանման կարիք ունեք, թե ինչ է կատարվում այստեղ, գրեք մեկնաբանություններում կամ PM-ում։ Առայժմ ես ձեզ կասեմ միայն այն պարամետրերի մասին, որոնք ես դրել եմ ՊԱՇՏՊԱՆԱԿԱՆ ՏՎՅԱԼՆԵՐԻ ԲԱՆԱ-ում:
BLOCKSIZE-ի համար մենք ունենք արժեքների «փակ» ցուցակ, և ես չեմ կատարել կրկնօրինակում BLOCKSIZE < 4KB-ով: ՄԱՔՍՏՐԱՆՑԵԼ ցանկացած թիվ, որը 64 ԿԲ-ի բազմապատիկ է՝ 64 ԿԲ-ից մինչև 4 ՄԲ: Իմ համակարգի լռելյայն 1024 ԿԲ է, ես վերցրել եմ 512 - 1024 - 2048 - 4096:
BUFFERCOUNT-ով ավելի դժվար էր - կարող է լինել ցանկացած դրական թիվ, բայց հղումն ասում է
Msg 3013, Level 16, State 1, Line 7 BACKUP DATABASE-ը անսովոր կերպով ավարտվում է:
Msg 701, Level 17, State 123, Line 7 Այս հարցումը գործարկելու համար ռեսուրսների «լռելյայն» ռեսուրսների լողավազանում անբավարար համակարգային հիշողություն կա:
Համեմատության համար ես նախ ցույց կտամ կրկնօրինակի գործարկման արդյունքները՝ ընդհանրապես որևէ պարամետր չնշելով.
BACKUP DATABASE [bt]
TO DISK = 'D:SQLServerbackupbt.bak'
WITH COMPRESSION;
Դե, կրկնօրինակում և կրկնօրինակում.
Մշակված 1070072 էջ «bt» տվյալների բազայի համար, ֆայլ «bt» ֆայլ 1-ում:
Մշակված է 2 էջ «bt» տվյալների բազայի համար, «bt_log» ֆայլը ֆայլ 1-ում:
BACKUP DATABASE-ը հաջողությամբ մշակել է 1070074 էջ 53.171 վայրկյանում (157.227 ՄԲ/վ):
Սցենարն ինքնին, ստուգելով պարամետրերը, աշխատեց մի քանի ժամվա ընթացքում, բոլոր չափումները կատարվեցին
SELECT TOP 7 WITH TIES
compressed_size,
block_size,
buffer_count,
transfer_size,
DATEDIFF(SECOND, start_date, finish_date) AS backup_time_sec
FROM ##bt_results
ORDER BY backup_time_sec ASC;
Ուշադրություն, շատ կարևոր նշում
Մենք կարող ենք վստահորեն ասել, որ արժեքների այս միջակայքերում պարամետրերի և պահեստային արագության միջև կապը պատահական է, չկա օրինակ: Բայց ներկառուցված պարամետրերից հեռանալն ակնհայտորեն լավ է ազդել արդյունքի վրա
Նրանք. Միայն BACKUP-ի ստանդարտ պարամետրերը կառավարելով կրկնապատկվեց կրկնօրինակի հեռացման ժամանակը՝ 2 վայրկյան, սկզբում 26-ի դիմաց: Դա վատ չէ, չէ՞: Բայց մենք պետք է տեսնենք, թե ինչ կլինի վերականգնման հետ: Իսկ եթե հիմա 53 անգամ ավելի երկար պահանջվի վերականգնման համար:
Նախ, եկեք չափենք, թե որքան ժամանակ է պահանջվում կանխադրված կարգավորումներով կրկնօրինակը վերականգնելու համար.
RESTORE DATABASE [bt]
FROM DISK = 'D:SQLServerbackupbt.bak'
WITH REPLACE, RECOVERY;
Դե, դուք ինքներդ գիտեք, որ ճանապարհները կան, փոխարինումը փոխարինում չէ, վերականգնումը վերականգնում չէ: Եվ ես դա անում եմ այսպես.
Մշակված 1070072 էջ «bt» տվյալների բազայի համար, ֆայլ «bt» ֆայլ 1-ում:
Մշակված է 2 էջ «bt» տվյալների բազայի համար, «bt_log» ֆայլը ֆայլ 1-ում:
RESTORE DATABASE-ը հաջողությամբ մշակել է 1070074 էջ 40.752 վայրկյանում (205.141 ՄԲ/վ):
Այժմ ես կփորձեմ վերականգնել փոխված BLOCKSIZE-ով, BUFFERCOUNT-ով և MAXTRANSFERSIZE-ով արված կրկնօրինակները:
BLOCKSIZE = 16384, BUFFERCOUNT = 224, MAXTRANSFERSIZE = 4194304
RESTORE DATABASE-ը հաջողությամբ մշակել է 1070074 էջ 32.283 վայրկյանում (258.958 ՄԲ/վ):
BLOCKSIZE = 4096, BUFFERCOUNT = 448, MAXTRANSFERSIZE = 4194304
RESTORE DATABASE-ը հաջողությամբ մշակել է 1070074 էջ 32.682 վայրկյանում (255.796 ՄԲ/վ):
BLOCKSIZE = 16384, BUFFERCOUNT = 448, MAXTRANSFERSIZE = 2097152
RESTORE DATABASE-ը հաջողությամբ մշակել է 1070074 էջ 32.091 վայրկյանում (260.507 ՄԲ/վ):
BLOCKSIZE = 4096, BUFFERCOUNT = 56, MAXTRANSFERSIZE = 4194304
RESTORE DATABASE-ը հաջողությամբ մշակել է 1070074 էջ 32.401 վայրկյանում (258.015 ՄԲ/վ):
RESTORE DATABASE-ի հայտարարությունը չի փոխվում վերականգնման ընթացքում, այդ պարամետրերը նշված չեն դրանում, SQL Server-ը ինքն է որոշում դրանք կրկնօրինակից: Եվ պարզ է, որ նույնիսկ վերականգնման դեպքում կարող է լինել շահույթ՝ գրեթե 20% ավելի արագ (Անկեղծ ասած, ես շատ ժամանակ չէի ծախսել վերականգնման վրա, ես անցա մի քանի «ամենաարագ» կրկնօրինակումներով և համոզվեցի, որ վատթարացում չկա:).
Համենայն դեպս, պարզաբանեմ, որ սրանք որոշ պարամետրեր չեն, որոնք օպտիմալ են բոլորի համար։ Դուք կարող եք ստանալ ձեր համար օպտիմալ պարամետրերը միայն թեստավորման միջոցով: Ես ստացել եմ այս արդյունքները, դուք կստանաք տարբեր արդյունքներ: Բայց դուք տեսնում եք, որ կարող եք «կարգավորել» ձեր կրկնօրինակները, և դրանք իրականում կարող են ավելի արագ ձևավորվել և տեղակայվել:
Ես նաև խորհուրդ եմ տալիս ամբողջությամբ կարդալ փաստաթղթերը, քանի որ կարող են լինել ձեր համակարգին հատուկ նրբերանգներ:
Քանի որ ես սկսեցի գրել կրկնօրինակների մասին, ուզում եմ անմիջապես գրել ևս մեկ «օպտիմալացման» մասին, որն ավելի տարածված է, քան «թյունինգ» պարամետրերը (որքան հասկանում եմ, որ այն օգտագործվում է առնվազն որոշ պահեստային կոմունալ ծրագրերի կողմից, գուցե պարամետրերի հետ միասին: նկարագրված է ավելի վաղ), բայց այն դեռևս նկարագրված չէ Habré-ում:
Եթե մենք նայենք փաստաթղթերի երկրորդ տողին, հենց ՊԱՇՏՊԱՆԱԿԱՆ ՏՎՅԱԼՆԵՐԻ ԲԱԶԱՆԻ տակ, այնտեղ կտեսնենք.
TO <backup_device> [ ,...n ]
Ի՞նչ եք կարծում, ինչ տեղի կունենա, եթե նշեք մի քանի backup_devices: Շարահյուսությունը դա թույլ է տալիս։ Եվ շատ հետաքրքիր բան տեղի կունենա՝ կրկնօրինակը պարզապես «կտարածվի» մի քանի սարքերի վրա։ Նրանք. յուրաքանչյուր «սարք» առանձին-առանձին անիմաստ կլինի, կորցրեց մեկը, կորցրեց ամբողջ պահուստը: Բայց ինչպե՞ս կանդրադառնա նման աղտոտումը պահուստավորման արագության վրա:
Փորձենք կրկնօրինակել երկու «սարքերի» վրա, որոնք գտնվում են նույն թղթապանակում կողք կողքի.
BACKUP DATABASE [bt]
TO
DISK = 'D:SQLServerbackupbt1.bak',
DISK = 'D:SQLServerbackupbt2.bak'
WITH COMPRESSION;
Աշխարհի հայրեր, ինչո՞ւ է դա արվում։
Մշակված 1070072 էջ «bt» տվյալների բազայի համար, ֆայլ «bt» ֆայլ 1-ում:
Մշակված 2 էջ տվյալների բազայի «bt», ֆայլ «bt»log' ֆայլ 1-ում:
BACKUP DATABASE-ը հաջողությամբ մշակել է 1070074 էջ 40.092 վայրկյանում (208.519 ՄԲ/վ):
Արդյո՞ք կրկնօրինակը դարձել է 25%-ով ավելի արագ: Իսկ եթե ավելացնենք ևս մի քանի սարք:
BACKUP DATABASE [bt]
TO
DISK = 'D:SQLServerbackupbt1.bak',
DISK = 'D:SQLServerbackupbt2.bak',
DISK = 'D:SQLServerbackupbt3.bak',
DISK = 'D:SQLServerbackupbt4.bak'
WITH COMPRESSION;
BACKUP DATABASE-ը հաջողությամբ մշակել է 1070074 էջ 34.234 վայրկյանում (244.200 ՄԲ/վ):
Ընդհանուր առմամբ, շահույթը կազմում է կրկնօրինակում ստանալու ժամանակի մոտ 35% -ը միայն այն պատճառով, որ կրկնօրինակը գրվում է միանգամից 4 ֆայլի վրա մեկ սկավառակի վրա: Ես ստուգեցի ավելի մեծ թիվ - իմ նոութբուքի վրա շահույթ չկա, օպտիմալը `4 սարք: Ձեզ համար - չգիտեմ, դուք պետք է ստուգեք: Դե, ի դեպ, եթե դուք ունեք այս սարքերը, դրանք իսկապես տարբեր սկավառակներ են, շնորհավորում ենք, շահույթը պետք է լինի ավելի նշանակալի:
Հիմա եկեք խոսենք այն մասին, թե ինչպես վերականգնել այս երջանկությունը: Դա անելու համար դուք պետք է փոխեք վերականգնման հրամանը և նշեք բոլոր սարքերը.
RESTORE DATABASE [bt]
FROM
DISK = 'D:SQLServerbackupbt1.bak',
DISK = 'D:SQLServerbackupbt2.bak',
DISK = 'D:SQLServerbackupbt3.bak',
DISK = 'D:SQLServerbackupbt4.bak'
WITH REPLACE, RECOVERY;
RESTORE DATABASE-ը հաջողությամբ մշակել է 1070074 էջ 38.027 վայրկյանում (219.842 ՄԲ/վ):
Մի փոքր ավելի արագ, բայց ինչ-որ տեղ մոտ, ոչ նշանակալի: Ընդհանրապես, կրկնօրինակն ավելի արագ է հեռացվում և նույն կերպ վերականգնվում՝ հաջողություն? Ինչ վերաբերում է ինձ, ապա դա բավականին հաջողություն է: Սա կարեւոր է, այնպես որ ես կրկնում եմ, եթե դուք եթե կորցնեք այս ֆայլերից գոնե մեկը, կկորցնեք ամբողջ կրկնօրինակը.
Եթե մատյանում նայեք Trace Flags 3213 և 3605 օգտագործմամբ ցուցադրվող պահուստային տեղեկատվությանը, ապա կնկատեք, որ մի քանի սարքերի վրա պահուստավորելիս առնվազն BUFFERCOUNT-ի թիվը մեծանում է: Հավանաբար, դուք կարող եք փորձել ընտրել ավելի օպտիմալ պարամետրեր BUFFERCOUNT, BLOCKSIZE, MAXTRANSFERSIZE-ի համար, բայց ես անմիջապես չհաջողվեց, և ես չափազանց ծույլ էի նորից նման փորձարկում իրականացնել, բայց այլ թվով ֆայլերի համար: Եվ դա ամոթ է անիվների համար: Եթե ցանկանում եք տանը նման թեստավորում կազմակերպել, ապա սցենարը վերամշակելը դժվար չէ։
Ի վերջո, եկեք խոսենք գնի մասին: Եթե կրկնօրինակը հեռացվում է օգտատերերի աշխատանքին զուգահեռ, ապա պետք է շատ պատասխանատու մոտենալ թեստավորմանը, քանի որ եթե կրկնօրինակն ավելի արագ է հեռացվում, սկավառակներն ավելի են լարվում, պրոցեսորի ծանրաբեռնվածությունը մեծանում է (դեռ պետք է սեղմել դա թռչում է), և, համապատասխանաբար, համակարգի ընդհանուր արձագանքունակությունը նվազում է:
Պարզապես կատակում եմ, բայց ես հիանալի հասկանում եմ, որ ես որևէ բացահայտում չեմ արել: Այն, ինչ գրված է վերևում, պարզապես ցուցադրում է, թե ինչպես կարող եք ընտրել կրկնօրինակներ վերցնելու օպտիմալ պարամետրերը:
Հիշեք, որ այն ամենը, ինչ անում եք, արվում է ձեր սեփական վտանգի և ռիսկի ներքո: Ստուգեք ձեր կրկնօրինակները և մի մոռացեք DBCC CHECKDB-ի մասին:
Source: www.habr.com