Hola Habr!
En este artículo intentaremos analizar la arquitectura de los sistemas de contabilidad (ERP, CRM, WMS, MES, B2B, ...) desde la perspectiva de la programación funcional. Los sistemas existentes son complejos. Se basan en un esquema de datos relacionales y tienen un enorme estado mutable en forma de cientos de tablas relacionadas. Además, la única "fuente de verdad" en tales sistemas es un diario de documentos primarios ordenados cronológicamente (impresiones de eventos del mundo real), que, obviamente, deben ser inmutables (y esta regla se observa en los sistemas auditados donde se prohíben las correcciones anteriores). El diario de documentos representa el 20% del volumen de la base de datos por la fuerza, y todo lo demás son abstracciones intermedias y agregados, que son convenientes para trabajar en SQL, pero que requieren una sincronización constante con los documentos y entre sí.
Si volvemos a las fuentes (eliminamos la redundancia de datos y nos negamos a almacenar agregados) e implementamos todos los algoritmos comerciales en forma de funciones que se aplican directamente a la secuencia de documentos primarios, obtendremos un DBMS funcional y un ERP funcional incorporado. El problema de rendimiento se resuelve gracias a la
memorización , y la cantidad de código funcional será bastante proporcional a la cantidad de SQL declarativo, y no será más difícil de entender. En este artículo, demostraremos el enfoque mediante el desarrollo del archivo DBMS más simple en TypeScript y
Deno runtime (un análogo de Node.js), así como también probaremos el rendimiento de convoluciones usando tareas comerciales típicas como ejemplo.
¿Por qué es esto relevante?
1) Un estado mutable + redundancia de datos es malo, especialmente cuando es necesario asegurar su sincronización constante con el flujo de documentos. Esta es una fuente de posibles discrepancias de credenciales (el equilibrio no converge) y es difícil detectar efectos secundarios.
2) Un esquema relacional rígido para almacenar datos de origen e intermedios es costoso en Big Data, sistemas heterogéneos y en condiciones de cambio rápido, es decir, de hecho, en todas partes. Sugerimos almacenar documentos en su forma original, ordenados por tiempo, permitiendo conexiones "de nuevo a viejo" y nunca al revés. Esto permitirá que la mayoría de los agregados se calculen con algoritmos de un solo paso directamente de los documentos, y
no se necesitan todas las demás tablas.
3) SQL está desactualizado, ya que supone la disponibilidad de cualquier dato en cualquier momento, y en los sistemas distribuidos obviamente este no es el caso: al desarrollar algoritmos de Big Data, debe estar preparado para que algunos de los datos necesarios aparezcan más tarde, y algunos ya aparecieron antes. Esto requiere un pequeño replanteamiento del lenguaje de consulta y una preocupación consciente por el almacenamiento en caché.
4) Modern PL le permite crear un sistema receptivo que opera con millones de registros localmente en una computadora portátil, donde el RDBMS simplemente no se instala. Y si hablamos de servidores, el esquema propuesto tiene más posibilidades de paralelismo, incluso en clústeres SPARK.
Antecedentes
Después de haber trabajado durante bastante tiempo con varios software de negocios (contabilidad, planificación, WMS), me encontré con dos problemas en casi todas partes: la dificultad de hacer cambios en el esquema de datos y la caída frecuente de la productividad cuando se hicieron estos cambios. En general, estos sistemas tienen una estructura compleja, ya que se les imponen requisitos conflictivos:
1) Auditabilidad. Es necesario almacenar todos los documentos primarios sin cambios. La división en directorios y operaciones es muy condicional; en los sistemas para adultos, los directorios se limitan a las versiones, donde cada cambio se realiza mediante un documento especial. Por lo tanto, los documentos fuente son una parte inmutable del sistema, y es la única "fuente de verdad", y todos los demás datos se pueden restaurar a partir de él.
2) rendimiento de la consulta. Por ejemplo, al crear una línea de pedido de ventas, el sistema debe calcular el precio del producto, teniendo en cuenta los descuentos, para lo cual es necesario extraer el estado del cliente, su saldo actual, historial de compras, acciones actuales en la región, etc. Naturalmente, toda la información necesaria no puede calcularse "sobre la marcha", sino que debe estar disponible en forma semiacabada. Por lo tanto, los sistemas existentes almacenan abstracciones convenientes sobre líneas de documentos (contabilizaciones), así como agregados precalculados (registros de acumulación, segmentos de tiempo, saldos actuales, contabilizaciones resumidas, etc.). Su volumen es de hasta el 80% del tamaño de la base de datos, la estructura de la tabla está rígidamente fija, con cualquier cambio en los algoritmos: el programador debe encargarse de la actualización correcta de los agregados. De hecho, agrega que este es el estado mutable del sistema.
3) Desempeño transaccional. Al retener cualquier documento, debe contar todos los agregados, y esto generalmente es una operación de bloqueo. Por lo tanto, los algoritmos de actualización de agregados son el punto más doloroso del sistema, y cuando se realizan una gran cantidad de cambios, existe un riesgo significativo de que algo se rompa, y luego los datos se "corroerán", es decir, los agregados ya no corresponderán a los documentos. Esta situación es el azote de todos los proyectos de implementación y el apoyo posterior.
Establecemos los fundamentos de la nueva arquitectura.
1) Almacenamiento. La base de la base de datos es un diario de documentos ordenados cronológicamente que reflejan los hechos consumados del mundo real. Los directorios también son documentos, solo de larga duración. Tanto el documento como cada versión de la entrada del directorio son inmutables. Ningún otro dato en forma de transacciones / registros / saldos se almacena en el sistema (una fuerte declaración provocativa, ocurre de manera diferente en la vida, pero debe luchar por la
perfección ). El documento tiene varios atributos del sistema:
{
"sys": {
"code": "partner.1", // - ,
"ts": 1578263624612, //
"id": "partner.1^1578263624612", // ID
"cache": 1 //
},
...
}
code ts , , — .
cache, -, -,
id,
code.
, . () — ,
ts. , - , ( , , ).
2) . id. «sql foreign key» — , , , ,
id . . , , ( — , ).
3) . , (.. ) , , 2 —
. , , . — , . . — , 1, .
, - — , .
4) . — , , , — . - — filter(), reduce(), get(), gettop(), . JOIN, , , , . , , ,
id / code, , ( ).
5) , . :
- cache, reduce() -, -;
- id / code, ;
- reduce() , , .
, - «» , , , . , , ,
filter() ,
reduce() — . .
6) 3- . — . ,
reduce() , — . — , (.. ). -, . fullscan . — , - , .
7) . -. — - , ( ), — .
, - . TypeScript ,
Deno — TypeScript
WASM,
Rust API, ( ).
2- , JSON, "\
x01", . API 3- :
type Document = any
type Result = any
public async get(id: string): Promise<Document | undefined>
public async gettop(code: string): Promise<Document | undefined>
public async reduce(
filter: (result: Result, doc: Document) => Promise<boolean>,
reducer: (result: Result, doc: Document) => Promise<void>,
result: Result
): Promise<Result>
id,
code, , , .
filter().reduce(), , — , — .
reduce() , .
,
filter reducer, - . , ,
get() reduce(). ( ) (. ).
. , . , — , .
{
"sys": {
"code": "partner.1",
"ts": 1578263624612,
"id": "partner.1^1578263624612",
"cache": 1
},
"type": "partner.retail",
"name": " "
}
{
"sys": {
"code": "invent.1",
"ts": 1578263624612,
"id": "invent.1^1578263624612",
"cache": 1
},
"type": "invent.material",
"name": " 20"
}
type — , , .
code — .
{
"sys": {
"code": "purch.1",
"ts": 1578263624613,
"id": "purch.1^1578263624613"
},
"type": "purch",
"date": "2020-01-07",
"partner": "partner.3^1578263624612",
"lines": [
{
"invent": "invent.0^1578263624612",
"qty": 2,
"price": 232.62838134273366
},
{
"invent": "invent.1^1578263624917",
"qty": 24,
"price": 174.0459600393788
}
]
}
(
purch | sale), c ( ).
, , .
import { FuncDB } from './FuncDB.ts'
const db = FuncDB.open('./sample_database/')
let res = await db.reduce(
async (_, doc) => doc.type == 'sale', //
async (result, doc) => {
result.doccou++
doc.lines.forEach(line => { //
result.linecou++
result.amount += line.price * line.qty
})
},
{amount: 0, doccou: 0, linecou: 0} //
)
console.log(`
amount total = ${res.amount}
amount per document = ${res.amount / res.doccou}
lines per document = ${res.linecou / res.doccou}`
)
, Map.
class ResultRow { //
invent_name = ''
partner_name = ''
debit_qty = 0
debit_amount = 0
credit_qty = 0
credit_amount = 0
}
let res = await db.reduce(
async (_, doc) => doc.type == 'purch' || doc.type == 'sale',
async (result, doc) => {
// await -
const promises = doc.lines.map(async (line) => {
const key = line.invent + doc.partner
let row = result.get(key)
if (row === undefined) {
row = new ResultRow()
// ( )
row.invent_name = (await db.get(line.invent))?.name ?? ' not found'
row.partner_name = (await db.get(doc.partner))?.name ?? ' not found'
result.set(key, row)
}
if (doc.type == 'purch') {
row.debit_qty += line.qty
row.debit_amount += line.qty * line.price
} else if (doc.type == 'sale') {
row.credit_qty += line.qty
row.credit_amount += line.qty * line.price
}
})
await Promise.all(promises)
},
new Map<string, ResultRow>() // ()
)
, . , , . , — — fullscan, .
:
: 100 + 100 +
100 . : 10 + 10 +
10 .Intel Celeron CPU N2830 @ 2.16 GHz— , , . , 10 .
- 100 . 11.1 :file: database_immutable.json:
100200 docs parsed (0 errors)
50018 docs processed (0 errors)
11.098s elapsed
file: database_current.json:
10020 docs parsed (0 errors)
4987 docs processed (0 errors)
1.036s elapsed
amount total = 623422871.2641689
amount per document = 11389.839613851627
lines per document = 3.6682561432355896
file: database_current.json:
10021 docs parsed (0 errors)
4988 docs processed (0 errors)
1.034s elapsed
amount total = 623433860.2641689
amount per document = 11389.832290707558
lines per document = 3.6682073954983925
, . , :
8.8s — JSON, "\
x01"
1.9s — JSON
0.4s — +
Deno, , unicode, V8 - . , WASM/Rust , , JSON , — . .
- , . 3 , , , , , fullscan. , :
- 100 . 13.3 :file: database_immutable.json:
100200 docs parsed (0 errors)
100000 docs processed (0 errors)
13.307s elapsed
file: database_current.json:
10020 docs parsed (0 errors)
10000 docs processed (0 errors)
1.296s elapsed
invent name | partner name | debet qty | debet amount | credit qty | credit amount | balance amount
===========================================================================
invent 92 | partner 50 | 164 | 34795.53690513125 | 338 | 64322.24591475369 | -29526.709009622435
invent 44 | partner 32 | 285 | 57382.115033253926 | 209 | 43572.164405352596 | 13809.95062790133
invent 95 | partner 32 | 340 | 73377.08274368728 | 205 | 42007.69685305944 | 31369.38589062784
invent 73 | partner 32 | 325 | 57874.269249290744 | 300 | 58047.56414301135 | -173.29489372060198
invent 39 | partner 32 | 333 | 69749.88883753444 | 415 | 86369.07805766111 | -16619.189220126675
invent 80 | partner 49 | 388 | 74965.03809449819 | 279 | 51438.03787960939 | 23527.0002148888
invent 99 | partner 49 | 337 | 69360.84770099446 | 292 | 58521.2605634746 | 10839.587137519862
invent 38 | partner 45 | 302 | 63933.21291162898 | 217 | 44866.95192796074 | 19066.26098366824
invent 34 | partner 45 | 343 | 69539.75051653324 | 205 | 41155.65340219566 | 28384.09711433758
invent 41 | partner 45 | 278 | 63474.209440233106 | 258 | 45246.446456763035 | 18227.76298347007
< tail skipped >
- , 2 , —
2.6s. , , , .
, , , — . , (
-,
EDI, - ), .
UPD1) ,
VolCh .
2) CouchDB,
apapacy .
PS