The Ugly Org - una DB para mi amigo George Hotz y comma.ai en Rust

desu

Otra cosa que hacia geohotz q creo que no hace falta, era inicializar las cosas como borradas al hacer Get/Head.

https://github.com/vrnvu/rust-minikeyvalue/pull/16/files

No creo que haga falta. Es mas claro usar un None y devolver 404 si no lo tenemos en leveldb. Si hay algun error de inconsistencia en el sistema tener un None o un borrado, fallara igual.

desu

He hecho la prueba de quitar leveldb, (tambien estoy usando otros algoritmos de consistent hashing), y sin el leveldb, haciendo todas las request a nginx tienes esto:

flox [rust-minikeyvalue default] ➜  rust-minikeyvalue git:(master) ✗ wrk -t2 -c100 -d10s http://localhost:3000/key

Running 10s test @ http://localhost:3000/key
  2 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     7.34ms   20.80ms 234.48ms   91.70%
    Req/Sec    42.19k    21.63k   89.23k    62.98%
  795671 requests in 10.06s, 58.43MB read
  Non-2xx or 3xx responses: 795671
Requests/sec:  79080.98
Transfer/sec:      5.81MB


flox [rust-minikeyvalue default] ➜  rust-minikeyvalue git:(master) ✗ go run tools/thrasher.go
starting thrasher
20000 write/read/delete in 13.283534167s
thats 1505.61/sec

Practicamente el mismo rendimiento, pero fijaros en las peticiones, antes tardaban <1ms ahora tardan un par de ms que es lo que tardas en golpear el nginx. Esto si en lugar de local fuera una network real quizás hablaríamos de 5-10ms a 20-50ms bien buenos... y la tail me la ha aumentado mucho.

Asi que leveldb/boltdb y similares, practicamente son zero-cost. Van tan rapido que es mejor ponerlo y gestionar el estado que no sacarlo y tragarte un x10-x100 en degradado de performance.

desu

El algoritmo de hashing nuevo lo voy a dejar, ya no tengo interés en mantener interoperabilidad total. Ya he arreglado bastantes cosas igualmente.

https://github.com/vrnvu/rust-minikeyvalue/pull/17

Esta bastante bien xq inicializamos el ring con los volumenes que tenemos disponibles y como sabéis en esta arquitectura hay que parar el master si hay que rebalancear o hacer cualquier cosa...


Tambien he estado usando esta herramienta: https://tokio.rs/tokio/topics/tracing-next-steps

No vale para mucho la verdad...

Donde este un buen flaemgraph de cpu y de memoria que se quite. El problema es que los flamegraph con codigo tan async/io no estan bien optimizados y no te dan datos relevantes.. Intentare mejorar el análisis y el tooring de eso un poco.

desu

A alguien en Axum, estoy migrando todo de Warp a Axum, le pareció CHACI PIRULE al devolver las respuestas, en lugar de tener un ResopnseBuilder y toda la pesca que fuese automático... y que funcionase con tuplas y cosas raras... y si devuelves en un sitio solo un status code en otro lado no puedes devolver status code + headers, y si devuelves en un sitio 2 headers no puedes devolver en otro sitio 3 headers...

https://docs.rs/axum/0.7.5/axum/response/index.html

Impresionante, xq? quien ha pedido esta mierda? quien quiere esta mierda?

"for more low level control" dice la docs que use esto:

async fn response() -> Response {
    Response::builder()
        .status(StatusCode::NOT_FOUND)
        .header("x-foo", "custom header")
        .body(Body::from("not found"))
        .unwrap()
}

Panda de subnromales, que cojones voy a usar si no..

desu

Migrado de Warp a Axum, mismos benchmarks...

https://github.com/vrnvu/rust-minikeyvalue/pull/18/files

Pero bueno, este framework es mas bajo nivel asi que creo que me gusta mas, fijaros en algunos cambios lo meme que son... porque rust no tiene una buena stdlib...

Y asi unos cuantos, todas las librerías y frameworks tienen decenas de cosas que son lo mismo pero con otro nombre y copy pasteado porque no esta en la stdlib estable!


