Caja para almacenar datos en aplicaciones go

imagen

Una breve nota sobre una base de datos de clave-valor incrustada llamada Coffer escrita en Golang. Si es muy breve: cuando la base de datos está detenida, los datos están en el disco, cuando se inicia, los datos se copian en la memoria. La lectura viene de la memoria. Durante la grabación, los datos de la memoria cambian y los cambios se escriben en el registro del disco. El tamaño máximo de los datos almacenados está limitado por el tamaño de la RAM. La API le permite crear encabezados para registros de bases de datos y usarlos en transacciones, mientras mantiene la consistencia de los datos.

Pero primero, una pequeña introducción lírica. Érase una vez, cuando la hierba era más verde, me llevó incrustar una base de datos de valores clave para la aplicación go. Mirando alrededor y tropezando con diferentes paquetes, de alguna manera no encontré lo que me gustaría (subjetivamente), y simplemente apliqué la solución con una base de datos relacional externa. Gran solución de trabajo. Pero como dicen, se encontró una cuchara, pero el sedimento permaneció. En primer lugar, quería exactamente el nativo, escrito en la base de datos de Go, directamente nativo-nativo. Y los hay, solo tienes que lucir increíble. Sin embargo, no hay un millón de ellos. Esto es incluso sorprendente cuando considera que un programador es raro en el mundo que no escribió una base de datos, un marco o un juego casual en su vida.


Bueno, puedes intentar apilar tu bicicleta sobre tus rodillas, con blackjack y otras golosinas. Al mismo tiempo, todo el mundo sabe, o al menos adivina, que escribir incluso una simple base de datos de valores clave parece simple solo a primera vista. Pero, de hecho, todo es mucho más divertido (y sucedió). Y tenía curiosidad sobre ACID y me preocupaban las transacciones. Las transacciones verdaderas son más probables en el sentido financiero, porque Entonces estaba ocupado en fintech.


Seguridad de datos


Considere el caso cuando, durante el funcionamiento de una aplicación con grabación activa, la fuente de alimentación de la computadora se cubrió con un recipiente de cobre y el disco no se rompió. Si en este momento la aplicación recibió la ok de la base de datos, los datos de esta operación no se perderán. Si la aplicación recibió una respuesta negativa, por supuesto, la operación no se completó. Bueno, el caso en el que la aplicación envió una solicitud, pero no recibió una respuesta: esta operación probablemente no se completó, pero existe una pequeña posibilidad de que la operación caiga en el registro, pero exactamente en el momento en que se envió la respuesta, la alimentación se apagó.


¿Cómo en el último caso averiguar qué pasó con las últimas operaciones? Esta es una pregunta interesante. Indirectamente, puede adivinar al respecto (sacar conclusiones) al observar el valor del registro de interés después del lanzamiento de una nueva aplicación desde la base de datos. Sin embargo, si las operaciones son lo suficientemente frecuentes, me temo que no ayudarán. Puede ver el archivo del último registro (estará con el número más alto), pero manualmente esto es inconveniente. Creo que, en el futuro, puede agregar la capacidad de ver registros en la API (naturalmente, los registros en este caso no deberían eliminarse).


Francamente, yo mismo no saqué el cable del enchufe, porque No quiero arriesgarme por el hierro por el simple hecho de verificar la base de datos. En las pruebas, solo estropeo los archivos de registro normales, y en este caso, todo sucede como esperaba. Sin embargo, no hay experiencia en el uso práctico de la base de datos, no funcionó en la producción y existen riesgos. Sin embargo, para proyectos favoritos, creo que la base de datos se puede usar sin miedo. En general, el descargo de responsabilidad habitual, sin garantías.


Actualmente, la base de datos no está protegida contra el uso en dos aplicaciones diferentes (o lo mismo, no importa aquí) configuradas para funcionar con el mismo directorio. ¡Tenga en cuenta este momento! Y, sin embargo, dado que la base de datos está incorporada y luego le pasa algún tipo de tipo de referencia en los argumentos, definitivamente no vale la pena cambiarla en algún lugar de la rutina paralela.



Configuracion


La base de datos tiene bastantes parámetros que se pueden configurar, sin embargo, casi todos tienen valores predeterminados, por lo que todo puede encajar en una línea corta cof, err, wrn := Db(dirPath).Create() devuelve un error (si se produce un error, trabaje más con la base de datos prohibido) y advertencia, que puede conocer, pero esto no interfiere con el funcionamiento de la base de datos.


