Bienvenido, soy Luis y esta vez les traigo este nuevo artículo.
Esta es una historia sobre cómo no gastar ni un centavo utilizando tres servicios ETA (tiempo estimado de llegada) en lugar de uno.
Todo se basa en mi experiencia personal trabajando como desarrollador back-end en el proyecto GoDee. GoDee es un proyecto de puesta en marcha que ofrece reservar asientos en un autobús en línea.
Índice
Prehistoria
GoDee es un servicio de transporte público. El transporte en autobús por GoDee es más conveniente que las motocicletas comunes en el sudeste asiático y más barato que un taxi.
El sistema basado en aplicaciones permite a los usuarios encontrar una ruta adecuada, seleccionar la hora, reservar el asiento y pagar el viaje en línea.
Y uno de los problemas de GoDee son los atascos de tráfico que afectan gravemente la experiencia del usuario. Los usuarios se cansan de esperar y se molestan al intentar adivinar la hora de llegada del autobús.
Por lo tanto, para que los desplazamientos fueran más convenientes, se necesitaba un servicio para calcular la hora aproximada de llegada del autobús, también conocida como ETA.
Desarrollar ETA desde cero llevaría al menos un año. Entonces, para acelerar el proceso, GoDee decidió implementar la herramienta API de Google Distance Matrix. Posteriormente desarrollaron su propio microservicio Pifia.
Problemas
Con el tiempo, el negocio creció y la base de usuarios aumentó. Encontramos un problema con el aumento de solicitudes en la API de Google Distance Matrix.
Debido a que cada solicitud cuesta dinero, la API de Google proporciona 10.000
consultas gratuitas por mes, después de las cuales cada 1.000
consultas se cobran $ 20
. En ese momento, teníamos alrededor de 150.000
solicitudes por mes.
Mi mentor estaba muy insatisfecho con eso. Y dijo que el sistema debería cambiar el almacenamiento en caché para almacenar ETA cada 30
minutos.
En ese momento, el sistema envió solicitudes a la API de Google cada 3 segundos
para obtener datos nuevos. Sin embargo, tal algoritmo de almacenamiento en caché no era eficiente, ya que los minibuses estaban atrapados en el tráfico.
Y así, la distancia solo cambiaba una vez cada diez minutos. Hubo otro matiz. Por ejemplo, cinco usuarios solicitan información sobre el mismo bus y esta es la misma solicitud. La caché solucionó este tipo de problemas.
Código de la cuenta
Servicios alternativos
La caché funcionó, pero no hace mucho tiempo que GoDee creció aún más y enfrentó el mismo problema: la cantidad de consultas ha aumentado nuevamente.
Se decidió reemplazar la API de Google con OSRM. Básicamente, OSRM es un servicio para construir una ruta basada en ETA (esta es una descripción aproximada pero breve, si necesita detalles, aquí está la enlace).
OSRM tiene un problema: crea rutas y calcula ETA sin tener en cuenta el tráfico. Para resolver este problema, comencé a buscar servicios que pudieran proporcionar información sobre el tráfico en la parte especificada de la ciudad.
AQUÍ Traffic estaba proporcionando los datos que necesitaba. Después de un pequeño estudio de la documentación, escribí un pequeño código que obtiene información de tráfico cada 30 minutos.
Y para cargar información de tráfico en OSRM, escribí un pequeño script con el comando ./osrm-contract data.osrm --segment-speed-file updates.csv
(más detalles aquí).
Tiempo de matemáticas: cada media hora, hay una solicitud a AQUÍ para obtener información de tráfico, son dos solicitudes por hora, es decir, un día son 48
solicitudes (24 * 2 = 48
) y un mes son aproximadamente ≈ 1.488 (48 * 31 = 1.488)
al año 17.520
.
Sí, tenemos estas solicitudes gratuitas desde AQUÍ durante 15 años
sería suficiente.
// everything that these structures mean is described here https://developer.here.com/documentation/traffic/dev_guide/topics/common-acronyms.html type hereResponse struct { RWS []rws `json:"RWS"` } type rws struct { RW []rw `json:"RW"` } type rw struct { FIS []fis `json:"FIS"` } type fis struct { FI []fi `json:"FI"` } type fi struct { TMC tmc `json:"TMC"` CF []cf `json:"CF"` } type tmc struct { PC int `json:"PC"` DE string `json:"DE"` QD string `json:"QD"` LE float64 `json:"LE"` } type cf struct { TY string `json:"TY"` SP float32 `json:"SP"` SU float64 `json:"SU"` FF float64 `json:"FF"` JF float64 `json:"JF"` CN float64 `json:"CN"` } type geocodingResponse struct { Response response `json:"Response"` } type response struct { View []view `json:"View"` } type view struct { Result []result `json:"Result"` } type result struct { MatchLevel string `json:"MatchLevel"` Location location `json:"Location"` } type location struct { DisplayPosition position `json:"DisplayPosition"` } type position struct { Latitude float64 `json:"Latitude"` Longitude float64 `json:"Longitude"` } type osmInfo struct { Waypoints []waypoints `json:"waypoints"` Code string `json:"code"` } type waypoints struct { Nodes []int `json:"nodes"` Hint string `json:"hint"` Distance float64 `json:"distance"` Name string `json:"name"` Location []float64 `json:"location"` } type osmDataTraffic struct { FromOSMID int ToOSMID int TubeSpeed float64 EdgeRate float64 } // CreateTrafficData - function creates a cvs file containing traffic information func CreateTrafficData(h config.TrafficConfig) error { osm := make([]osmDataTraffic, 0) x, y := mercator(h.Lan, h.Lon, h.MapZoom) quadKey := tileXYToQuadKey(x, y, h.MapZoom) trafficInfo, err := getTrafficDataToHereService(quadKey, h.APIKey) if err != nil { return err } for _, t := range trafficInfo.RWS[0].RW { for j := 0; j < len(t.FIS[0].FI)-1; j++ { position, err := getCoordinateByStreetName(t.FIS[0].FI[j].TMC.DE, h.APIKey) if err != nil { logrus.Error(err) continue } osmID, err := requestToGetNodesOSMID(position.Latitude, position.Longitude, h.OSMRAddr) if err != nil { logrus.Error(err) continue } osm = append(osm, osmDataTraffic{ FromOSMID: osmID[0], ToOSMID: osmID[1], TubeSpeed: 0, EdgeRate: t.FIS[0].FI[j].CF[0].SU, }) } } if err := createCSVFile(osm); err != nil { return err } return nil } // http://mathworld.wolfram.com/MercatorProjection.html func mercator(lan, lon float64, z int64) (float64, float64) { latRad := lan * math.Pi / 180 n := math.Pow(2, float64(z)) xTile := n * ((lon + 180) / 360) yTile := n * (1 - (math.Log(math.Tan(latRad)+1/math.Cos(latRad)) / math.Pi)) / 2 return xTile, yTile } // http://mathworld.wolfram.com/MercatorProjection.html func tileXYToQuadKey(xTile, yTile float64, z int64) string { quadKey := "" for i := uint(z); i > 0; i-- { var digit = 0 mask := 1 << (i - 1) if (int(xTile) & mask) != 0 { digit++ } if (int(yTile) & mask) != 0 { digit = digit + 2 } quadKey += fmt.Sprintf("%d", digit) } return quadKey } // requestToGetNodesOSMID - function for getting osm id by coordinates func requestToGetNodesOSMID(lan, lon float64, osrmAddr string) ([]int, error) { osm := osmInfo{} // here it is necessary that at the beginning lon And then lan // WARN only Ho Chi Minh url := fmt.Sprintf("http://%s/nearest/v1/driving/%v,%v", osrmAddr, lon, lan) resp, err := http.Get(url) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("Status code %d", resp.StatusCode) } body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } err = json.Unmarshal(body, &osm) if err != nil { return nil, err } if len(osm.Waypoints) == 0 { return nil, fmt.Errorf("Nodes are empty, lan: %v, lon: %v", lan, lon) } return osm.Waypoints[0].Nodes, nil } // https://developer.here.com/documentation/geocoder/dev_guide/topics/quick-start-geocode.html // getCoordinateByStreetName - function of the coordinates by street name func getCoordinateByStreetName(streetName, apiKey string) (position, error) { streetName += " Ho Chi Minh" url := fmt.Sprintf("https://geocoder.ls.hereapi.com/6.2/geocode.json?apiKey=%s&searchtext=", apiKey) gr := geocodingResponse{} streetNames := strings.Split(streetName, " ") for _, s := range streetNames { url += s + "+" } resp, err := http.Get(url) if err != nil { return position{}, err } if resp.StatusCode != http.StatusOK { return position{}, fmt.Errorf("Status code %d", resp.StatusCode) } body, err := ioutil.ReadAll(resp.Body) if err != nil { return position{}, err } err = json.Unmarshal(body, &gr) if err != nil { return position{}, err } if len(gr.Response.View) == 0 { return position{}, errors.New("View response empty") } for _, g := range gr.Response.View[0].Result { if g.MatchLevel == "street" { return g.Location.DisplayPosition, nil } } return position{}, fmt.Errorf("street: %s not found", streetName) } func getTrafficDataToHereService(quadKey, apiKey string) (hereResponse, error) { rw := hereResponse{} url := fmt.Sprintf("https://traffic.ls.hereapi.com/traffic/6.2/flow.json?quadkey=%s&apiKey=%s", quadKey, apiKey) resp, err := http.Get(url) if err != nil { return rw, err } if resp.StatusCode != http.StatusOK { return rw, fmt.Errorf("Status code %d", resp.StatusCode) } body, err := ioutil.ReadAll(resp.Body) if err != nil { return rw, err } err = json.Unmarshal(body, &rw) if err != nil { return rw, err } return rw, nil } func createCSVFile(data []osmDataTraffic) error { if err := os.Remove("./traffic/result.csv"); err != nil { logrus.Error(err) } file, err := os.Create("./traffic/result.csv") if err != nil { return err } defer file.Close() writer := csv.NewWriter(file) defer writer.Flush() for _, value := range data { str := createArrayStringByOSMInfo(value) err := writer.Write(str) if err != nil { logrus.Error(err) } } return nil } func createArrayStringByOSMInfo(data osmDataTraffic) []string { var str []string str = append(str, fmt.Sprintf("%v", data.FromOSMID)) str = append(str, fmt.Sprintf("%v", data.ToOSMID)) str = append(str, fmt.Sprintf("%v", data.TubeSpeed)) str = append(str, fmt.Sprintf("%v", data.EdgeRate)) return str }
Código para conseguir tráfico.
Las pruebas preliminares mostraron que el servicio funciona perfectamente, pero hay un problema, AQUÍ proporciona información de tráfico en «galimatías» y los datos no coinciden con el formato OSRM.
Para que la información se ajuste, necesita usar otro servicio AQUÍ para geocodificación + OSRM (para obtener puntos en el mapa). Esto es aproximadamente 450.000
solicitudes por mes.
Posteriormente, OSRM se abandonó porque el número de solicitudes excedió el límite gratuito. No nos rendimos y habilitamos la API HERE Distance Matrix y eliminamos temporalmente la API Google Distance Matrix.
La lógica AQUÍ es simple: enviamos coordenadas del punto A
al punto B
y obtenemos la hora de llegada del autobús.
Después de que instalamos todo en el servidor de prueba y comenzamos a verificar, recibimos los primeros comentarios de los probadores. Dijeron que ETA lee la hora incorrectamente.
Comenzamos a buscar el problema, miramos los registros (usamos Data dog para los registros), los registros y las pruebas mostraron que todo funcionaba perfectamente.
Decidimos preguntar sobre el problema con un poco más de detalle, y resultó que si el automóvil está en el tráfico durante 15 minutos, ETA muestra la misma hora.
Decidimos que esto se debe a la caché porque almacena la hora original y no la actualiza durante 30 minutos.
Este problema también se comprobó en el servicio de mapas de Google. No hubo ningún problema. Los propios servicios muestran esta ETA. Explicamos todo a los probadores y empresas, y aceptaron todo.
El líder de nuestro equipo sugirió conectar otro servicio de ETA y devolver la API de Google como una opción de respaldo y escribir código con la lógica de los servicios de conmutación (el cambio era necesario si las solicitudes superan el número gratuito de solicitudes).
El código funciona de la siguiente manera:
val = getCount() // getting the number of queries used if getMax() <= val { // checking for the limit of free requests for the service used newService = switchService(s) // // if the limit is reached, switch the service return return newService(from, to) // giving the logic of the new service
Encontramos el siguiente servicio Mapbox, lo conectamos, lo instalamos y funcionó. Como resultado, nuestra ETA tuvo:
Este artículo describe cómo estábamos tratando de conectar más servicios para el uso gratuito de ETA porque la empresa no quería pagar por el servicio.
PD: Como desarrollador, creo que si la herramienta es buena y hace bien su trabajo, entonces puedes pagar por los servicios de la herramienta (o buscar proyectos de código abierto.
Espero que te haya sido de utilidad. Gracias por leer este artículo.
Añadir comentario