Estoy un poco atascado con el profiling.

Tenemos un codigo un 35% mas rapido y eficiente pero no escala :/

desu

A tomar por culo, tiro 1k req de PUT, luego hago 1M de GET.

Mi codigo funciona, el de geohotz peta.

Su codigo de mierda:

starting thrasher
Starting GET round 1 of 1000
Starting GET round 101 of 1000
Starting GET round 201 of 1000
Starting GET round 301 of 1000
Starting GET round 401 of 1000
Starting GET round 501 of 1000
GET FAILED Get "http://localhost:3001/sv05/ab/46/L2JlbmNobWFyay02OTY1NDM2MjY1Njg2MzQxNzM3": read tcp [::1]:52957->[::1]:3001: read: socket is not connected
ERROR on GET, round 578, key 746
exit status 255

Mi faaaking rust masterrace:

starting thrasher
Starting GET round 1 of 1000
Starting GET round 101 of 1000
Starting GET round 201 of 1000
Starting GET round 301 of 1000
Starting GET round 401 of 1000
Starting GET round 501 of 1000
Starting GET round 601 of 1000
Starting GET round 701 of 1000
Starting GET round 801 of 1000
Starting GET round 901 of 1000
Completed 1000 PUTs and 1000000 GETs (1000 rounds) in 37.005502417s
Total operations: 1001000, that's 27050.03 ops/sec

Empate 1 - 1

3
desu

Voy a hacer un comentario hoy sobre diseño de APIs y una cosa que no me gusta sobre las recomendaciones del DDD y similares. La gente que diseña estas librerías lo hace para jugar el rato o montar sobre-enginieria, pocas veces lo hace gente para trabajar (go). Y esto se nota mucho en que siguen ideas como MODELAR EL DOMINIO en lugar de diseñar cosas UTILES.

Una manera muy fácil de ver si una librerías la ha escrito alguien que sabe lo que hace es ver si la ha escrito para los usuarios. Y aqui es importante hacer hincapié en un detalle sutil que se convierte en critico con los años. La documentación NO debe ser necesaria. La documentación es una AYUDA para visualizar la API/Libreria, los métodos las interfaces el sistema de tipado. Pero no debo necesitar estar constantemente re-buscando en los ejemplos porque no entiendo que cosas van con que. Y ahi esta el problema de Rust. Es una torre de naipes de sistemas de traits construido para la masturbacion de sus autores en su gran mayoria.

Asi pues, como se detecta si algo esta escrito para los usuarios? Las cosas son obvias donde van. Solo hay 1 manera de hacer las cosas. Los modulos indican intención y porque, no DOMINIO. Es decir, todo lo contrario que te enseñan en la universidad y todo lo que se ha hecho en unix/gpl y demas escuelas de open source toda la vida. Por ejemplo:

En Axum tenemos el concepto de extractors. Son las cosas que nos "extraen" información de las Request como el body,headers,paths params,query params... y ademas sirve para pasar el estado. La idea conceptual es request => extractors => handler => response.

Pues bien, fijémonos en la MIERDA que es AXUM. AXUM y la gran mayoria de software que tocas: https://docs.rs/axum/latest/axum/extract/index.html#common-extractors

use axum::{
    extract::{Request, Json, Path, Extension, Query},
    routing::post,
    http::header::HeaderMap,
    body::{Bytes, Body},
    Router,
};
use serde_json::Value;
use std::collections::HashMap;

// `Path` gives you the path parameters and deserializes them. See its docs for
// more details
async fn path(Path(user_id): Path<u32>) {}

// `Query` gives you the query parameters and deserializes them.
async fn query(Query(params): Query<HashMap<String, String>>) {}

// `HeaderMap` gives you all the headers
async fn headers(headers: HeaderMap) {}

// `String` consumes the request body and ensures it is valid utf-8
async fn string(body: String) {}

// `Bytes` gives you the raw request body
async fn bytes(body: Bytes) {}

// We've already seen `Json` for parsing the request body as json
async fn json(Json(payload): Json<Value>) {}

// `Request` gives you the whole request for maximum control
async fn request(request: Request) {}

