Dalam perjalanan ke DBMS fungsional dan NoSQL ERP: penyimpanan saldo dan penetapan biaya

Halo, Habr!

Kami terus mempelajari penerapan prinsip-prinsip pemrograman fungsional dalam desain ERP. Pada artikel sebelumnya, kita berbicara tentang mengapa ini perlu, meletakkan dasar arsitektur, dan menunjukkan konstruksi konvolusi sederhana menggunakan contoh pernyataan terbalik. Sebenarnya, pendekatan sumber acara diusulkan, tetapi karena pemisahan basis data menjadi bagian yang tidak berubah dan dapat berubah, kita mendapatkan satu sistem kombinasi keuntungan dari peta / pengurangan penyimpanan dan DBMS dalam memori, yang memecahkan masalah kinerja dan masalah skalabilitas. Pada artikel ini saya akan memberi tahu (dan menunjukkan prototipe pada TypeScript dan Deno runtime ) bagaimana cara menyimpan register saldo instan dalam sistem tersebut dan menghitung biaya. Bagi mereka yang belum membaca artikel 1 - ringkasan singkat:

1. Jurnal dokumen . ERP yang dibangun berdasarkan RDBMS adalah keadaan yang sangat besar yang dapat berubah dengan akses kompetitif, oleh karena itu tidak dapat diukur, tidak dapat didengar dengan baik, dan tidak dapat diandalkan dalam operasinya (ini memungkinkan inkonsistensi data). Dalam ERP fungsional, semua data disusun dalam bentuk jurnal yang dipesan secara kronologis dari dokumen-dokumen primer yang tidak dapat diubah, dan tidak ada yang lain selain dokumen-dokumen ini. Tautan diselesaikan dari dokumen baru ke yang lama dengan ID lengkap (dan tidak pernah sebaliknya), dan semua data lainnya (saldo, register, perbandingan) dihitung konvolusi, yaitu, hasil cache dari fungsi murni pada aliran dokumen. Kurangnya status fungsi + kemampuan memberi kita peningkatan keandalan (blockchain sangat cocok dengan skema ini), dan sebagai bonus kami mendapatkan penyederhanaan skema penyimpanan + cache adaptif alih-alih keras (disusun berdasarkan tabel).

Seperti inilah tampilan fragmen data dalam ERP kami
//   { "type": "person", //  ,      "key": "person.0", //    "id": "person.0^1580006048190", //  +    ID "erp_type": "person.retail", "name": "   " } //  "" { "type": "purch", "key": "purch.XXX", "id": "purch.XXX^1580006158787", "date": "2020-01-21", "person": "person.0^1580006048190", //    "stock": "stock.0^1580006048190", //    "lines": [ { "nomen": "nomen.0^1580006048190", //    "qty": 10000, "price": 116.62545127448834 } ] } 

2. Kekebalan dan mutabilitas . Jurnal dokumen dibagi menjadi 2 bagian yang tidak sama:

  • Bagian yang besar dan tidak dapat diubah terletak pada file JSON, tersedia untuk dibaca berurutan, dan dapat disalin ke node server, memastikan konkurensi membaca. Konvolusi yang dihitung pada bagian yang tidak dapat diubah di-cache, dan sampai pergeseran, titik imunitas juga tidak berubah (yaitu direplikasi).
  • Bagian yang dapat berubah yang lebih kecil adalah data saat ini (dalam hal akuntansi - periode saat ini), di mana Anda dapat mengedit dan membatalkan dokumen (tetapi tidak menghapus), menyisipkan dan mengatur kembali hubungan secara surut (misalnya, mencocokkan penerimaan dengan pengeluaran, menghitung ulang biaya, dll. .). Data yang dapat diubah dimasukkan ke dalam memori secara keseluruhan, yang menyediakan perhitungan konvolusi cepat dan mekanisme transaksional yang relatif sederhana.

3. Konvolusi . Karena kurangnya GABUNG semantik, bahasa SQL tidak cocok, dan semua algoritma ditulis dalam filter / mengurangi gaya fungsional, ada juga pemicu (event handler) untuk jenis dokumen tertentu. Filter / pengurangan perhitungan disebut konvolusi. Algoritma konvolusi untuk pengembang aplikasi terlihat seperti lulus penuh melalui jurnal dokumen, namun, kernel melakukan optimasi selama eksekusi - hasil antara yang dihitung dari bagian yang tidak dapat diubah diambil dari cache dan kemudian "dihitung" dari bagian yang dapat diubah. Jadi, mulai dari peluncuran kedua, konvolusi dihitung seluruhnya dalam RAM, yang mengambil sepersekian detik pada satu juta dokumen (kami akan menunjukkan ini dengan contoh). Konvolusi dihitung pada setiap panggilan, karena sangat sulit untuk melacak semua perubahan dalam dokumen yang dapat berubah (pendekatan imperatif-reaktif), dan perhitungan dalam RAM murah, dan kode pengguna dengan pendekatan ini sangat disederhanakan. Konvolusi dapat menggunakan hasil konvolusi lain, mengekstraksi dokumen dengan ID, dan mencari dokumen di cache atas dengan kunci.