No llenaré el texto con descripciones engorrosas, si es necesario, mírelas en el archivo Léame del repositorio: github.com/claygod/coffer/blob/master/README_RU.md#config Tenga en cuenta el método de controlador que conecta el controlador para la transacción, escribiré un par de líneas al respecto más abajo, aquí solo los enumero:


  • Db (dirPath)
  • BatchSize (batchSize)
  • LimitRecordsPerLogfile (limitRecordsPerLogfile)
  • FollowPause (100 * time.Second)
  • LogsByCheckpoint (1000)
  • AllowStartupErrLoadLogs (verdadero)
  • MaxKeyLength (maxKeyLength)
  • MaxValueLength (maxValueLength)
  • MaxRecsPerOperation (1,000,000)
  • RemoveUnlessLogs (verdadero)
  • Memoria de límite (100 * 1,000,000)
  • LimitDisk (1000 * 1,000,000)
  • Handler ("handler1" y handler1)
  • Handler ("handler2" y handler2)
  • Manejadores (map [string] * handler)
  • Crear ()

API


En la medida de lo posible, hice la API simple, y para una base de valor clave, no seas demasiado inteligente:


  • Inicio: inicia la base de datos
  • Detener: detener la base de datos
  • StopHard: una parada independientemente de las operaciones que se estén realizando en este momento (tal vez la elimine)
  • Guardar: guarda una instantánea del estado actual de la base de datos
  • Escribir: agregue un registro a la base de datos
  • WriteList: agregue varios registros a la base de datos (modos estricto y opcional)
  • WriteListUnsafe: agregue múltiples registros a la base de datos sin tener en cuenta la seguridad de los datos
  • Leer: obtener un registro por clave
  • ReadList: obtenga una lista de registros
  • ReadListUnsafe: obtenga una lista de registros sin tener en cuenta la seguridad de los datos
  • Eliminar: eliminar un registro
  • DeleteList: elimina varios registros en modo estricto / opcional
  • Transacción: ejecutar una transacción
  • Recuento: cuántos registros hay en la base de datos
  • CountUnsafe: cuántos registros hay en la base de datos (un poco más rápido, pero no seguro)
  • RecordsList: una lista de todas las claves de la base de datos
  • RecordsListUnsafe: una lista de todas las claves de la base de datos (un poco más rápido, pero inseguro)
  • RecordsListWithPrefix: una lista de claves con el prefijo especificado
  • RecordsListWithSuffix: una lista de claves con el final especificado

Breves explicaciones a la API:


  • Modo estricto: hazlo todo o nada.
  • Modo opcional: haga todo lo que funcione.
  • StopHard: quizás este método debería eliminarse de la API hasta que se decida.
  • Todos los métodos de RecordList no son rápidos, porque No hay índices en la tienda en este momento, mientras que este es un escaneo completo.
  • Todos los métodos inseguros son más rápidos, pero la consistencia no está implícita al usarlos. Es lógico usarlos en una base de datos detenida para su llenado rápido o algo más en la misma línea.
  • El seguidor supervisa la actualización periódica de la instantánea de la base de datos, por lo que el método Guardar es más probable para algunos casos especiales en los que definitivamente desea crear una nueva instantánea (hasta que se me ocurra tal caso, pero tal vez lo sea).

