Eri ohjelmointikielillä testaaminen: Kokeilimme ja vertailimme, kuinka nopeasti eri ohjelmointikielet vastaavat http-kutsuihin. Katso yllättävät tulokset!
Kellottaen pintaa
”Kellottaen pintaa” ilmaisu tulee vanhasta työnsuunnittelun termistä kellottaa, johon liittyy työn pilkkominen osiin ja niiden keston mittaaminen kellon avulla. Tähän liittyy myös vähemmän tykätty työrooli eli niin sanottu ”kellokalle”. Tässä tapauksessa vaan yksittäisten suoritusten kestoajan mittaaminen.
Eri ohjelmointikielillä testaaminen
Kokeillaan eri teknologioilla eli tässä tapauksessa lähinnä eri ohjelmointikielillä määrittelyn mukaisen web – palvelin toteutuksen suorituskykyä. Määrittely koskee http-kutsuihin vastaamista, mutta koodipohja tarjoaa kielestä riippuen mahdollisuuden laajentaa toteutusta esimerkiksi tietokantahakuihin. Testaus tapahtuu samalla koneella, missä serveriä ajetaan. Toteutukset on pyritty tekemään yhden kooditiedoston avulla, joiden lisäksi tulevat käytettävät kirjastot. Ensin on testattu kielen mukana tai muuten helposti tulevaa http-palvelinta sitten on jatkettu vastaavia toteutuksia erillisen http palvelimen alle. Jos ratkaisu ei helposti tukenut moniajoa niin se jätettiin erillisen http-palvelimen asiaksi, samoin id – kenttää ei väkisin synkronoitu, jos toteutus tapahtui moniajona. Eroja kielten välille olisi varmaan tullut lisää jos prosessointitarvetta ohjelmakoodissa olisi lisätty. FastCGI puolelle lähdettäessä on useita palvelinvaihtoehtoja ja eri kielten toimivuus niissä vaihtelee ja näistä tulevia yhdistelmiä olisikin sitten useita, tässä niiden käsittelyä on vasta aloitettu.
Yhteenveto kokeilujen perusteella
Kuormitustestausohjelmista
Http-servereiden kuormitustestauksesta voisi sanoa sen verran, että kannattaa ajaa testiohjelmaa samalla koneella kuin testattavaa http-serveriäkin, ettei tule testanneeksi enimmäkseen nettiyhteyttä. Testiohjelman tulisi olla tehokas, että se itse ei muodostu pullonkaulaksi ja kevyt, että pääosa cpu:sta jäisi testattavalle http-serverille. ”Keep alive” ja ”ipv6” -yhteyksiä olisi hyvä myös tukea. Kokeilin testiohjelmia httperf, ab ja weighttp.
* httperf, tulee käyttöjärjestelmä jakelun mukana, yksisäikeinen kätevä ohjelma
* ab eli ”Apache HTTP server benchmarking tool”, mukana Apache httpd:n työkaluissa, yksisäikeinen kätevä ohjelma
* weighttp, liittyy Lighttpd web serveriin kts: https://fi.wikipedia.org/wiki/Lighttpd, haettavissa tuolta: https://github.com/lighttpd/weighttp.git ja käännettävissä helposti, monisäikeinen kevyt ohjelma, puuttuu tuki post-kutsuille.
Tavallaan se testausohjelma, joka antaa parhaat lukemat http-serverille on itse kevyin.
Toki eri testiohjelmia voi käyttää myös yhdessä.
Erillinen http-palvelin
Erillistä http-palvelinta olisi hyvä käyttää sen tuomien lisämahdollisuuksien, mm. tietoturvan vuoksi. Proxy- eli edustapalvelinratkaisu päälle laittaminen olisi houkutteleva, mutta se johtaisi helposti monoliitin rakentamiseen proxyn taakse ja taustapalvelimien portit olisi konfiguroitava. Mutta, jos tekisi näistä pienistä palveluista FastCGI sovelluksia, niin ne voisi erikseen vaan kääntää ja kopioida varsinaisen http-palvelimen FastCGI – hakemistoon, josta ne olisivat heti käytettävissä ja kutsuttavissa omalla url:llaan. FastCGI palveluiden hyvä puoli CGI – palveluihin nähden on että jokaista http-kutsua varten ei tarvitse käynnistää uutta prosessia, vaan valmis prosessi odottaa holdissa kuluttamatta cpu:ta uutta kutsua esimerkiksi c-ssä: while(FCGI_Accept() >= 0){… jossa luuppi jatkuu kun FCGI_Accept() herää.
Kuormituksen kasvaessa Http-palvelimen FastCGI osaa käynnistää palveluista lisää prosesseja ja kuormituksen pienentyessä vähentää. Ja jos on pelkoa että pieni FastCGI palvelun koodi vuotaa resursseja, niin voidaan asettaa kutsumäärä, jonka jälkeen se sammutetaan ja käynnistetään uudelleen joka tapauksessa.
Apache
Esimerkiksi Apachen http-palvelimeen FastCGI palvelun konfigurointi oletus tiedostoon:
/etc/httpd/conf/httpd.conf, kun apache-mod_fcgid on asennettu, käy lisäämällä oikeille paikoilleen seuraavat:
<IfModule alias_module>
ScriptAlias /fcgi-bin/ ”/var/www/fcgi-bin/”
</IfModule>
<Location /var/www/fcgi-bin>
SetHandler fcgid-script
Options +ExecCGI
Order allow,deny
Allow from all
</Location>
Lisää konfigurointi ohjeita: https://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html
Määritelmä: https://fastcgi-archives.github.io/FastCGI_Specification.html
Wiki ja lisää palvelimia ja kieliä: https://en.wikipedia.org/wiki/FastCGI
Määrittely
Haaveena on, että määrittelyt olisi niin hyviä, että kaikki voisivat koodata sillä kielellä kuin haluavat, kunhan palvelut toimisivat sovitussa ympäristössä ja vastaavat riittävän monta kertaa sekunnissa, reagoivat oikein oikeisiin ja vääriin kutsuihin ja mitä muuta on määritelty. Määrittely olisi hyvä sisältää ajettavat testit. Mikäli toteuttaja vaihtuu ja määrittelyyn tulee myöhemmin muutos, uuden toteuttajan ei olisi pakko katsoa edeltäjän koodia ollenkaan vaan koodata uuden määrittelyn mukainen palvelu. Siksi palveluiden tulisi olla pieniä.
Vaikka vain kokeiltaisiin eri teknologioiden tehokkuutta, olisi hyvä määritellä testiesimerkki, jonka avulla kokeilu suoritetaan.
Tehtävänä on toteuttaa palvelu,
1) joka vastataan http GET kutsuun,
2) jossa on ”who” parametri, joka on pystyttävä poimimaan kutsusta ja palauttamaan yhdistettynä vastaukseen ”quaerit”.
3) Lisäksi halutaan id, jonka palvelun tulee generoida yhdestä tai useammasta juoksevasta sarjasta.
4) Vastaukseen lisätään pieni kevyt vakio kuorma ”message”:”Dolorem ipsum” ja vastauksen tulee olla json-muotoa.
5) Vastaukseen tulee lisätä oikein laskettu http header ”Content-Length”
6) ja vakio header: Content-Type: application/json; charset=utf-8
7) url:n alkuosa voidaan valita toteutuksen mukaan
8) palvelun on toimittava myös ”http Keep Alive”-tilassa vaikka ”Keep-Alive” headeria ei tarvitse palauttaa
Palvelun on vastattava seuraavien esimerkkien mukaisiin testeihin:
a) Kuormitettavuus
weighttp -n 1000000 -c 100 -t 8 -k http://localhost/koeurl?who=Cicero
finished in 37 sec, 493 millisec and 951 microsec, 26670 req/s, 7427 kbyte/s
requests: 1000000 total, 1000000 started, 1000000 done, 1000000 succeeded, 0 failed, 0 errored
status codes: 1000000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 285185850 bytes total, 225566777 bytes http, 59619073 bytes data
b) Headereiden tarkistus ja vastauksen muoto
curl -i -X GET http://localhost/koeurl?who=Cicero
HTTP/1.1 200 OK
Date: Sun, 19 Mar 2023 21:17:31 GMT
Server: Apache/2.4.56 (Mageia) mod_fcgid/2.3.9
Content-Length: 60
Content-Type: application/json; charset=utf-8
{”message”:”Dolorem ipsum”, ”id”:57089, ”quaerit”:”Cicero”}
Testit
Nodejs
Testit
weighttp -n 1000000 -c 100 -t 8 -k http://localhost:8000/nodeans?who=Cicero
finished in 40 sec, 623 millisec and 983 microsec, 24616 req/s, 6319 kbyte/s
requests: 1000000 total, 1000000 started, 1000000 done, 1000000 succeeded, 0 failed, 0 errored
status codes: 1000000 2xx, 0 3xx, 0 4xx, 0 5xx
curl -i -X GET http://localhost:8000/nodeans?who=Cicero
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 62
Date: Mon, 13 Mar 2023 19:53:53 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{”message”:”Dolorem ipsum”, ”id”:1000000, ”quaerit”:”Cicero”}
Koodi
// aja: node http.js
const http = require("http")
const url = require('url')
const host = 'localhost'
const port = 8000
let id = 0
const requestListener = function (req, res) {
// console.log("req: " + JSON.stringify(url.parse(req.url, true).query))
let who = url.parse(req.url, true).query.who
let body = '{"message":"Dolorem ipsum", "id":'+ id++ +', "quaerit":"'+ who +'"}\n'
res.setHeader("Content-Type", "application/json; charset=utf-8")
res.setHeader("Content-Length", body.length)
res.writeHead(200)
res.end(body)
}
const server = http.createServer(requestListener)
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`)
})
Python
Testit
weighttp -n 100000 -c 100 -t 8 -k http://localhost:8000/pyuvans?who=Cicero
finished in 19 sec, 885 millisec and 523 microsec, 5028 req/s, 981 kbyte/s
requests: 100000 total, 100000 started, 100000 done, 100000 succeeded, 0 failed, 0 errored
status codes: 100000 2xx, 0 3xx, 0 4xx, 0 5xx
curl -i -s -X GET http://localhost:8000/pyuvans?who=Cicero
HTTP/1.1 200 OK
date: Thu, 23 Mar 2023 10:56:40 GMT
server: uvicorn
Content-Type: application/json; charset=utf-8
Content-Length: 56
{”message”:”Dolorem ipsum”, ”id”:0, ”quaerit”:”Cicero”}
Koodi
# kts. https://www.uvicorn.org/
# toki jos haluaa hepottaa koodia kts. https://fastapi.tiangolo.com/
import uvicorn
from urllib import parse
id = 0;
async def app(scope, receive, send):
global id
paramss = scope['query_string'].decode("utf-8")
parmd = parse.parse_qs(paramss)
who = parmd['who'][0]
body = b'{"message":"Dolorem ipsum", "id":'+ bytes(str(id),'UTF-8') + b', "quaerit":"'+ bytes(who,'UTF-8') +b'"}\n'
assert scope['type'] == 'http'
await send({
'type': 'http.response.start',
'status': 200,
'headers': [
[b'Content-Type', b'application/json; charset=utf-8'],
[b'Content-Length', bytes(str(len(body)),'UTF-8')],
],
})
await send({
'type': 'http.response.body',
'body': body
})
id += 1
if __name__ == "__main__":
uvicorn.run("pyunians:app", port=8000, log_level="error")
Lisäksi kirjaston lataus
# cat requirements.txt
### virtual envin luonti, joka asentaa myös pip-komennon
# python3 -m venv venv –system-site-packages
# kts. https://www.uvicorn.org/
### virtual envin käyttöön otto
# source venv/bin/activate
# pip install -r requirements.txt
### ajaminen
# uvicorn pyunians:app
# tai
# python pyunians.py
### Tästä alkaa varsinainen virtual env requirements jota pip lukee
uvicorn
Java
Javallakin onnistuu määrittelyn mukainen web-serveri yhdellä lähdekooditiedostolla, tosin sitä olisi luullut nopeammaksi
Testit
weighttp -n 100000 -c 100 -t 8 -k http://localhost:8000/echo?who=Cicero
finished in 43 sec, 901 millisec and 334 microsec, 2277 req/s, 406 kbyte/s
requests: 100000 total, 100000 started, 100000 done, 100000 succeeded, 0 failed, 0 errored
status codes: 100000 2xx, 0 3xx, 0 4xx, 0 5xx
curl -i -s -X GET http://localhost:8000/echo?who=Cicero
HTTP/1.1 200 OK
Date: Mon, 13 Mar 2023 19:37:54 GMT
Content-type: application/json; charset=utf-8
Content-length: 61
{”message”:”Dolorem ipsum”, ”id”:100000, ”quaerit”:”Cicero”}
Koodi
// ajo: java Httpkoe.java
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
public class Httpkoe {
int id = 0;
public static Map<String, String> getParamMap(String query) {
if (query == null || query.isEmpty()) return Collections.emptyMap();
return Stream.of(query.split("&"))
.filter(s -> !s.isEmpty())
.map(kv -> kv.split("=", 2))
.collect(Collectors.toMap(x -> x[0], x -> x[1]));
}
class Handler implements HttpHandler {
public void handle(HttpExchange xchg) throws IOException {
Map params = getParamMap(xchg.getRequestURI().getQuery());
String form = "{\"message\":\"Dolorem ipsum\", \"id\":%d, \"quaerit\":\"%s\"}\n";
xchg.getResponseHeaders().add("Content-Type", "application/json; charset=utf-8");
String response = String.format(form, id, params.get("who"));
xchg.sendResponseHeaders(200, response.length());
OutputStream os = xchg.getResponseBody();
os.write(response.toString().getBytes());
os.close();
id++;
}
}
void aloita(HttpServer server) {
server.createContext("/echo", new Handler());
server.start();
}
public static void main(String[] args) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
Httpkoe httpkoe = new Httpkoe();
httpkoe.aloita(server);
}
}
Go
Go (golang) :lla määrittelyn mukainen juttu tehtiin jo neljännes miljoonaa kertaa sekunnissa
Testit
weighttp -n 10000000 -c 100 -t 8 -k http://localhost:8000/gohttp?who=Cicero
finished in 39 sec, 312 millisec and 300 microsec, 254373 req/s, 45927 kbyte/s
requests: 10000000 total, 10000000 started, 10000000 done, 10000000 succeeded, 0 failed, 0 errored
status codes: 10000000 2xx, 0 3xx, 0 4xx, 0 5xx
curl -i -s -X GET http://localhost:8000/gohttp?who=Cicero
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 14 Mar 2023 19:53:35 GMT
Content-Length: 62
{”message”:”Dolorem ipsum”, ”id”:9807936, ”quaerit”:”Cicero”}
Koodi
// kts https://gobyexample.com/http-servers
// aja: go run gohttpd.go
// tai
// build: go build gohttpd.go
// aja: ./gohttpd
package main
import (
"fmt"
"net/http"
)
var id = 0
func hello(w http.ResponseWriter, req *http.Request) {
who := req.URL.Query().Get("who")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
fmt.Fprintf(w, "{\"message\":\"Dolorem ipsum\", \"id\":%d, \"quaerit\":\"%s\"}\n", id, who)
id++
}
func main() {
http.HandleFunc("/gohttp", hello)
http.ListenAndServe(":8000", nil)
}
C
C:lläkin saa tehtyä määrittelyn mukaisen jutun, ei ihan niin siistillä koodilla kuin go:ssa, mutta yhdellä kooditiedostolla kuitenkin ja päästäänkin sitten jo puolen miljoonan paremmalle puolelle suorituksissa sekunnissa. Mietityttämään jäi tuliko varattu muisti aina vapautettua oikein.
Testit
weighttp -n 10000000 -c 100 -t 8 -k http://localhost:8000/chttp?who=Cicero
finished in 18 sec, 188 millisec and 715 microsec, 549791 req/s, 112153 kbyte/s
requests: 10000000 total, 10000000 started, 10000000 done, 10000000 succeeded, 0 failed, 0 errored
status codes: 10000000 2xx, 0 3xx, 0 4xx, 0 5xx
curl -i -s http://localhost:8000/chttp?who=Cicero
HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Length: 62
Content-Type: application/json; charset=utf-8
Date: Wed, 15 Mar 2023 19:55:04 GMT
{”message”:”Dolorem ipsum”, ”id”:9909580, ”quaerit”:”Cicero”}
Koodi
#define _GNU_SOURCE
#include <microhttpd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
// kts. https://www.gnu.org/software/libmicrohttpd/
// ja https://www.gnu.org/software/libmicrohttpd/tutorial.html
//
// käännös: gcc -O3 -lmicrohttpd microhttpdkoe.c -o microhttpdkoe
// ajo: ./microhttpdkoe 8000
#define PAGE "<html><head><title>libmicrohttpd demo</title>" \
"</head><body>libmicrohttpd demo</body></html>\n"
int id = 0;
enum MHD_Result print_out_key (void *cls, enum MHD_ValueKind kind,
const char *key, const char *value)
{
printf ("%s: %s\n", key, value);
return MHD_YES;
}
static enum MHD_Result
ahc_echo(void * cls,
struct MHD_Connection * connection,
const char * url,
const char * method,
const char * version,
const char * upload_data,
size_t * upload_data_size,
void ** ptr) {
static int dummy;
const char * page = cls;
struct MHD_Response * response;
int ret;
if (0 != strcmp(method, "GET"))
return MHD_NO; /* unexpected method */
if (&dummy != *ptr)
{
/* The first time only the headers are valid,
do not respond in the first round... */
*ptr = &dummy;
return MHD_YES;
}
if (0 != *upload_data_size)
return MHD_NO; /* upload data in a GET!? */
*ptr = NULL; /* clear context pointer */
//MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, &print_out_key, NULL); // debugia varten
//printf("? %s\n", MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, "who"));
const char * who = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, "who");
const char * form = "{\"message\":\"Dolorem ipsum\", \"id\":%d, \"quaerit\":\"%s\"}\n";
char * vastaus;
if (-1 == asprintf (&vastaus, form, id++, who))
{
/* oho */
return MHD_NO;
}
//printf("pit: %d str: %s\n", strlen(vastaus), vastaus);
response = MHD_create_response_from_buffer (strlen(vastaus),
(void*) vastaus,
MHD_RESPMEM_PERSISTENT);
MHD_add_response_header (response, "Content-Type", "application/json; charset=utf-8");
ret = MHD_queue_response(connection,
MHD_HTTP_OK,
response);
MHD_destroy_response(response);
//free(vastaus);
return ret;
}
int main(int argc,
char ** argv) {
struct MHD_Daemon * d;
if (argc != 2) {
printf("%s PORT\n",
argv[0]);
return 1;
}
d = MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION,
atoi(argv[1]),
NULL,
NULL,
&ahc_echo,
PAGE,
MHD_OPTION_END);
if (d == NULL)
return 1;
(void) getc (stdin);
MHD_stop_daemon(d);
return 0;
}
C-Apache-FastCGI
Määrittelyn mukainen toiminto c:llä toteutettuna Apachen fcgi palveluna antaa ihan hyvät vauhdit, Apache hidastaa, mutta tarjoaa mahdollisuuden lisätä kaikkea kivaa kuten filtterit ja staattiset tiedosto yms.
Testaus
weighttp -n 1000000 -c 100 -t 8 -k http://localhost/fcgi-bin/ceefcgi.fcgi?who=Cicero
finished in 17 sec, 791 millisec and 527 microsec, 56206 req/s, 15659 kbyte/s
requests: 1000000 total, 1000000 started, 1000000 done, 1000000 succeeded, 0 failed, 0 errored
status codes: 1000000 2xx, 0 3xx, 0 4xx, 0 5xx
curl -i -X GET http://localhost/fcgi-bin/ceefcgi.fcgi?who=Caius
HTTP/1.1 200 OK
Date: Sun, 19 Mar 2023 20:58:57 GMT
Server: Apache/2.4.56 (Mageia) mod_fcgid/2.3.9
Content-Length: 59
Content-Type: application/json; charset=utf-8
{”message”:”Dolorem ipsum”, ”id”:17862, ”quaerit”:”Caius”}
Koodi
#define _GNU_SOURCE
#include "fcgi_stdio.h"
#include <stdlib.h>
#include <string.h>
// esimerkki fcgi-ohjelma
// käännös gcc ceefcgi.c -o ceefcgi.fcgi -lfcgi -O3 -Wall -Wextra -pedantic -std=c11
int main(void)
{
int id = 0;
while(FCGI_Accept() >= 0){
char *query_string = getenv("QUERY_STRING");
char *who = strstr(query_string, "who=" );
if(!who) who = "";
else who = who + 4;
char * vastaus;
const char * form = "{\"message\":\"Dolorem ipsum\", \"id\":%d, \"quaerit\":\"%s\"}\n";
int contentlen = asprintf(&vastaus, form, id++, who);
printf("Content-Type: application/json; charset=utf-8\r\n"
"Content-Length: %d\r\n"
"\r\n"
"%s",
contentlen, vastaus);
free(vastaus);
}
return 0;
}
Go-Apache-FastCGI
Sitten saman määrittelyn mukainen toiminto go:llä toteutettuna Apachen fcgi palveluna on sekin vauhdikas, eikä tarvitse miettiä onko kaikki free(…) kutsut paikoillaan kuten c:ssä.
Testi
weighttp -n 1000000 -c 100 -t 8 -k http://localhost/fcgi-bin/gosfcgi.fcgi?who=Cicero
finished in 37 sec, 164 millisec and 515 microsec, 26907 req/s, 7493 kbyte/s
requests: 1000000 total, 1000000 started, 1000000 done, 1000000 succeeded, 0 failed, 0 errored
status codes: 1000000 2xx, 0 3xx, 0 4xx, 0 5xx
curl -i -X GET http://localhost/fcgi-bin/gosfcgi.fcgi?who=Cicero
HTTP/1.1 200 OK
Date: Fri, 17 Mar 2023 21:32:16 GMT
Server: Apache/2.4.55 (Mageia) mod_fcgid/2.3.9
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8
{”message”:”Dolorem ipsum”, ”id”:12353, ”quaerit”:”Cicero”}
Koodi
Tuossa go:n fcgi.Serve:ssä ollaan holdissa ja se vastaa c:n while(FCGI_Accept() >= 0){ :äää
// käännös:
// go build gosfcgi.go
// asennus:
// sudo cp gosfcgi /var/www/fcgi-bin/gosfcgi.fcgi
package main
import (
"fmt"
"log"
"strconv"
"net/http"
"net/http/fcgi"
)
var id = 0
func homeView(w http.ResponseWriter, req *http.Request) {
kuka := req.URL.Query().Get("who")
vastaus := fmt.Sprintf("{\"message\":\"Dolorem ipsum\", \"id\":%d, \"quaerit\":\"%s\"}\n", id, kuka)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Content-Length", strconv.Itoa(len(vastaus)))
fmt.Fprintf(w, vastaus)
id++
}
func main() {
err := fcgi.Serve(nil, http.HandlerFunc(homeView)) // nil tässä tarkoitta stedio:n käyttöä
if err != nil {
log.Fatal(err)
}
}