4. Versi dokumen dan caching . Setiap dokumen memiliki kunci unik dan ID unik (kunci + cap waktu). Dokumen dengan kunci yang sama diorganisasikan ke dalam grup, catatan terakhir adalah yang terkini (saat ini), dan sisanya bersifat historis.

Cache adalah segala sesuatu yang dapat dihapus dan dikembalikan lagi dari jurnal dokumen ketika database dimulai. Sistem kami memiliki 3 cache:

  • Cache dokumen dengan akses ID. Biasanya, ini adalah direktori dan dokumen semi permanen, seperti jurnal tingkat pengeluaran. Atribut caching (ya / tidak) terkait dengan jenis dokumen, cache diinisialisasi pada awal pertama database dan kemudian didukung oleh kernel.
  • Tembolok atas dokumen dengan akses kunci. Menyimpan versi terbaru dari entri direktori dan register instan (mis. Saldo dan saldo). Tanda perlunya caching teratas terkait dengan jenis dokumen, cache teratas diperbarui oleh kernel saat membuat / memodifikasi dokumen apa pun.
  • Cache konvolusi yang dihitung dari bagian database yang tidak dapat diubah adalah kumpulan pasangan kunci / nilai. Kunci konvolusi adalah representasi string dari kode algoritma + nilai awal serial dari akumulator (di mana parameter perhitungan input ditransmisikan), dan hasil konvolusi adalah nilai akhir akumulator serial (dapat berupa objek atau koleksi yang kompleks).

Penyimpanan saldo


Kami melanjutkan ke topik artikel - penyimpanan residu. Hal pertama yang terlintas dalam pikiran adalah untuk mengimplementasikan sisanya sebagai konvolusi, parameter input yang akan menjadi kombinasi analis (misalnya, nomenklatur + gudang + batch). Namun, dalam ERP kita perlu mempertimbangkan harga biaya, yang perlu untuk membandingkan biaya dengan saldo (algoritma FIFO, batch FIFO, rata-rata gudang - secara teoritis kita dapat meratakan biaya untuk setiap kombinasi analis). Dengan kata lain, kita memerlukan sisanya sebagai entitas independen, dan karena semuanya adalah dokumen dalam sistem kami, sisanya juga merupakan dokumen.

Sebuah dokumen dengan tipe “saldo” dihasilkan oleh pemicu pada saat memposting baris dokumen pembelian / penjualan / pergerakan, dll. Kunci saldo adalah kombinasi dari analis, saldo dengan kunci yang sama dari grup sejarah, elemen terakhir yang disimpan dalam cache atas dan langsung tersedia. Saldo bukan posting, dan karena itu tidak diringkas - catatan terakhir relevan, dan catatan paling awal menyimpan sejarah.

Saldo menyimpan jumlah dalam unit penyimpanan dan jumlah dalam mata uang utama, dan membagi yang kedua menjadi yang pertama - kami mendapatkan biaya instan di persimpangan analis. Dengan demikian, sistem tidak hanya menyimpan riwayat lengkap residu, tetapi juga riwayat lengkap biaya, yang merupakan nilai tambah untuk audit hasil. Saldo itu ringan, jumlah saldo maksimum sama dengan jumlah baris dokumen (sebenarnya lebih sedikit jika garis dikelompokkan berdasarkan kombinasi analis), jumlah catatan saldo teratas tidak tergantung pada volume database, dan ditentukan oleh jumlah kombinasi analis yang terlibat dalam mengendalikan saldo dan menghitung biaya, sehingga ukurannya Cache teratas kami selalu dapat diprediksi.

Posting Habis


Awalnya, saldo dibentuk oleh dokumen tanda terima dari jenis "pembelian" dan disesuaikan dengan dokumen pengeluaran apa pun. Misalnya, pemicu untuk dokumen penjualan melakukan hal berikut:

  • mengekstrak saldo saat ini dari cache teratas
  • memeriksa ketersediaan kuantitas
  • menyimpan tautan ke saldo saat ini di baris dokumen, dan biaya instan
  • menghasilkan neraca baru dengan jumlah dan jumlah yang dikurangi