Un caso de uso simple:

 package main import ( "fmt" "github.com/claygod/coffer" ) const curDir = "./" func main() { // STEP init db, err, wrn := coffer.Db(curDir).Create() switch { case err != nil: fmt.Println("Error:", err) return case wrn != nil: fmt.Println("Warning:", err) return } if !db.Start() { fmt.Println("Error: not start") return } defer db.Stop() // STEP write if rep := db.Write("foo", []byte("bar")); rep.IsCodeError() { fmt.Sprintf("Write error: code `%d` msg `%s`", rep.Code, rep.Error) return } // STEP read rep := db.Read("foo") rep.IsCodeError() if rep.IsCodeError() { fmt.Sprintf("Read error: code `%v` msg `%v`", rep.Code, rep.Error) return } fmt.Println(string(rep.Data)) } 

Transacciones


Como se mencionó anteriormente, mi definición de transacciones puede no coincidir con la generalmente aceptada en la construcción de DB, quizás estén unidas solo por una idea. En una implementación específica, una transacción es un determinado encabezado especificado en la etapa de configuración de la base de datos (método del Handler ). Cuando invocamos una transacción con este encabezado, la base de datos bloquea los registros con los que trabajará el encabezado y transfiere sus valores actuales al encabezado. El encabezado manipula estos datos según sea necesario, y devuelve los nuevos valores de la base de datos, y eso los guarda en cien. Después de eso, los registros se desbloquean y están disponibles para otras operaciones.


Hay ejemplos en el repositorio que revelan la esencia del uso de transacciones muy bien. Por curiosidad, hice un pequeño ejemplo financiero, en el que hay operaciones de débito y crédito, transferencia, compra y venta. Fue muy fácil escribir este ejemplo y, al mismo tiempo, esta implementación a la altura de las rodillas es bastante consistente y adecuada para su uso en diversas soluciones financieras, o por ejemplo en logística.


Un punto importante: el código del controlador no se almacena en la base de datos. Tuve la idea de almacenarlo en un registro, pero me pareció demasiado derrochador, por lo que no lo compliqué y, en consecuencia, la responsabilidad de la coherencia de los controladores entre los diferentes inicios de la base de datos recae en el desarrollador del código que utiliza la base de datos. Los controladores definitivamente no se pueden cambiar si la aplicación y la base de datos dejaron de fallar. En este caso, primero debe iniciar la base de datos y luego detenerla regularmente; se creará una nueva instantánea de datos. Para no confundirse, le aconsejo que use el número de versión en el nombre de los controladores.



Recibir y procesar respuestas.


La base de datos devuelve informes que indican el estado de la respuesta y con los datos. Dado que hay muchos códigos, y escribir un interruptor con el procesamiento de cada uno de ellos es problemático, es posible que desee verificar por aprox. Esto no debe hacerse. El hecho es que el código puede tener el estado Ok, Error, Panic. Con Ok, todo está claro, pero ¿qué pasa con los otros dos? Si el estado es Error, se completa una operación específica o no se completa. Este error debe manejarse adecuadamente en la aplicación. Sin embargo, es posible trabajar más con la base de datos (y es necesario). Otra cosa de pánico: el trabajo con la base de datos debe suspenderse.


Verificar IsCodeError simplifica el trabajo con todos los errores, por lo que si no está interesado en los detalles, continúe trabajando.
La verificación IsCodePanic cubre todos los casos en los que se debe detener el trabajo con la base de datos.


En el caso simple, un interruptor triple es suficiente para procesar la respuesta:


  • IsCodeOk : continúe trabajando como IsCodeOk
  • IsCodeError : registra el error del informe y trabaja más
  • IsCodePanic : registra el error del informe y deja de trabajar con la base de datos

Fuera de


Para el nombre, se eligió una de las opciones para traducir el palabras al inglés. Preferiría el box , por supuesto, pero esta es una palabra demasiado popular, espero que lo haga el coffer .
El tema con ACID me parece bastante holístico, por lo que diría que Coffer está comprometido con esto, pero no es un hecho, y no afirmo que haya tenido éxito.



Rendimiento


Inmediatamente escribí una base de datos teniendo en cuenta la concurrencia y la competencia. Es en este modo que muestra su eficacia (aunque esto probablemente se dice demasiado fuerte). En los resultados a continuación, el punto de referencia muestra un ancho de banda de 200k rps. Por supuesto, este es un banco artificial, y la realidad será completamente diferente, porque mucho depende del tamaño de los datos grabados, la cantidad de datos ya grabados, el rendimiento del hierro y la fase de la luna. Pero la tendencia es al menos comprensible. Si la base de datos se usa en modo de subproceso único, cada solicitud se ejecuta solo después de recibir la respuesta a la anterior, la velocidad será lenta y le aconsejaría que busque otras bases de datos, pero no Coffer.


  • BenchmarkCofferTransactionSequence-4 2000 227928 ns / op
  • BenchmarkCofferTransactionPar32HalfConcurent-4 100000 4199 ns / op

Por cierto, si alguien pasa tiempo y se inclina a sí mismo un repositorio con Coffer, si es posible, ejecute el banco acostado en él. Estoy muy interesado en qué máquinas mostrará el rendimiento de la base de datos. En primer lugar, por supuesto, todo depende del disco. Esto se hizo especialmente claro para mí después de comprar recientemente un nuevo Samsung EVO. Pero no se preocupe, esto no es un sustituto de un disco muerto. El viejo Toshiba continúa sirviendo correctamente y ahora almacena mi archivo de video.


El reloj incorporado en la memoria sigue siendo un mapa simple, ni siquiera dividido en secciones. Por supuesto, puede ser excelente mejorarlo, por ejemplo, para que sea rápido seleccionar teclas por prefijos y sufijos. Si bien no hice esto, tk. La funcionalidad principal, como digo el chip DB, lo veo en las transacciones, y el cuello de botella en el rendimiento de las transacciones funcionará con el disco, y solo entonces, con la memoria.



Licencia


Ahora que la licencia le permite almacenar hasta diez millones de registros en la base de datos, me pareció que este es un número suficiente. Otros planes para el desarrollo de la base de datos están en proceso de formación.
En general, es interesante para mí usar la base de datos como un paquete y centrarme principalmente en su API.



Conclusiones


Recientemente, a menudo me encuentro con la tarea de escribir servicios con la característica de alta disponibilidad. Desafortunadamente, debido a que esto casi siempre implica la presencia de varias instancias, no vale la pena usar la base de datos incorporada con tal caso. Queda la opción de una aplicación o servicio regular que existe en una instancia. Me parece un caso más raro, pero sin embargo lo es, y en ese caso es bueno tener una base de datos que intente, en la medida de lo posible, guardar los datos almacenados en ella. El Cofre que creé está tratando de resolver ese problema. Veamos cómo lo hace.



Agradecimientos


  • Para todos los que leen el artículo hasta el final
  • Comentaristas que deseen compartir sus opiniones.
  • Enviado en una información personal sobre errores tipográficos y errores en el texto
  • Vecino tocando música por la noche

Referencias


Repositorio de base de datos
Descripción en ruso

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


All Articles