// `Extension` extracts data from "request extensions"
// This is commonly used to share state with handlers
async fn extension(Extension(state): Extension<State>) {}

No os voy a decir el problema, os voy a dar la solución para terminar el post, pensad en ello vosotros mismos, veréis que es muy obvio. De nada.

use axum::{
    extract::{Request, Json, String, Path, Extension, Query, header::HeaderMap, body::Bytes, body::Body},
    routing::post,
    Router,
};
use serde_json::Value;
use std::collections::HashMap;

// `Path` gives you the path parameters and deserializes them. See its docs for
// more details
async fn path(Path(user_id): Path<u32>) {}

// `Query` gives you the query parameters and deserializes them.
async fn query(Query(params): Query<HashMap<String, String>>) {}

// `HeaderMap` gives you all the headers
async fn headers(headers: HeaderMap) {}

// `String` consumes the request body and ensures it is valid utf-8
async fn string(body: String) {}

// `Bytes` gives you the raw request body
async fn bytes(body: Bytes) {}

// We've already seen `Json` for parsing the request body as json
async fn json(Json(payload): Json<Value>) {}

// `Request` gives you the whole request for maximum control
async fn request(request: Request) {}

// `Extension` extracts data from "request extensions"
// This is commonly used to share state with handlers
async fn extension(Extension(state): Extension<State>) {}

Venga, para fperos:

use axum::{
    routing::post,
    Router,
};
use serde_json::Value;
use std::collections::HashMap;

// `Path` gives you the path parameters and deserializes them. See its docs for
// more details
async fn path(extract::Path(user_id): extract::Path<u32>) {}

// `Query` gives you the query parameters and deserializes them.
async fn query(extract::Query(params): extract::Query<HashMap<String, String>>) {}

// `HeaderMap` gives you all the headers
async fn headers(headers: extract::HeaderMap) {}

// `String` consumes the request body and ensures it is valid utf-8
async fn string(body: extract::String) {}

// `Bytes` gives you the raw request body
async fn bytes(body: extract::Bytes) {}

// We've already seen `Json` for parsing the request body as json
async fn json(extract::Json(payload): Json<extract::Value>) {}

// `Request` gives you the whole request for maximum control
async fn request(request: extract::Request) {}

// `Extension` extracts data from "request extensions"
// This is commonly used to share state with handlers
async fn extension(extract::Extension(state): extract::Extension<State>) {}

Imaginate que quiero sacar un header concreto, ahora mismo NO puedo, he tenido que pasar todos los headers, entre comillas ya me entendéis. Pero podria tener un custom T que implementase extract::ExtractFromRequest y fuera. Asi de fácil.

Marie Kondo: Rule 4: Tidy by Category, Not by Location

desu

Es impresionante, el open source te pone en manifesto todas las FPeadas del mundo y la mediocridad del ingeniero medio:
https://github.com/tokio-rs/axum/issues/289

  • any() no funciona muy bien no?
  • no, pero puedes usar este workaround, con el que NO FUNCIONA ningún extractor ni otro feature de la libreria, y no tiene nada que ver con la otra manera que hacemos los routing y handlers y ya esta...
  • perfecto
  • ok cierro MR

obviamente el workaround no lo entiende ni el que lo ha escrito...

como mas miro Axum menos me gusta, menuda porqueria en serio:

no es tan dificil pasar variables a las task de tokio eh.. déjame escribir una puta función normal...

ves los comentarios: https://www.reddit.com/r/rust/comments/15159w6/comment/js6rhzl/

y la gente te pide re-hacer DI y Spring en Rust madre mia aaaa a aa aa a

otro framework que me parece una mierda 2 de 2. el ecosistema rust es una porqueria.

desu

Otra decision because potato.

En lugar de construir errores estaba pensando en devolver un enum de error e implementar e trait IntoResponse. Hasta aqui bien. La verdad, tansolo es una guía de estilo y no tiene mucha importancia. Yo prefiero devolver siempre las response builder porque es lo mas flexible, cómodo y claro de que esta pasando. Pero por probar que no se diga... pues bien, lo he probado.

