Firebird'da Verilerin Paralel Okunması
D.Simonov, V.Horsun
sürüm 1.0.5, 05.12.2023
Bu materyal, IBSurgeon www.ib-aid.com tarafından sponsor edilmiş ve desteklenerek oluşturulmuştur; IBSurgeon, HQbird (Firebird’ün gelişmiş dağıtımı) satıcısı ve Firebird için performans optimizasyonu, migrasyon ve teknik destek hizmetleri sağlayıcısıdır.
Materyal, Public Documentation License https://www.firebirdsql.org/file/documentation/html/en/licenses/pdl/public-documentation-license.html altında lisanslanmıştır.
İlgili materyaller:
- Ücretsiz kitap “Firebird 5’in Detaylı Yeni Özellikleri”: HTML ve PDF (119 sayfa) olarak mevcuttur.
Önsöz
Firebird 5.0, gbak yardımcı programını kullanarak yedek oluştururken paralellik kullanma yeteneğini, diğer paralel işlevlerin yanı sıra tanıttı. Başlangıçta bu işlev HQbird 2.5’te ortaya çıktı, ardından HQbird 3.0 ve 4.0’da, daha sonra Firebird 5.0’a taşındı.
Bu makalede, gbak yardımcı programı içinde paralel yedekler oluştururken kullanılan işlevleri ele alacağız. Ayrıca bunların uygulamalarınızda paralel veri okuma için nasıl kullanılabileceğini de göstereceğiz.
Burada Firebird motoru içinde SQL sorguları çalıştırırken tabloların paralel taranmasından değil, uygulamanız içindeki paralel akışlarda veri okumaktan bahsettiğimizi belirtmek önemlidir.
Örnek Araç FBCSVExport
Firebird DBMS’den paralel veri okumayı göstermek için, bir veya daha fazla tablodan verileri CSV formatına aktaran örnek bir yardımcı program yazılmıştır.
Açıklaması ve açık kaynak kodu burada: https://github.com/IBSurgeon/FBCSVExport.git
Yardımcı programın açıklamasında ve aşağıdaki makaleden görebileceğiniz gibi, paralel işleme sayesinde verileri dışa aktarmak ve diğer paralel işlemleri 1 iş parçacığına göre 2-10 kat daha hızlı gerçekleştirmek mümkündür (donanıma bağlı olarak).
Her türlü soru için lütfen [email protected] adresiyle iletişime geçin.
Paralel okuma
Birkaç tablodan verileri paralel olarak nasıl okuyacağımızı düşünelim. Bildiğiniz gibi Firebird, sorguların yalnızca her sorgu ayrı bir bağlantıda çalıştırılırsa paralel olarak çalıştırılmasına izin verir.
Bir çalışan iş parçacığı havuzu oluşturalım. Ana uygulama iş parçacığı da bir çalışan iş parçacığıdır, bu nedenle ek çalışan iş parçacıklarının sayısı N - 1 olmalıdır; burada N, toplam paralel çalışan sayısıdır. Her çalışan iş parçacığı kendi bağlantısını ve işlemini çalıştıracaktır.
İlk sorun: okunan verilerin tutarlılığı nasıl sağlanır?
Tutarlı veri okuma
Her çalışan iş parçacığı kendi bağlantısını ve kendi işlemini kullandığından, tutarsız okuma sorunu ortaya çıkar - tablo diğer kullanıcılar tarafından eşzamanlı olarak değiştirilirse, okunan veriler tutarsız olabilir. Tek iş parçacıklı modda, gbak SNAPSHOT izolasyon moduna sahip bir işlem kullanır; bu, SNAPSHOT işleminin başlangıcında tutarlı bilgilerin okunmasını mümkün kılar. Ancak burada birden fazla işlemimiz var ve aynı değişmez verileri okumak için aynı “anlık görüntüyü” görmeleri gereklidir.
SNAPSHOT izolasyon moduna sahip farklı işlemler için paylaşılan bir anlık görüntü oluşturma mekanizması Firebird 4.0’da tanıtıldı (başlangıçta HQBird 2.5’te, ancak Firebird 4.0/HQBird 4.0’da daha basit ve daha verimlidir). Paylaşılan bir anlık görüntü oluşturmanın iki yolu vardır:
- SQL ile
- ana işlemden (ana çalışan iş parçacığında başlatılan) anlık görüntü numarasını alın.
SELECT RDB$GET_CONTEXT('SYSTEM', 'SNAPSHOT_NUMBER') FROM RDB$DATABASE
- diğer işlemleri aşağıdaki SQL ile başlatın:
SET TRANSACTION SNAPSHOT AT NUMBER snapshot_number
burada snapshot_number, önceki sorgu tarafından alınan numaradır.
- API ile
-
ana işlemden (ana çalışan iş parçacığında başlatılan)
isc_transaction_infoveyaITransaction.getInfoişleviylefb_info_tra_snapshot_numberetiketiyle anlık görüntü numarasını alın; -
isc_tpb_at_snapshot_numberetiketiyle alınan anlık görüntü numarasıyla diğer işlemleri başlatın.
Örnek araç FBCSVExport ve gbak, ikinci yaklaşımı kullanır. Bu yaklaşımlar karıştırılabilir - örneğin, anlık görüntü numarasını SQL ile alın ve elde edilen anlık görüntü numarasını API ile diğer işlemleri başlatmak için kullanın veya tam tersi.
FBCSVExport içinde anlık görüntü numarasını aşağıdaki kodla alıyoruz:
ISC_INT64 getSnapshotNumber(Firebird::ThrowStatusWrapper* status, Firebird::ITransaction* tra)
{
ISC_INT64 ret = 0;
unsigned char in_buf[] = { fb_info_tra_snapshot_number, isc_info_end };
unsigned char out_buf[16] = { 0 };
tra->getInfo(status, sizeof(in_buf), in_buf, sizeof(out_buf), out_buf);
unsigned char* p = out_buf, * e = out_buf + sizeof(out_buf);
while (p < e)
{
short len = 0;
switch (*p++)
{
case isc_info_error:
case isc_info_end:
p = e;
break;
case fb_info_tra_snapshot_number:
len = static_cast(isc_vax_integer(reinterpret_cast<char*>(p), 2));
p += 2;
ret = isc_portable_integer(p, len);
p += len;
break;
}
}
return ret;
}
Anlık görüntü numarasıyla işlem başlatmak için aşağıdaki kodu kullanırız:
Firebird::AutoDispose tpbWorkerBuilder(fbUtil->getXpbBuilder(&status, Firebird::IXpbBuilder::TPB, nullptr, 0));
tpbWorkerBuilder->insertTag(&status, isc_tpb_concurrency);
tpbWorkerBuilder->insertBigInt(&status, isc_tpb_at_snapshot_number, snapshotNumber);
Firebird::AutoRelease workerTra(
workerAtt->startTransaction(
&status,
tpbWorkerBuilder->getBufferLength(&status),
tpbWorkerBuilder->getBuffer(&status)
)
);
Artık farklı bağlantılardan okunan veriler tutarlı olacaktır, bu nedenle yükü çalışan iş parçacıklarına dağıtabiliriz.
Yükü çalışan iş parçacıkları arasında tam olarak nasıl dağıtmalıyız? Tüm tabloların veya bir yedek kopyanın tamamen dışa aktarılması durumunda, en basit seçenek tablo başına bir çalışan iş parçacığı olacaktır. Ancak bu yaklaşımla şu sorunla karşılaşırız: veritabanında çok sayıda küçük tablo ve bir büyük tablo varsa veya hatta yalnızca bir tablo varsa ve çok büyükse, iyileşme göremeyiz. Bu durumda, bazı iş parçacıkları büyük bir tablo alır ve kalan iş parçacıkları boşta kalır. Bunun olmasını önlemek için, büyük bir tabloyu parçalar halinde işlemek gerekir.
| Not | Aşağıdaki materyal tabloların tamamen okunmasına ayrılmıştır; bazı sorgulardan (veya görünümden) paralel okuma düzenlemek istiyorsanız, gerçek verilere bağlı olarak biraz farklı bir yaklaşım gerekecektir. |
Büyük tabloyu parçalara bölme
Diyelim ki tamamını mümkün olduğunca hızlı okumak istediğimiz yalnızca bir büyük tablomuz var. Onu birkaç parçaya bölmek ve her parçayı kendi akışından bağımsız olarak okumak önerilir. Her iş parçacığının veritabanına kendi bağlantısı olmalıdır.
Bu durumda aşağıdaki sorular ortaya çıkar:
-
Tablo kaç işleme parçasına bölünmelidir?
-
Veri erişimi açısından tabloyu bölmenin en iyi yolu nedir?
Bu soruları sırayla yanıtlayalım.
Tablo kaç işleme parçasına bölünmelidir?
İdeal senaryoyu varsayalım - sunucu ve istemci Firebird’e ayrılmıştır, yani tüm CPU’lar tamamen bizim emrimizdedir. O zaman önerilir:
a) Maksimum paralel parça sayısı olarak sunucudaki CPU çekirdek sayısının iki katını kullanın. Neden 2x çekirdek? IO ile ilgili gecikmeler olacağını kesin olarak biliyoruz, bu nedenle CPU’nun biraz ekstra kullanımına izin verebiliriz. Ancak bu sayı başlangıç ayarı olarak düşünülmelidir, pratikte verilere bağlıdır.
b) İstemcideki çekirdek sayısını dikkate alın: sunucuda çok daha fazlası varsa (olağan durum), istemciyi aşırı yüklememek için bölüm parça sayısını daha da sınırlamak mantıklı olabilir (zaten daha fazlasını işleyemez ve akış değiştirme maliyetleri ortadan kalkmaz). İstemcinin CPU yükünü ve sunucuyu izleyerek daha kesin karar vermek mümkün olacaktır - istemcide %100 ise, ancak sunucuda belirgin şekilde daha azsa, parça sayısını azaltmak mantıklıdır.
c) istemci ve sunucu aynı ana bilgisayarsa, (a) maddesine bakın.
İstemci ve/veya sunucu başka bir şeyle meşgulse, parça sayısını azaltmanız gerekebilir. Sunucudaki disklerin aynı anda birçok IO isteğini işleme yeteneği de bunu etkileyebilir (kuyruk boyutunu ve yanıt süresini izleyin).
Veri erişimi açısından tabloyu bölmenin en iyi yolu nedir?
Etkili bir paralel işleme uygulamak için, işlerin işleyiciler arasında eşit dağılımını sağlamak ve karşılıklı senkronizasyonlarını en aza indirmek önemlidir. Ayrıca, işleyicilerin senkronizasyonunun hem sunucu tarafında hem de istemci tarafında gerçekleşebileceğini hatırlamanız gerekir. Örneğin, birden fazla işleyici aynı veritabanı bağlantısını kullanmamalıdır. Daha az belirgin bir örnek: farklı işleyicilerin aynı veritabanı sayfalarından kayıtlar okuması kötüdür. Örneğin, iki işleyici çift ve tek kayıtları okuduğunda - bu etkili değildir. İstemcide senkronizasyon, görevlerin dağıtımı sırasında, alınan verilerin işlenmesi sırasında (sonuçlar için bellek ayırma) vb. gerçekleşebilir.
“Adil” bölümlemenin sorunlarından biri, istemcinin kayıtların sayfalar arasında (ve dizin anahtarları arasında) nasıl dağıtıldığını, kaç kayıt veya veri sayfası olduğunu bilmemesidir (büyük tablolar için kayıt sayısını önceden saymak çok uzun sürecektir).
gbak‘ın bu sorunu nasıl çözdüğünü görelim.
gbak için bir iş birimi, aynı işaretçi sayfasına (PP) ait veri sayfalarındaki (DP) kayıt kümesidir. Bir yandan, işleyiciyi sık sık yeni bir veri parçası istemek zorunda kalmadan meşgul tutmak için oldukça büyük bir kayıt sayısıdır (senkronizasyon). Öte yandan, bu tür kayıt kümeleri tam olarak aynı boyutta olmasa bile, çalışanların nispeten eşit yüklenmesine izin verecektir. Yani, bir çalışanın bir PP’den N kayıt okuması, diğerinin M kayıt okuması oldukça olasıdır ve M, N’den oldukça farklı olabilir. Bu yaklaşım ideal değildir, ancak uygulanması oldukça basittir ve genellikle en azından büyük ölçeklerde (onlarca veya yüzlerce (veya daha fazla) PP ile) oldukça etkilidir.
Belirli bir tablo için PP (İşaretçi Sayfaları) sayısını nasıl alırsınız? RDB$PAGES tablosundan hesaplamak oldukça kolay ve en önemlisi hızlıdır:
SELECT RDB$PAGE_SEQUENCE
FROM RDB$PAGES
WHERE RDB$RELATION_ID = ? AND RDB$PAGE_TYPE = 4
ORDER BY RDB$PAGE_SEQUENCE DESC ROWS 1
Ardından, PP sayısını çalışan sayısına bölebilir ve her çalışana kendi parçasını verebiliriz. Veri dağılımını bilen geliştirici tarafından paralel işleme yapıldığı senaryo için bu iyidir. Ancak daha yaygın senaryo için, bu tür “büyük” parçaların aynı miktarda iş anlamına geleceğinin garantisi yoktur. 15 çalışanın işini bitirip boşta durduğu ve 16.’nın 10M kaydını uzun süre okuduğu bir durumu görmek istemiyoruz.
Bu yüzden gbak bunu farklı yapar. Her işlemciye aynı anda 1 PP veren bir iş koordinatörü vardır. Koordinatör toplam kaç PP olduğunu ve kaç tanesinin iş için verildiğini bilir. Çalışan kayıtlarını okumayı bitirdiğinde, yeni bir PP numarası için koordinatörle iletişime geçer. PP bitene kadar (veya aktif çalışanlar olana kadar) devam eder. Elbette, çalışanların koordinatörle bu tür etkileşimi senkronizasyon gerektirir. Deneyimler, bir PP’nin verdiği iş miktarının çok sık senkronize olmamaya izin verdiğini göstermektedir. Bu yaklaşım, her PP’ye ait gerçek kayıt sayısından bağımsız olarak tüm çalışanları (ve dolayısıyla CPU çekirdeklerini) pratik olarak eşit şekilde yüklemeye izin verir.
İşleyici, kayıtlarını PP’sinden nasıl okur? Bunu yapmak için, Firebird 4.0’dan başlayarak (ilk olarak HQBird 2.5’te ortaya çıktı) MAKE_DBKEY() adlı yerleşik bir fonksiyon vardır. Onun yardımıyla, belirtilen PP üzerindeki ilk kayıt için RDB$DB_KEY (fiziksel kayıt numarası) alabilirsiniz.
Ve bu RDB$DB_KEY‘lerin yardımıyla gerekli kayıtlar seçilir:
SELECT *
FROM relation
WHERE RDB$DB_KEY >= MAKE_DBKEY(:rel_id, 0, 0, :loPP)
AND RDB$DB_KEY < MAKE_DBKEY(:rel_id, 0, 0, :hiPP)
Örneğin, loPP = 0 ve hiPP = 1 ayarlarsanız, PP = 0 olan tüm kayıtlar okunur ve yalnızca ondan.
Artık gbak‘ın nasıl çalıştığı hakkında bir fikriniz olduğuna göre, FBCSVExport yardımcı programının uygulanmasının açıklamasına geçebilirsiniz.
FBCSVExport yardımcı programının uygulanması
FBCSVExport yardımcı programı, Firebird veritabanı tablolarındaki verileri CSV formatına dışa aktarmak için tasarlanmıştır.
Her tablo, .csv adlı bir dosyaya dışa aktarılır. Normal (tek iş parçacıklı modda) tablolardaki veriler, tablo adlarının alfabetik sırasına göre sırayla dışa aktarılır.
Paralel modda, tablolar paralel olarak dışa aktarılır ve her tablo ayrı bir iş parçacığında işlenir. Tablo çok büyükse, parçalara bölünür ve her parça ayrı bir akışta dışa aktarılır. Büyük bir tablonun her parçası için .csv.partN adında ayrı bir dosya oluşturulur; burada N parça numarasıdır.
Büyük bir tablonun tüm parçaları dışa aktarıldığında, parça dosyaları .csv adlı bir dosyada birleştirilir.
Hangi tabloların dışa aktarılacağını belirtmek için normal bir ifade kullanılır. Yalnızca normal tablolar dışa aktarılabilir (sistem tabloları, GTT, görünümler, harici tablolar desteklenmez). Normal ifadeler SQL sözdiziminde olmalıdır, yani SIMILAR TO yüklemi içinde kullanılanlar.
Dışa aktarılacak tabloların listesini ve çok iş parçacıklı modda PP’lerinin listesini seçmek için aşağıdaki sorguyu kullanırız:
SELECT
R.RDB$RELATION_ID AS RELATION_ID,
TRIM(R.RDB$RELATION_NAME) AS RELATION_NAME,
P.RDB$PAGE_SEQUENCE AS PAGE_SEQUENCE,
COUNT(P.RDB$PAGE_SEQUENCE) OVER(PARTITION BY R.RDB$RELATION_NAME) AS PP_CNT
FROM RDB$RELATIONS R
JOIN RDB$PAGES P ON P.RDB$RELATION_ID = R.RDB$RELATION_ID
WHERE R.RDB$SYSTEM_FLAG = 0 AND
R.RDB$RELATION_TYPE = 0 AND
P.RDB$PAGE_TYPE = 4 AND
TRIM(R.RDB$RELATION_NAME) SIMILAR TO CAST(? AS VARCHAR(8191))
ORDER BY R.RDB$RELATION_NAME, P.RDB$PAGE_SEQUENCE
Tek iş parçacıklı modda bu sorgu şu şekilde basitleştirilebilir:
SELECT
R.RDB$RELATION_ID AS RELATION_ID,
TRIM(R.RDB$RELATION_NAME) AS RELATION_NAME,
0 AS PAGE_SEQUENCE,
1 AS PP_CNT
FROM RDB$RELATIONS R
WHERE R.RDB$SYSTEM_FLAG = 0 AND
R.RDB$RELATION_TYPE = 0 AND
TRIM(R.RDB$RELATION_NAME) SIMILAR TO CAST(? AS VARCHAR(8191))
ORDER BY R.RDB$RELATION_NAME
Tek iş parçacıklı modda, PAGE_SEQUENCE ve PP_CNT alanlarının değerleri kullanılmaz; çıktı mesajlarını birleştirmek için isteğe eklenirler.
Bu sorgunun sonucu, yapıların bir vektörüne dönüştürülür:
struct TableDesc
{
TableDesc() = default;
TableDesc(const OutputRecord& rec)
: releation_id(rec->releation_id)
, relation_name(rec->relation_name.str, rec->relation_name.length)
, page_sequence(rec->page_sequence)
, pp_cnt(rec->pp_cnt)
{}
short releation_id;
std::string relation_name;
int32_t page_sequence;
int64_t pp_cnt;
};
Bu vektör, şu şekilde bildirilen bir fonksiyon kullanılarak doldurulur:
std::vector getTablesDesc(
Firebird::ThrowStatusWrapper* status,
Firebird::IAttachment* att,
Firebird::ITransaction* tra,
unsigned int sqlDialect,
const std::string& tableIncludeFilter,
bool singleWorker = true);
Son parametre singleWorker, std::vector doldurma modunu değiştirir; singleWorker = true ise tek iş parçacıklı mod için istek kullanılır, singleWorker = false ise çok iş parçacıklı mod için daha pahalı ve karmaşık bir sorgu kullanılır. Uygulamanın kendisini vermeyeceğim, oldukça basit ve projenin kaynak kodunda görebilirsiniz.
Bir tabloyu CSV formatına dışa aktarmak için, aşağıdaki yöntemleri içeren CSVExportTable sınıfı geliştirilmiştir:
void prepare(Firebird::ThrowStatusWrapper* status, const std::string& tableName,
unsigned int sqlDialect, bool withDbkeyFilter = false);
void printHeader(Firebird::ThrowStatusWrapper* status, csv::CSVFile& csv);
void printData(Firebird::ThrowStatusWrapper* status, csv::CSVFile& csv, int64_t ppNum = 0);
prepare yöntemi, bir tabloyu CSV formatına dışa aktarmak için kullanılan bir sorgu oluşturmak ve hazırlamak içindir. İç sorgu, withDbkeyFilter parametresine bağlı olarak farklı şekilde oluşturulur. withDbkeyFilter = true ise, sorgu RDB$DB_KEY aralığına göre filtreleme ile oluşturulur:
SELECT *
FROM tableName
WHERE RDB$DB_KEY >= MAKE_DBKEY('tableName', 0, 0, ?)
AND RDB$DB_KEY < MAKE_DBKEY('tableName', 0, 0, ?)
aksi takdirde basitleştirilmiş bir sorgu kullanılır:
SELECT *
FROM tableName
withDbkeyFilter parametresinin değeri, çok iş parçacıklı mod kullanılıyorsa ve tablo büyükse true olarak ayarlanır. pp_cnt > 1 ise tabloyu büyük olarak kabul ederiz.
printHeader yöntemi, bir CSV dosyasının başlığını (tablo sütun adları) yazdırmak içindir.
printData yöntemi, istek RDB$DB_KEY aralığına göre filtre kullanılarak hazırlandıysa, PP sayfa numarası ppNum‘dan CSV dosyasına tablo verilerini yazdırır; aksi takdirde tüm tablo verilerini yazdırır.
Şimdi tek iş parçacıklı modun koduna bakalım:
...
// Ana bağlantıyı açma
Firebird::AutoRelease att(
provider->attachDatabase(
&status,
m_database.c_str(),
dbpLength,
dpb
)
);
// Ana işlemi SNAPSHOT izolasyon modunda başlatma
Firebird::AutoDispose tpbBuilder(fbUtil->getXpbBuilder(&status, Firebird::IXpbBuilder::TPB, nullptr, 0));
tpbBuilder->insertTag(&status, isc_tpb_concurrency);
Firebird::AutoRelease tra(
att->startTransaction(
&status,
tpbBuilder->getBufferLength(&status),
tpbBuilder->getBuffer(&status)
)
);
// m_filter içindeki normal ifadeyi kullanarak tabloların listesini alın.
// m_parallel, paralel iş parçacığı sayısını ayarlar; 1'e eşit olduğunda,
// tabloların listesini almak için basitleştirilmiş bir sorgu kullanılır,
// aksi takdirde her tablo için PP'lerin listesi ve sayıları oluşturulur.
auto tables = getTablesDesc(&status, att, tra, m_sqlDialect, m_filter, m_parallel == 1);
if (m_parallel == 1) {
FBExport::CSVExportTable csvExport(att, tra, fb_master);
for (const auto& tableDesc : tables) {
// burada RDB$DB_KEY aralık filtresi kullanmanın bir anlamı yok
csvExport.prepare(&status, tableDesc.relation_name, m_sqlDialect, false);
const std::string fileName = tableDesc.relation_name + ".csv";
csv::CSVFile csv(m_outputDir / fileName);
if (m_printHeader) {
csvExport.printHeader(&status, csv);
}
csvExport.printData(&status, csv);
}
}
Buradaki her şey oldukça basit ve ek açıklama gerektirmiyor, bu yüzden çok iş parçacıklı kısma geçelim.
Dışa aktarmanın çok iş parçacıklı modda gerçekleşmesi için ek m_parallel - 1 işçi iş parçacığı oluşturmak gerekir. Ek iş parçacığı sayısı neden 1 eksik? Evet, çünkü ana iş parçacığı da verileri dışa aktaracak ve ek iş parçacıklarıyla eşittir. Ana ve ek akışın ortak kısmını ayrı bir fonksiyona taşıyalım:
void ExportApp::exportByTableDesc(Firebird::ThrowStatusWrapper* status, FBExport::CSVExportTable& csvExport, const TableDesc& tableDesc)
{
// tableDesc pp_cnt > 1 ise, yalnızca tablonun bir kısmını tanımlar ve
// RDB$DB_KEY aralık filtresi kullanarak sorgu oluşturmak gerekir.
bool withDbKeyFilter = tableDesc.pp_cnt > 1;
csvExport.prepare(status, tableDesc.relation_name, m_sqlDialect, withDbKeyFilter);
std::string fileName = tableDesc.relation_name + ".csv";
// Bu tablonun ilk parçası değilse, bu parçayı .csv.part dosyasına yazın; burada
// N - PP numarası. Daha sonra tablo parçaları tek bir .csv dosyasında birleştirilecektir.
if (tableDesc.page_sequence > 0) {
fileName += ".part_" + std::to_string(tableDesc.page_sequence);
}
csv::CSVFile csv(m_outputDir / fileName);
// CSV dosyasının başlığı yalnızca tablonun ilk parçasında yazdırılmalıdır.
if (tableDesc.page_sequence == 0 && m_printHeader) {
csvExport.printHeader(status, csv);
}
csvExport.printData(status, csv, tableDesc.page_sequence);
}
Tabloların veya parçalarının açıklamaları, TableDesc yapılarına sahip ortak bir vektörde bulunur. Bu vektörden her işçi iş parçacığı bir tablo veya sonraki parçayı alır. Veri yarışlarını önlemek için paylaşılan kaynağa erişimi senkronize etmek gerekir. Ancak std::vector‘ün kendisi değişmez, bu nedenle yalnızca bu vektördeki dizin olan paylaşılan değişkeni senkronize edebilirsiniz. Bu, std::atomic kullanılarak kolayca yapılabilir.
if (m_parallel == 1) {
...
}
else {
// Ek işçi iş parçacığı sayısını belirleme
const auto workerCount = m_parallel - 1;
// Ana işlemden anlık görüntü numarasını alma
auto snapshotNumber = getSnapshotNumber(&status, tra);
// iş parçacığı içindeki istisnayı saklamak için değişken
std::exception_ptr exceptionPointer = nullptr;
std::mutex m;
// atomik sayaç
// sonraki tablonun veya parçasının dizinidir
std::atomic<size_t> counter = 0;
// işçi iş parçacığı havuzu
std::vector<std::thread> thread_pool;
thread_pool.reserve(workerCount);
for (int i = 0; i < workerCount; i++) {
// her iş parçacığı için kendi bağlantımızı oluştururuz
Firebird::AutoRelease workerAtt(
provider->attachDatabase(
&status,
m_database.c_str(),
dbpLength,
dpb
)
);
// ve paylaşılan bir anlık görüntü oluşturmak için
// anlık görüntü numarasını ilettiğimiz kendi işlemimiz
Firebird::AutoDispose tpbWorkerBuilder(fbUtil->getXpbBuilder(&status, Firebird::IXpbBuilder::TPB, nullptr, 0));
tpbWorkerBuilder->insertTag(&status, isc_tpb_concurrency);
tpbWorkerBuilder->insertBigInt(&status, isc_tpb_at_snapshot_number, snapshotNumber);
Firebird::AutoRelease workerTra(
workerAtt->startTransaction(
&status,
tpbWorkerBuilder->getBufferLength(&status),
tpbWorkerBuilder->getBuffer(&status)
)
);
// bir iş parçacığı oluştur
std::thread t([att = std::move(workerAtt), tra = std::move(workerTra), this,
&m, &tables, &counter, &exceptionPointer]() mutable {
Firebird::ThrowStatusWrapper status(fb_master->getStatus());
try {
FBExport::CSVExportTable csvExport(att, tra, fb_master);
while (true) {
// atomik sayacı artır
size_t localCounter = counter++;
// tablolar veya parçaları bittiyse, çık
// sonsuz döngüden çık ve iş parçacığını bitir
if (localCounter >= tables.size())
break;
// tablonun veya parçasının açıklamasını al
const auto& tableDesc = tables[localCounter];
// ve dışa aktarımı yap
exportByTableDesc(&status, csvExport, tableDesc);
}
if (tra) {
tra->commit(&status);
tra.release();
}
if (att) {
att->detach(&status);
att.release();
}
}
catch (...) {
// bir istisna oluşursa, sonraki için sakla
// ana iş parçacığında serbest bırakma
std::unique_lock<std::mutex> lock(m);
exceptionPointer = std::current_exception();
}
});
thread_pool.push_back(std::move(t));
}
// export in the main thread
FBExport::CSVExportTable csvExport(att, tra, fb_master);
while (true) {
// increment the atomic counter
size_t localCounter = counter++;
if (localCounter >= tables.size())
break;
// if the tables or their parts are over, exit
// from an endless loop
const auto& tableDesc = tables[localCounter];
exportByTableDesc(&status, csvExport, tableDesc);
}
// wait for the worker threads to complete
for (auto& th : thread_pool) {
th.join();
}
// if there was an exception in the worker threads, throw it again
if (exceptionPointer) {
std::rethrow_exception(exceptionPointer);
}
...
Geriye kalan tek şey, tabloların parçaları için oluşturulan dosyaları bu tabloların her biri için tek bir dosyada birleştirmektir.
for (size_t i = 0; i < tables.size(); i++) {
const auto& tableDesc = tables[i];
// if the number of PP is greater than 1,
// then the table is large and there were several parts for it
if (tableDesc.pp_cnt > 1) {
// main file for the table
std::string fileName = tableDesc.relation_name + ".csv";
std::ofstream ofile(m_outputDir / fileName, std::ios::out | std::ios::app);
i++;
for (int64_t j = 1; j < tableDesc.pp_cnt; j++, i++) {
// files of table parts
std::string partFileName = fileName + ".part_" + std::to_string(j);
auto partFilePath = m_outputDir / partFileName;
std::ifstream ifile(partFilePath, std::ios::in);
ofile << ifile.rdbuf();
ifile.close();
fs::remove(partFilePath);
}
ofile.close();
}
}
Aracın performansını tek iş parçacıklı ve çok iş parçacıklı modda ölçelim.
FBCSVExport aracının kıyaslama testi
Öncelikle, orta düzey bir ev bilgisayarında çok iş parçacıklı ve tek iş parçacıklı dışa aktarma modlarının karşılaştırma sonuçlarına bakalım. === Windows
-
İşletim sistemi: Windows 10 x64.
-
İşlemci: Intel Core i3 8100, 4 çekirdek, 4 iş parçacığı.
-
Bellek: 16 GB
-
Disk alt sistemi: NVME SSD (veritabanı), SATA SSD (CSV dosyalarını saklama klasörü).
-
Firebird 4.0.4 x64
Sonuçlar:
CSVExport.exe -H --table-filter="COLOR|BREED|HORSE|COVER|MEASURE|LAB_LINE|SEX" --parallel=1 \
-d inet://localhost:3054/horses -u SYSDBA -p masterkey --charset=WIN1251 -o ./single
Elapsed time in milliseconds parallel_part: 35894 ms
Elapsed time in milliseconds: 36317 ms
CSVExport.exe -H --table-filter="COLOR|BREED|HORSE|COVER|MEASURE|LAB_LINE|SEX" --parallel=2 \
-d inet://localhost:3054/horses -u SYSDBA -p masterkey --charset=WIN1251 -o ./multi
Elapsed time in milliseconds parallel_part: 19259 ms
Elapsed time in milliseconds: 20760 ms
CSVExport.exe -H --table-filter="COLOR|BREED|HORSE|COVER|MEASURE|LAB_LINE|SEX" --parallel=4 \
-d inet://localhost:3054/horses -u SYSDBA -p masterkey --charset=WIN1251 -o ./multi
Elapsed time in milliseconds parallel_part: 19600 ms
Elapsed time in milliseconds: 21137 ms
Test sonuçlarından, iki iş parçacığı kullanıldığında hızlanmanın 1,8 kat olduğu açıkça görülüyor; bu iyi bir sonuçtur. Ancak 4 iş parçacığında paralel dışa aktarma da 1,8 kat iyileşme gösterdi. Neden 3-4 kat değil? Gerçek şu ki, Firebird sunucusu ve dışa aktarma yardımcı programı, yalnızca 4 çekirdeğe sahip aynı bilgisayarda çalışıyor. Böylece, Firebird sunucusu tabloyu okumak için 4 iş parçacığı kullanır ve FBCSVExport yardımcı programı da 4 iş parçacığı kullanır. Açıkçası, bu durumda 2 kattan fazla bir hızlanma elde etmek oldukça zordur. Bu nedenle, çekirdek sayısının önemli ölçüde daha fazla olduğu başka bir donanım üzerinde deneyeceğiz.
Linux
-
İşletim sistemi: CentOS 8.
-
İşlemci: 2 adet Intel Xeon E5-2603 v4 işlemci, toplam 12 çekirdek, 12 iş parçacığı.
-
Bellek: 32 GB
-
Disk alt sistemi: SAS HDD (RAID 10)
-
Firebird 4.0.4 x64
Sonuçlar:
[denis@copyserver build]$ ./CSVExport -H --table-filter="COLOR|BREED|HORSE|COVER|MEASURE|LAB_LINE|SEX" --parallel=1 \
-d inet://localhost/horses -u SYSDBA -p masterkey --charset=UTF8 -o ./single
Elapsed time in milliseconds parallel_part: 57547 ms
Elapsed time in milliseconds: 57595 ms
[denis@copyserver build]$ ./CSVExport -H --table-filter="COLOR|BREED|HORSE|COVER|MEASURE|LAB_LINE|SEX" --parallel=4 \
-d inet://localhost/horses -u SYSDBA -p masterkey --charset=UTF8 -o ./multi
Elapsed time in milliseconds parallel_part: 17755 ms
Elapsed time in milliseconds: 18148 ms
[denis@copyserver build]$ ./CSVExport -H --table-filter="COLOR|BREED|HORSE|COVER|MEASURE|LAB_LINE|SEX" --parallel=6 \
-d inet://localhost/horses -u SYSDBA -p masterkey --charset=UTF8 -o ./multi
Elapsed time in milliseconds parallel_part: 13243 ms
Elapsed time in milliseconds: 13624 ms
[denis@copyserver build]$ ./CSVExport -H --table-filter="COLOR|BREED|HORSE|COVER|MEASURE|LAB_LINE|SEX" --parallel=12 \
-d inet://localhost/horses -u SYSDBA -p masterkey --charset=UTF8 -o ./multi
Elapsed time in milliseconds parallel_part: 12712 ms
Elapsed time in milliseconds: 13140 ms
Bu durumda, dışa aktarma için en uygun iş parçacığı sayısı 6’dır (Firebird için 6 iş parçacığı ve FBCSVExport yardımcı programı için 6 iş parçacığı). Aynı zamanda, oldukça iyi bir ölçeklenebilirlik gösteren 5 kat hızlanma elde etmeyi başardık. Linux sunucusunda ve Windows bilgisayarında aynı veritabanlarını kullandık ve muhtemelen fark etmişsinizdir ki, Windows’ta tek iş parçacıklı dışa aktarma neredeyse 2 kat daha hızlıydı: bunun nedeni daha hızlı disk alt sistemidir (NVME sürücü, RAID’de birleştirilmiş SAS sürücülerinden çok daha hızlıdır).
Özet
Bu makalede, paralellik kullanarak Firebird DBMS tablolarından verileri etkili bir şekilde nasıl okuyacağımızı inceledik. Ayrıca, kendi yazılımınızda bu tür okumayı düzenlemek için Firebird DBMS’nin bazı yeteneklerini nasıl kullanabileceğinize dair bir örnek gösterildi.
Bu materyal için yardımlarından dolayı Firebird çekirdek geliştiricisi Vladislav Khorsun’a çok teşekkür ederiz.
Herhangi bir soru veya yorum için lütfen [email protected] adresine e-posta gönderin.