Gzip es un programa que nos permite comprimir información y es usado principalmente para reducir el tamaño de los payload en la red. Internamente lo usamos para ahorrar almacenamiento, pero sus casos de usos varían.
Para empezar, podemos escribir una función que reciba nuestro mensaje en bytes y retorna []byte con nuestra información ya codificada, la llamaremos compressData.
func compressData(b []byte) ([]byte, error) {
var buf bytes.Buffer
zw := gzip.NewWriter(&buf)
if _, err := zw.Write(s); err != nil {
return nil, err
}
if err := zw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
A primera vista vemos que nuestra función hace justo lo que necesita, sin embargo no es tan eficiente como deberia. Para mostrarlo, escribimos un benchmark sencillo.
func BenchmarkCompressData(b *testing.B) {
var d []byte
var err error
for n := 0; n < b.N; n++ {
d, err = compressData(data)
if err != nil {
b.Fatal(err)
}
}
}
Estos son los resultados del benchmark:
BenchmarkCompressData-4 2576 389615 ns/op 815650 B/op 22 allocs/op
Vemos que por cada operación se usa mas de 815KB y toma mas de 385K ns, numeros considerablemente altos. Mirando un poco la documentación de gzip, encontramos que se puede reusar el mismo writer usando la funcion Reset(), editando nuestra funcion, quedaria algo como:
var zw = gzip.NewWriter(ioutil.Discard)
func compressData(b byte[]) ([]byte, error) {
var buf bytes.Buffer
zw.Reset(&buf)
_, err := zw.Write(b)
if err != nil {
return nil, err
}
err = zw.Close()
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
Donde el único cambio es usar la función reset y volver a zw una variable global(haciendo que solo se llame una única vez). Los benchmark para este nuevo enfoque son bastante buenos.
BenchmarkCompressData-4 27927 46759 ns/op 496 B/op 3 allocs/op
Podemos ver que conseguimos una mejora bastante notable, pasamos de 835 KB a menos de 1 KB y de 385K ns a 46K ns. Pero aquí no acaba esta historia, el ejemplo anterior tiene un gran problema. ¿Que pasaria si este código lo ponemos en un servidor? 2 request podrían acceder a zw al mismo tiempo y hacer que el gzip writer usará el buffer que no le corresponde, esto se conoce como un data race.
En ese caso es posible implementar una solución usando sync.Pool, su funcionamiento es sencillo, se define un método que se encarga de crear el writer a medida que nuevos procesos quieren hacer uso de este, así evitando que múltiples procesos modifiquen a un único writer.
En el repo de grpc hay una muy buena implementacion de de un pool para gzip.