Contoh perubahan keseimbangan saat menjual

 //    { "type": "bal", "key": "bal|nomen.0|stock.0", "id": "bal|nomen.0|stock.0^1580006158787", "qty": 11209, //  "val": 1392411.5073958784 //  } //  "" { "type": "sale", "key": "sale.XXX", "id": "sale.XXX^1580006184280", "date": "2020-01-21", "person": "person.0^1580006048190", "stock": "stock.0^1580006048190", "lines": [ { "nomen": "nomen.0^1580006048190", "qty": 20, "price": 295.5228788368553, //   "cost": 124.22263425781769, //  "from": "bal|nomen.0|stock.0^1580006158787" // - } ] } //    { "type": "bal", "key": "bal|nomen.0|stock.0", "id": "bal|nomen.0|stock.0^1580006184281", "qty": 11189, "val": 1389927.054710722 } 

Kode Kelas TypeScript Document Handler

 import { Document, DocClass, IDBCore } from '../core/DBMeta.ts' export default class Sale extends DocClass { static before_add(doc: Document, db: IDBCore): [boolean, string?] { let err = '' doc.lines.forEach(line => { const key = 'bal' + '|' + db.key_from_id(line.nomen) + '|' + db.key_from_id(doc.stock) const bal = db.get_top(key, true) // true -  ,    - const bal_qty = bal?.qty ?? 0 //   const bal_val = bal?.val ?? 0 //   if (bal_qty < line.qty) { err += '\n"' + key + '": requested ' + line.qty + ' but balance is only ' + bal_qty } else { line.cost = bal_val / bal_qty //     line.from = bal.id } }) return err !== '' ? [false, err] : [true,] } static after_add(doc: Document, db: IDBCore): void { doc.lines.forEach(line => { const key = 'bal' + '|' + db.key_from_id(line.nomen) + '|' + db.key_from_id(doc.stock) const bal = db.get_top(key, true) const bal_qty = bal?.qty ?? 0 const bal_val = bal?.val ?? 0 db.add_mut( { type: 'bal', key: key, qty: bal_qty - line.qty, val: bal_val - line.cost * line.qty // cost   before_add() } ) }) } } 

Tentu saja, adalah mungkin untuk tidak menyimpan biaya secara langsung dalam garis pengeluaran, tetapi mengambilnya dengan referensi dari neraca, tetapi faktanya adalah saldo adalah dokumen, ada banyak dari mereka, tidak mungkin untuk men-cache semuanya, dan mendapatkan dokumen dengan ID dengan membaca dari disk mahal ( cara mengindeks file berurutan untuk akses cepat - saya akan memberi tahu Anda waktu berikutnya).

Masalah utama yang ditunjukkan oleh komentator adalah kinerja sistem, dan kami memiliki segalanya untuk mengukurnya pada jumlah data yang relatif relevan.

Sumber data generasi


Sistem kami akan terdiri dari 5.000 rekanan (pemasok dan pelanggan), 3.000 item, 50 gudang, dan 100 ribu dokumen dari setiap jenis - pembelian, transfer, penjualan. Dokumen dihasilkan secara acak, rata-rata 8,5 baris per dokumen. Jalur pembelian dan penjualan menghasilkan satu transaksi (dan satu saldo), dan dua jalur pergerakan, menghasilkan 300 ribu dokumen primer menghasilkan sekitar 3,4 juta transaksi, yang konsisten dengan volume bulanan ERP provinsi. Kami menghasilkan bagian yang bisa berubah dengan cara yang sama, hanya dengan volume 10 kali lebih sedikit.

Kami menghasilkan dokumen dengan skrip . Mari kita mulai dengan pembelian, selama sisa dokumen, pemicu akan memeriksa saldo di persimpangan item dan gudang, dan jika setidaknya satu baris tidak lulus, skrip akan mencoba membuat dokumen baru. Saldo dibuat secara otomatis oleh pemicu, jumlah maksimum kombinasi analis sama dengan jumlah nomenklatur * jumlah gudang, mis. 150rb

Ukuran DB dan Cache


Setelah skrip selesai, kita akan melihat metrik basis data berikut:

  • bagian abadi: dokumen 3.7kk (primer 300rb , sisanya sisanya) - file 770 Mb
  • bagian yang bisa diubah: 370k dokumen (30k primer, sisanya saldo) - file 76 Mb
  • cache dokumen paling atas: dokumen 158k (referensi + potongan saldo saat ini) - file 20 MB
  • cache dokumen: dokumen 8.8k (hanya direktori) - file <1 Mb

Benchmarking


Inisialisasi basis. Dengan tidak adanya file cache, database pada mulanya mengimplementasikan pemindaian penuh:

  • file data yang tidak dapat diubah (mengisi cache untuk jenis dokumen yang di-cache) - 55 detik
  • file data yang dapat diubah (memuat seluruh data ke dalam memori dan memperbarui cache teratas) - 6 detik

Saat ada cache, menaikkan basis lebih cepat:

  • file data yang dapat diubah - 6 detik
  • file cache teratas - 1,8 detik
  • cache lainnya - kurang dari 1 detik

Konvolusi pengguna apa pun (misalnya, ambil skrip untuk membuat lembar omset) pada panggilan pertama meluncurkan pemindaian file yang tidak dapat diubah, dan data yang dapat diubah sudah dipindai dalam RAM:

  • file data abadi - 55 detik
  • array yang bisa berubah dalam memori - 0,2 detik

Dalam panggilan berikutnya, ketika parameter input cocok, kurangi () akan mengembalikan hasilnya dalam 0,2 detik , sambil melakukan hal berikut setiap kali:

  • mengekstraksi hasil dari pengurangan cache dengan kunci (dengan mempertimbangkan parameter)
  • memindai array yang dapat berubah dalam memori ( 370k dokumen)
  • “Menghitung” hasilnya dengan menerapkan algoritma konvolusi ke dokumen yang difilter ( 20k )

Hasilnya cukup menarik untuk volume data seperti itu, laptop single-core saya, tidak adanya DBMS sama sekali (kami tidak lupa bahwa ini hanya prototipe), dan algoritma satu langkah dalam bahasa TypeScript (yang masih dianggap sebagai pilihan sembrono untuk perusahaan- aplikasi backend).

Optimalisasi Teknis


Setelah memeriksa kinerja kode, saya menemukan bahwa lebih dari 80% dari waktu dihabiskan membaca file dan parsing Unicode, yaitu File.read () dan TextDecoder (). Decode () . Selain itu, antarmuka file tingkat tinggi di Deno hanya asinkron, dan seperti yang baru-baru ini saya ketahui , harga async / menunggu terlalu tinggi untuk tugas saya. Oleh karena itu, saya harus menulis pembaca sinkron saya sendiri, dan tanpa benar-benar mengganggu dengan optimasi, untuk meningkatkan kecepatan membaca murni sebanyak 3 kali, atau, jika Anda menghitung dengan parsing JSON - sebanyak 2 kali, pada saat yang sama secara global menyingkirkan sinkronisasi. Mungkin karya ini perlu ditulis ulang tingkat rendah (atau mungkin seluruh proyek). Menulis data ke disk juga sangat lambat, meskipun ini kurang penting untuk prototipe.

Langkah selanjutnya


1. Tunjukkan penerapan algoritma ERP berikut ini dalam gaya fungsional:

  • manajemen cadangan dan kebutuhan terbuka
  • perencanaan rantai pasokan
  • perhitungan biaya produksi dengan mempertimbangkan biaya overhead

2. Beralih ke format penyimpanan biner, mungkin ini akan mempercepat pembacaan file. Atau bahkan memasukkan semuanya ke dalam bahasa Mongo.

3. Transfer FuncDB dalam mode multi-pengguna. Sesuai dengan prinsip CQRS , pembacaan dilakukan langsung oleh node server ke mana file database yang tidak dapat diubah disalin (atau digeledah melalui jaringan), dan perekaman dilakukan melalui titik REST tunggal yang mengelola data yang dapat diubah, cache dan transaksi.

4. Akselerasi mendapatkan dokumen yang tidak di-cache oleh ID karena pengindeksan file sekuensial (yang tentu saja melanggar konsep algoritma single-pass kami, tetapi keberadaan segala kemungkinan selalu lebih baik daripada tidak ada).

Ringkasan


Sejauh ini, saya belum menemukan satu alasan pun untuk meninggalkan ide DBMS / ERP fungsional, karena terlepas dari non-universalitas dari DBMS untuk tugas tertentu (akuntansi dan perencanaan), kami memiliki kesempatan untuk mendapatkan beberapa peningkatan skalabilitas, kemampuan mendengar dan keandalan sistem target - semua berkat ketaatan dasar prinsip FP.

Kode proyek lengkap

Jika ada yang ingin bermain sendiri:

  • instal deno
  • klon repositori
  • jalankan skrip pembuatan basis data dengan kontrol residu (generate_sample_database_with_balanses.ts)
  • jalankan skrip contoh 1..4 berbaring di folder root
  • datang dengan contoh Anda sendiri, menyandikan, menguji, dan beri saya umpan balik

PS
Keluaran konsol dirancang untuk Linux, mungkin di bawah Windows esc-sequence tidak akan berfungsi dengan benar, tetapi saya tidak perlu memeriksanya :)

Terima kasih atas perhatian anda

Source: https://habr.com/ru/post/id485508/


All Articles