El codigo es bastante simple, la gran ventaja de esto es que si tienes una api con diez mil endpoints pues unifica todo en un sitio y evita que un fpero recién salido de la universidad te envié un status code que no de deba sin testear... y yo que se, por algun motivo eso pase la code review y termine en prod...

Y nos permite escribir codigo usando ?:

Que discutiblemente, según estilos y gustos, pros/cons variados, podemos decir que se lee mas claro. Como veis no hay mucha diferencia. Pues bien, el problema:

Si quiero usar los helpers para headers típicos como Content-Length y Location debo meterle un to_string(). porque la api que han diseñado, no permite que yo le pase un String y me lo convierta a su tipo de header, porque lo han hecho a propósito que nadie pueda implementarlo sino cumple las restricciones que se han inventado...

Lo mas simple.. meterlo en el struct ya no vale. Sorpresa! Me dice que puedo pasarlo por parámetro... ósea que si ahora creo una función, para crear mi struct/enum de error, poder hacer lo que no puedo hacer si primero creo el struct e implemento el error! Osea esta del puto reves jajajajaja xq si yo ahora hago esto:

para que cojones quiero un struct de error??? si le estoy pasando a la función todos los parámetros en el formato que necesito para hacer una response builder, pues hago una response builder que es lo que tenia!


Osea que esto es lo que tengo, y si quiero puedo wrappearlo en un Err(AppError) que implementa IntoResposne para que en otra llamada se cree lo que ya tenia escrito! jajajajaja

Es flipante chavales, flipante.

1
desu
#21desu:

tendré que optimizarlo porque ademas estoy calculando un md5 de todo el contenido

Empate con el rendimiento de geohotz!!! 2500 req/s approx!

flox [rust-minikeyvalue default] ➜  rust-minikeyvalue git:(master) ✗ go run tools/thrasher.go
starting thrasher
20000 write/read/delete in 7.778764375s
thats 2571.10/sec
flox [rust-minikeyvalue default] ➜  rust-minikeyvalue git:(master) ✗ go run tools/thrasher.go
starting thrasher
20000 write/read/delete in 8.56443225s
thats 2335.24/sec

No me iba a ir yo con un 3-1! Ahora si que si, port completado.

Nos lo hemos follado bien.

Estoy tirando de nuevo todos los benchmarks para ponerlos en el README y los podais ver y tambien re-producir. Y ahora os enseño donde estaba el problema. Es un problema que ya comente y tenia fichado! Os lo cito arriba pero creía que no era para tanto.. pues si que era para tanto! En los profilers no me salía la verdad... Si hubiese tirado profiles de funciones a mas bajo nivel y no suit de test completas quizás lo veo... esto y quizás mas cosas.

La MR: https://github.com/vrnvu/rust-minikeyvalue/pull/19

Siuuuu

El readme

2
desu

Performance benchmarks

The code performs equal or better than the original Go implementation.

Our implementatio results first, second the Go implementation!

 rust-minikeyvalue git:(master) ✗ go run tools/thrasher.go
 starting thrasher
 20000 write/read/delete in 7.778764375s
 thats 2571.10/sec
 minikeyvalue git:(master) ✗ go run tools/thrasher.go
 starting thrasher
 20000 write/read/delete in 7.651901291s
 thats 2613.73/sec

As you can see the avg latency, stdv and max are way better than the Go implementation. Around 35% improvement!

 rust-minikeyvalue git:(master) ✗ wrk -t2 -c100 -d10s http://localhost:3000/key

 Running 10s test @ http://localhost:3000/key
   2 threads and 100 connections
   Thread Stats   Avg      Stdev     Max   +/- Stdev
     Latency   682.35us    0.98ms  31.44ms   99.02%
     Req/Sec    70.68k    11.48k  192.91k    91.04%
   1413493 requests in 10.10s, 130.76MB read
   Non-2xx or 3xx responses: 1413493
 Requests/sec: 139920.06
 Transfer/sec:     12.94M
 minikeyvalue git:(master) ✗ wrk -t2 -c100 -d10s http://localhost:3000/key

 Running 10s test @ http://localhost:3000/key
   2 threads and 100 connections
   Thread Stats   Avg      Stdev     Max   +/- Stdev
     Latency     1.22ms    3.50ms  66.29ms   93.28%
     Req/Sec    91.04k    33.44k  201.23k    75.38%
   1816291 requests in 10.08s, 142.04MB read
   Non-2xx or 3xx responses: 1816291
 Requests/sec: 180167.17
 Transfer/sec:     14.09MB

