Kmesh и k8s

О Kmesh

Сегодня мы рассмотрим ещё одну, представляющую интерес технологию (из широкого портфолио технологий сообщества дистрибутива openEuler), также предназначенную для увеличения быстродействия. И в этот раз мы посмотрим, как обстоят дела с увеличением производительности инфраструктурного слоя межпроцессных коммуникаций, иначе говоря service mesh и естественно, что всё это будет происходить внутри кластера kubernetes.

Данная технология называется Kmesh, документацию по ней можно найти на официальном сайте документации к openEuler https://docs.openeuler.org/en/docs/23.09/docs/Kmesh/kmesh.html и более полную документацию (но, к сожалению пока только на китайском языке) можно получить в gitee разработчиков по адресу https://gitee.com/openeuler/Kmesh/blob/openEuler-23.09/docs.

Предпосылки появления Kmesh

Разработчики Kmesh посчитали, что текущей производительности современных service mesh не хватает (в документации к Kmesh приводятся данные, что на каждый переход (hop) внутри mesh, Istio увеличивает задержку доступа на 2.65мс). Поэтому, сервисы, чувствительные к увеличению задержки даже на столь малые величины, должны получить возможность обмениваться запросами внутри mesh гораздо быстрее. И как решение данной проблемы должно стать применение технологии Kmesh.

Подробно останавливаться на всех тонкостях внутреннего устройства Kmesh мы не будем, поскольку даже в документации разработчики пометили некоторые модули как «not supported yet». Желающие могут сами ознакомится, используя приведённую выше ссылку на gitee разработчиков. Кратко же, можно сказать, что Kmesh использует ныне модную технологию eBPF и представляет собой модуль для ядра, сервис, а так же утилиты по запуску и настройке.

Схематично устройство Kmesh выглядит следующим образом:

Сразу оговоримся, что данная статья — лишь пробный шар в использовании этой технологии. Kmesh всё ещё является новинкой, поэтому в данной статье мы рассмотрим только базовую установку и попробуем провести простенький тест, который продемонстрирует, насколько эта технология позволяет уменьшить задержку. В следующих же релизах этой технологии, когда она получит развитие, мы обязательно рассмотрим её гораздо подробнее. Нам ещё только предстоит изучить, насколько эта технология подвергается классическому наблюдению (подобно istio) с использованием привычных инструментов, таких как kiali, jagger, zipkin и т.д…

Так же, автор статьи предполагает, что читатель не по наслышке знаком с системой оркестрации контейнеров kubernetes, ему понятны термины, обозначающие объекты и ресурсы этой системы, а так же он знает для чего предназначена Istio и приблизительно понимает, как она работает.

Установка и настройка Kmesh