Some small stats, Claude3.5 generated, report if anything is wrong, lgtm.

  • Latency improvements:

    • Average latency: 44.07% lower
    • Standard deviation: 72.00% lower
    • Max latency: 52.57% lower
  • Requests per second:

    • Our Rust implementation handles 22.34% fewer requests per second in this specific benchmark
      Also when I tested a heavy read scenario where our project exceeds:
      Our implementation:
       rust-minikeyvalue git:(master) ✗ go run tools/thrasher-read.go
       starting thrasher
       Starting GET round 1 of 1000
       Starting GET round 101 of 1000
       Starting GET round 201 of 1000
       Starting GET round 301 of 1000
       Starting GET round 401 of 1000
       Starting GET round 501 of 1000
       Starting GET round 601 of 1000
       Starting GET round 701 of 1000
       Starting GET round 801 of 1000
       Starting GET round 901 of 1000
       Completed 1000 PUTs and 1000000 GETs (1000 rounds) in 36.52528875s
       Total operations: 1001000, that's 27405.67 ops/sec

    Go code crashes! Try to run the test multiple times you will see with 1M GET request it usually fails.

 minikeyvalue git:(master) ✗ go run tools/thrasher-read.go 
 starting thrasher
 Starting GET round 1 of 1000
 Starting GET round 101 of 1000
 Starting GET round 201 of 1000
 Starting GET round 301 of 1000
 Starting GET round 401 of 1000
 Starting GET round 501 of 1000
 Starting GET round 601 of 1000
 Starting GET round 701 of 1000
 GET FAILED Get "http://localhost:3003/sv04/ba/3d/L2JlbmNobWFyay0xODA1MDI5MTk1NjU3NDg3MTAy": read tcp [::1]:56978->[::1]:3003: read: socket is not connected
 ERROR on GET, round 745, key 164
 exit status 255
1 respuesta
draz1c

Bueno, tras ver esto, le he pasado el contenido de este diario a NotebookLM y he generado un Podcast entre 2 personas hablando sobre ello :psyduck:

Summary

This text is a developer diary entry by a software engineer who is migrating a key-value store database written in Go to Rust. The developer, who refers to himself as "desu," is critical of the design choices made in popular Rust libraries, finding them overly complex and lacking practical usability. He contrasts the Rust libraries with the simplicity and efficiency of Go's standard library. "Desu" provides a detailed account of his process, highlighting the bugs he encounters, the performance comparisons between his Rust implementation and the original Go version, and his thoughts on software engineering principles.

Podcast

https://vocaroo.com/1d1MPGUBhzRN

5 1 respuesta
desu

#42 joder macho que profesional sueno y que bien hablan de mi

el contenido tiene algún fallo de contenido, pero esta muy guapo si

JuAn4k4

#41 Has pensado en mejorar los resultados de alguna forma ?

1 respuesta
desu

#44 para mejorar los resultados debería tirar benchmarks de funciones mas granulares, porque los flamegraph de mis thrasher.go que usamos de benchmark duran tanto y tienen tanta traza del runtime de tokio (async event loop) que no se ve nada.

se un par de sitio cosas que podria cambiar como lo hago a ciegas y probar, y he probado otros tantos como calcular con un spawn_blocking los hashes y hacer que el codigo en general bloquee menos el eventloop, pero no he visto mejoras.

asi que debería tirar un profiler de cpu y de memoria de mis funciones y ver ahi si hay algo mal. pero no lo he hecho.

Mi intuición me dice q el problema es como uso el runtime. Porque la performance de cada petición es rapidísima pero no estoy escalando. Por ejemplo en el put copio mucho el buffer del body. Nse. Debería mirarlo como digo.