В качестве service mesh, производительность которого мы будем улучшать мы возьмём Istio (https://istio.io) версии 1.16.7, установленный на кластер k8s версии 1.22.17.

Из документации к Kmesh мы находим, что этот сервис можно поставить в двух вариантах:

  1. В виде пакета, который ставим на каждый узел кластера и запускаем сервис при помощи systemd.
  2. В виде DaemonSet, который запускаем в кластере k8s. Предварительно надо собрать docker образ, в который включим пакет Kmesh.

В данной статье мы будем использовать первый подход.

У нас уже есть кластер k8s, давайте посмотрим лист описания его узлов:

$ kubectl get no -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k8s-2309-m1 Ready control-plane,master 23d v1.22.17 172.17.3.61 <none> openScaler 23.09 6.4.0-10.1.0.20.os2309.x86_64 iSulad://2.1.3
k8s-2309-m2 Ready control-plane,master 23d v1.22.17 172.17.3.62 <none> openScaler 23.09 6.4.0-10.1.0.20.os2309.x86_64 iSulad://2.1.3
k8s-2309-m3 Ready control-plane,master 23d v1.22.17 172.17.3.63 <none> openScaler 23.09 6.4.0-10.1.0.20.os2309.x86_64 iSulad://2.1.3
k8s-2309-w1 Ready <none> 23d v1.22.17 172.17.3.64 <none> openScaler 23.09 6.4.0-10.1.0.20.os2309.x86_64 iSulad://2.1.3

Из этого описания мы видим, что он развёрнут на базе ОС OpenScaler 23.09, при этом в качестве runtime у него используется iSulad 2.1.3.

Теперь, переходим к собственно установке и для этого на каждом узле кластера выполняем команду:

dnf install -y Kmesh

После установки пакета нужно настроить сервис Kmesh, для этого необходимо отредактировать файл /etc/kmesh/kmesh.json, заменив дефолтный адрес 192.168.0.1 на наш адрес (имеется в виду адрес внутри kubernetes) сервиса istiod.

Чтобы получить адрес, воспользуемся командой:
$ kubectl -n istio-system get svc | grep istiod
istiod ClusterIP 10.10.87.62 <none> 15010/TCP,15012/TCP,443/TCP,15014/TCP 18d

Мы видим, что у нас адрес серсиса istiod – 10.10.87.62. Прописываем этот адрес в конфиг /etc/kmesh/kmesh.json:

    "clusters": [
     {
       "name": "xds-grpc",
       "type" : "STATIC",
       "connect_timeout": "1s",
       "lb_policy": "ROUND_ROBIN",
       "load_assignment": {
         "cluster_name": "xds-grpc",
         "endpoints": [{
           "lb_endpoints": [{
             "endpoint": {
               "address":{
                 "socket_address": {
                   "protocol": "TCP",
                   "address": "10.10.87.62",
                   "port_value": 15010
                 }
               }
             }
           }]
         }]
        },

Для запуска Kmesh надо дать следующие команды:
systemctl enable kmesh
systemctl start kmesh

Также, всё это можно сделать одной командой:
systemctl enable –now kmesh

Но пока мы подождём давать эти команды. Для начала мы поднимем тестовое веб-приложение, на котором мы будем тестировать производительность запросов и снимем показания производительности без использования технологии Kmesh.

Подготовка стенда для тестирования

Для тестирования мы развернём типовую схему, где у нас будет фронт, обслуживающий внешние запросы и бэкенд, к которому будет обращаться фронт. Фронтом у нас выступит nginx, а в качестве бэкенда мы возьмём приложение https://github.com/mendhak/docker-http-https-echo, которое в ответ на запрос выводит клиенту все заголовки, которые приложение echo получило в запросе. Отсутствие в работе бэкенда какой-то навороченной внутренней логики позволяет снизить эффект её влияния на результаты тестирования.

Деплой и сервис для бэкенда echo (файл echo-test.yaml):

apiVersion: apps/v1
kind: Deployment
metadata:
name: echo1
namespace: test1
labels:
   app: echo
   version: v1
spec:
replicas: 1
selector:
   matchLabels:
     app: echo
template:
   metadata:
     labels:
       app: echo
       version: v1
   spec:
     containers:
     - name: echo
       image: mendhak/http-https-echo
       ports:
       - containerPort: 8080
         name: http-echo
       resources:
         requests:
           cpu: 10m
           memory: 25Mi
---
apiVersion: v1
kind: Service
metadata:
name: echo1
namespace: test1
spec:
selector:
   app: echo
   version: v1
ports:
- name: http-echo
   port: 8080
   targetPort: http-echo
    protocol: TCP

Конфиг для нашего фронт nginx (файл cm-ngx-frontend-conf.yaml):

apiVersion: v1
data:
nginx.conf: |-
   worker_processes  auto;
   pid        /nginx.pid;
   worker_rlimit_nofile 65536;
   events {
     worker_connections  4096;
   }

   http {
     default_type application/octet-stream;
     sendfile     on;
     tcp_nopush   on;

      log_format  main  '[$time_local] $remote_addr $remote_user $host:$server_port $server_name $proxy_add_x_forwarded_for '
           '$request $request_time $msec $status $bytes_sent $http_referer $http_user_agent '
           '$http_cookie $http_x_forwarded_for_y $http_x_forwarded_for '
           '$upstream_http_x_host $upstream_http_x_requestid $upstream_addr $upstream_status $upstream_response_time';

     access_log /var/log/nginx/access.log main;
     error_log /dev/stderr info;
     upstream echo1 {
         server echo1:8080 fail_timeout=20s ;
         keepalive 32;
       }

      server {
         listen 80;
         location / {
             return 200;
         }

          location /echo {
             proxy_pass http://echo1;
             proxy_set_header Host $host;
             proxy_intercept_errors on;
             proxy_http_version 1.1;
         }
     }
   }
kind: ConfigMap
metadata:
name: ngx-frontend-cfg
namespace: test1

Деплой для фронта (файл dep-ngx-frontend-a.yaml)

apiVersion: apps/v1
kind: Deployment
metadata:
name: ngx-frontend-a
namespace: test1
labels:
   app: ngx-frontend-a
   version: v1
spec:
replicas: 1
selector:
   matchLabels:
     app: ngx-frontend-a
     version: v1
template:
   metadata:
     labels:
       app: ngx-frontend-a
       version: v1
   spec:
     containers:
       - name: main-frontend-a
         image: nginx:1.25.3
         imagePullPolicy: IfNotPresent
         ports:
           - name: http-front
             containerPort: 80
         resources:
           requests:
             cpu: 10m
             memory: 25Mi
         volumeMounts:
           - name: conf
             mountPath: /etc/nginx/nginx.conf
             readOnly: true
             subPath: nginx.conf
           - name: www
             mountPath: /var/www
             readOnly: false
     volumes:
       - name: conf
         configMap:
           defaultMode: 444
           name: ngx-frontend-cfg
       - name: www
         emptyDir: {}

Сервис фронта (файл svc-ngx-frontend-a.yaml)

apiVersion: v1
kind: Service
metadata:
name: ngx-frontend-a
namespace: test1
labels:
   ngx: ngx-frontend-a
spec:
ports:
- port: 80
   name: http-front
   targetPort: http-front
selector:
   app: ngx-frontend-a

Давайте развёрнём наш тестовый стенд. Для начала, нам надо будет создать namespace test1:

$ kubectl create ns test1
namespace/test1 created

теперь мы заходим в каталог, где у нас находятся все наши заранее подготовленные yaml файлы и дадим команду:

kubectl apply -f cm-ngx-frontend-conf.yaml -f dep-ngx-frontend-a.yaml -f echo-test.yaml -f svc-ngx-frontend-a.yaml

Проверяем, что все наши приложения поднялись:

$ kubectl -n test1 get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
echo1-7454566d9-5cm7p 2/2 Running 0 19s 10.122.132.111 k8s-2309-m1 <none> <none>
ngx-frontend-a-f584f8f94-2pxhv 2/2 Running 0 12s 10.122.227.19 k8s-2309-m3 <none> <none>
ngx-frontend-a-f584f8f94-jxwsl 2/2 Running 0 14s 10.122.36.108 k8s-2309-w1 <none> <none>
ngx-frontend-a-f584f8f94-pvszx 2/2 Running 0 14s 10.122.110.26 k8s-2309-m2 <none> <none>
ngx-frontend-a-f584f8f94-wvc95 2/2 Running 0 12s 10.122.132.112 k8s-2309-m1 <none> <none>

Теперь нам надо опубликовать наружу наш фронт, чтобы мы могли посылать запросы в наш веб-сервис по сети.

Так как мы используем Istio, то мы создадим Gateway, VirtualService и DestinationRules:

Файл gateway.yaml:

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: gate-front-http
namespace: test1
spec:
selector:
   istio: ingressgateway
servers:
- port:
     number: 80
     name: http-front
     protocol: HTTP
   hosts:
   - front.test.local

Файл vs-front-http.yaml:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: vs-front-http
namespace: test1
spec:
hosts:
- "front.test.local"
gateways:
- gate-front-http
http:
- name: "main web page"
   match:
   - uri:
       prefix: /
   route:
   - destination:
       host: ngx-frontend-a
       port:
         number: 80

Файл dr-front.yaml:

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: front
namespace: test1
spec:
host: ngx-frontend-a.test1.svc.cluster.local
trafficPolicy:
   tls:
     mode: DISABLE

И применим их:

kubectl apply -f gateway.yaml -f vs-front-http.yaml -f dr-front.yaml

Теперь, давайте вспомним, какой внешний адрес нашего кластера kubernetes отвечает за запросы снаружи:

$ kubectl -n istio-system get svc | grep gateway
istio-ingressgateway LoadBalancer 10.10.166.221 172.17.3.110 15021:30782/TCP,80:32080/TCP,443:31638/TCP 19d

Видим, что адрес внешнего gateway для входящих запросов у нас 172.17.3.110, будем использовать его при запросах.

Пробуем теперь сделать запрос:

curl  -H 'Host: front.test.local' http://172.17.3.110/echo
{
"path": "/echo",
"headers": {
   "host": "front.test.local",
   "user-agent": "curl/8.1.2",
   "accept": "*/*",
   "x-forwarded-for": "172.17.3.61",
   "x-forwarded-proto": "http",
   "x-request-id": "2ca6087e-b363-431d-a881-20e995b44f2a",
   "x-envoy-decorator-operation": "ngx-frontend-a.test1.svc.cluster.local:80/*",
   "x-envoy-attempt-count": "1",
   "x-envoy-internal": "true",
   "x-envoy-peer-metadata": "CiMKDkFQUF9DT05UQUlORVJTEhEaD21haW4tZnJvbnRlbmQtYQoaCgpDTFVTVEVSX0lEEgwaCkt1YmVybmV0ZXMKHwoMSU5TVEFOQ0VfSVBTEg8aDTEwLjEyMi4yMjcuMTkKGQoNSVNUSU9fVkVSU0lPThIIGgYxLjE2LjcKwAEKBkxBQkVMUxK1ASqyAQoXCgNhcHASEBoObmd4LWZyb250ZW5kLWEKJAoZc2VjdXJpdHkuaXN0aW8uaW8vdGxzTW9kZRIHGgVpc3RpbwozCh9zZXJ2aWNlLmlzdGlvLmlvL2Nhbm9uaWNhbC1uYW1lEhAaDm5neC1mcm9udGVuZC1hCisKI3NlcnZpY2UuaXN0aW8uaW8vY2Fub25pY2FsLXJldmlzaW9uEgQaAnYxCg8KB3ZlcnNpb24SBBoCdjEKGgoHTUVTSF9JRBIPGg1jbHVzdGVyLmxvY2FsCigKBE5BTUUSIBoebmd4LWZyb250ZW5kLWEtZjU4NGY4Zjk0LTJweGh2ChQKCU5BTUVTUEFDRRIHGgV0ZXN0MQpQCgVPV05FUhJHGkVrdWJlcm5ldGVzOi8vYXBpcy9hcHBzL3YxL25hbWVzcGFjZXMvdGVzdDEvZGVwbG95bWVudHMvbmd4LWZyb250ZW5kLWEKFwoRUExBVEZPUk1fTUVUQURBVEESAioACiEKDVdPUktMT0FEX05BTUUSEBoObmd4LWZyb250ZW5kLWE=",
   "x-envoy-peer-metadata-id": "sidecar~10.122.227.19~ngx-frontend-a-f584f8f94-2pxhv.test1~test1.svc.cluster.local",
   "x-b3-traceid": "5ee40739ba134e5a1422feb2aa3f28b7",
   "x-b3-spanid": "80bd0386edd91b0d",
   "x-b3-parentspanid": "1422feb2aa3f28b7",
   "x-b3-sampled": "0"
},
"method": "GET",
"body": "",
"fresh": false,
"hostname": "front.test.local",
"ip": "172.17.3.61",
"ips": [
   "172.17.3.61"
],
"protocol": "http",
"query": {},
"subdomains": [
   "front"
],
"xhr": false,
"os": {
   "hostname": "echo1-7454566d9-5cm7p"
},
"connection": {}

Отлично, запрос работает. Из результатов мы видим, что запрос прошёл через фронт сервер ngx-frontend-a-f584f8f94-2pxhv.

Таким образом, мы можем сделать скрипт, который будет посылать запросы (при помощи утилиты curl), замерять времена time_connect, time_starttransfer, time_total (единица измерения — секунда), а так же фиксировать, через какой фронт сервер прошёл запрос.

Маленькое пояснение, по используемым в тестировании временам:

  • time_connect — это время, затраченное на подключение по TCP
  • time_starttransfer — время, которое, которое сервер затратил на обработку запроса, до времени посылки первого байта ответа. По сути — это показатель длительности подготовки сервера к ответу на запрос.
  • time_total — общая продолжительность запроса curl.

Тестирование с выключенным сервисом Kmesh

Подготавливаем скрипт:

#!/bin/bash
CURL_RESULT="curl_result"
OUT_RESULT=test.result

for i in {1..100}
do
       TIME_SCORE=$(curl -o $CURL_RESULT -s -w "%{time_connect}:%{time_starttransfer}:%{time_total}n"  -H 'Host: front.test.local' http://172.17.3.110/echo)
       front_test=$(grep x-envoy-peer-metadata-id  $CURL_RESULT  | awk  -F """ '{print $4}')
       f1m1=$(echo $front_test | grep wvc95)
       f2m2=$(echo $front_test | grep pvszx)
       f3m3=$(echo $front_test | grep 2pxhv)
       f4w1=$(echo $front_test | grep jxwsl)
       if [[ -n $f1m1 ]]
       then
               front=M1
       elif [[ -n $f2m2 ]]
       then
               front=M2
       elif [[ -n $f3m3 ]]
       then
               front=M3
       elif [[ -n $f4w1 ]]
       then
               front=W1
       else
               front=NOT
       fi
       echo $front $TIME_SCORE >> $OUT_RESULT
       rm $CURL_RESULT
       sleep 0.2
done

Видим, что скрипт посылает 100 запросов с задержкой между запросами 200мс и записывает результаты тестирования в файл.

В результате работы скрипта, мы получили следующие (средние) данные по замеряемым временам:

Для фронта ngx-frontend-a-f584f8f94-wvc95:
time_connect – 0,000462
time_starttransfer – 0,006229
time_total – 0,006287


Для фронта ngx-frontend-a-f584f8f94-pvszx:
time_connect – 0,000464
time_starttransfer – 0,006497
time_total – 0,006555


Для фронта ngx-frontend-a-f584f8f94-2pxhv:
time_connect – 0,000554
time_starttransfer – 0,013321
time_total – 0,013381


И для фронта ngx-frontend-a-f584f8f94-jxwsl:
time_connect – 0,000473
time_starttransfer – 0,006493
time_total – 0,006548

Тестирование с включенным сервисом Kmesh

Теперь, давайте включим сервис Kmesh на узлах (для этого мы применим команды, которые мы уже упоминали ранее), убедимся, что модуль ядра kmesh подгрузился в память и запустим тесты заново.

И для фронта ngx-frontend-a-f584f8f94-wvc95:
time_connect – 0,000413
time_starttransfer – 0,005036
time_total – 0,005090

Для фронта ngx-frontend-a-f584f8f94-pvszx:
time_connect – 0,000450
time_starttransfer – 0,004982
time_total – 0,005040

И для фронта ngx-frontend-a-f584f8f94-2pxhv:
time_connect – 0,000422
time_starttransfer – 0,004885
time_total – 0,004939

И для фронта ngx-frontend-a-f584f8f94-jxwsl:
time_connect – 0,000408
time_starttransfer – 0,005035
time_total – 0,005092

Сравнение результатов и подведение итогов

Сведя полученные результаты и произведя подсчёты, мы увидели следующие результаты:

 

На сколько процентов с kmesh быстрее, чем без него:

 

connect

start transfer

total

 

 

 

 

ngx-frontend-a-f584f8f94-wvc95

12,07

23,71

23,50

ngx-frontend-a-f584f8f94-pvszx

3,00

30,40

30,07

ngx-frontend-a-f584f8f94-2pxhv

31,37

172,70

170,91

ngx-frontend-a-f584f8f94-jxwsl

15,86

28,97

28,60

Во-первых, мы увидели, что у нас на стенде есть «плохой узел», где работает ngx-frontend-a-f584f8f94-2pxhv показания которого сильно выбиваются из общей картины (судя по цифрам, прирост в скорости составил нереальные 170%). Напомню, что всё тестирование у нас происходит в кластере k8s, развёрнутым на виртуальных машинах. Поэтому, результаты ngx-frontend-a-f584f8f94-2pxhv можно особо не учитывать, но мы оставим их в табличке, пусть будут.

Во-вторых мы видим, что если время подключения у нас изменилось очень мало, то времена start transfer и total с использованием технологии Kmesh дали ощутимый прирост, почти до 30%.

Отсюда можно сделать вывод, что технология Kmesh представляет существенный интерес для её дальнейшего изучения и более тщательного тестирования, чем мы и постараемся заняться по мере выхода новых версий этой технологии.