Kubernetes. типы services

Thank you for reading this post, don't forget to subscribe!

ClusterIP
Endpoint
Сер­ви­сы без селекторов
EndpointSlice
Headless Service
ExternalName
External IPs
NodePort

Поды – это не посто­ян­ные сущ­но­сти кла­сте­ра. В любой момент вре­ме­ни вы може­те доба­вить новый под или уда­лить не нуж­ный. При пере­ме­ще­нии пода меж­ду нода­ми кла­сте­ра, под созда­ет­ся на новой ноде и уда­ля­ет­ся на ста­рой. При этом у пода меня­ет­ся IP адрес. Имен­но поэто­му не сто­ит обра­щать­ся к поду по ip адресу.

В Kubernetes, для досту­па к поду (набо­рам подов) исполь­зу­ют­ся сер­ви­сы (service). Сер­вис – это абстрак­ция, опре­де­ля­ю­щая набор подов и поли­ти­ку досту­па к ним.

Пред­по­ло­жим, что в систе­ме есть при­ло­же­ние, про­из­во­дя­щее обра­бот­ку запро­сов. При­ло­же­ние рабо­та­ет без сохра­не­ния состо­я­ния и поэто­му может лег­ко гори­зон­таль­но мас­шта­би­ро­вать­ся. Для обра­бот­ки пото­ка запро­сов нам потре­бо­ва­лось несколь­ко экзем­пля­ров при­ло­же­ния (подов) и нам необ­хо­ди­мо рас­пре­де­лить нагруз­ку меж­ду ними.

Если бы мы не исполь­зо­ва­ли Kubernetes, нам бы при­шлось ста­вить перед при­ло­же­ни­я­ми какую-то про­грам­му, зани­ма­ю­щу­ю­ся рас­пре­де­ле­ни­ем запро­сов. Напри­мер nginx. И каж­дый раз при изме­не­нии коли­че­ства при­ло­же­ний, при пере­ез­де при­ло­же­ния с одно­го сер­ве­ра на дру­гой пере­на­стра­и­вать nginx.

В Kubernetes забо­ту о рас­пре­де­ле­нии нагруз­ки или досту­па к груп­пе при­ло­же­ний ложит­ся на сер­вис. При опре­де­ле­нии сер­ви­са обыч­но доста­точ­но ука­зать селек­тор, опре­де­ля­ю­щий выбор подов, на кото­рые будут пере­сы­лать­ся запро­сы. Так же суще­ству­ет воз­мож­ность опре­де­ле­ния сер­ви­сов без селекторов

ClusterIP

Ути­ли­та kubectl может создать сер­вис исполь­зуя аргу­мен­ты команд­ной стро­ки. Но мы будем поль­зо­вать­ся yaml файлами.

 

В при­ве­дён­ном выше при­ме­ре созда­ёт­ся сер­вис с име­нем service-name. Kubernetes при­сва­и­ва­ет сер­ви­су ip адрес и добав­ля­ет его дан­ные в DNS сер­вер. После это­го вы може­те обра­тить­ся к сер­ви­су по его име­ни. В нашем слу­чае сер­вис при­ни­ма­ет запро­сы на 80-м пор­ту и, если мы нахо­дим­ся в том же нейм­с­пей­се, мы можем обра­тить­ся к нему http://service-name. Если в дру­гом, то с ука­за­ни­ем нейм­с­пей­са: http://service-name.namespace.svc

При­хо­дя­щие запро­сы сер­вис будет пере­сы­лать на порт 8080 подам с мет­ка­ми (label) app: selector. Если в систе­ме будет несколь­ко подов с таким селек­то­ром, сер­вис будет пере­рас­пре­де­лять запро­сы меж­ду ними. По умол­ча­нию по алго­рит­му round robbin.

В каче­стве зна­че­ния targetPort мож­но исполь­зо­вать име­на пор­тов. Конеч­но, если вы его опи­са­ли при опре­де­ле­нии пода. Это удоб­но, если вы ссы­ла­е­тесь на поды, у кото­рых опре­де­ле­ны раз­ные номе­ра пор­тов, но под одним именем.

Рас­смот­рим при­мер. Deployment для сер­ве­ра Tomcat.

Файл 01-deployment.yaml.

В deployment ука­за­но нали­чие двух реплик (подов) при­ло­же­ния. Объ­яв­ля­ет­ся порт при­ло­же­ния 8080, с назва­ни­ем tomcat. При­ло­же­нию при­сва­и­ва­ет­ся мет­ка: app: tomcat.

Сер­вис, предо­став­ля­ю­щий досту­пы к этим подам мож­но объ­явить сле­ду­ю­щим образом:

Файл 02-service.yaml

В раз­де­ле selector мы ука­зы­ва­ем мет­ку при­ло­же­ния, на кото­рые мы будем ссы­лать­ся. В раз­де­ле ports гово­рим, что к сер­ви­су нуж­но обра­щать­ся на 80-й порт. Запрос будет пере­слан при­ло­же­нию на порт, име­ю­щий имя tomcat.

При­ме­ним фай­лы манифеста.

kubectl apply -f 01-deployment.yaml
kubectl apply -f 02-service.yaml

При­ме­ча­ние. Кла­стер kuberntes уста­нав­ли­вал­ся со сле­ду­ю­щи­ми пара­мет­ра­ми сети:

Посмот­рим инфор­ма­цию о запу­щен­ных подах. Вывод про­грам­мы немно­го обрезан.

Если обра­тить­ся к любо­му поду напря­мую, мы уви­дим ответ сер­ве­ра tomcat.

Cпи­сок сер­ви­сов в namespace default:

Посмот­рим подроб­нее на сервис.

# kubectl get svc tomcat-main -o yaml

Систе­ма выде­ли­ла сер­ви­су вир­ту­аль­ный ip адрес:

clusterIP: 10.233.47.171

Адрес вир­ту­аль­ный. Это зна­чит, что вы не най­де­те на машине Linux интер­фей­са с таким ip. Сле­ду­ет отме­тить, что за выда­чу ip для сер­ви­сов отве­ча­ет kube-proxy, а не модуль IPAM драй­ве­ра сети Kubernetes.

Обра­ти­те вни­ма­ние, что ip адре­са сер­ви­сам выда­ют­ся из диа­па­зо­на, опре­де­лен­но­го при помо­щи serviceSubnet, задан­ном при уста­нов­ке кластера.

В прин­ци­пе, ip адрес мож­но опре­де­лять сра­зу в фай­ле мани­фе­ста. Если это ip занят – то вы полу­чи­те сооб­ще­ние об ошиб­ке. Но нет осо­бо­го смыс­ла зани­мать­ся явным ука­за­ни­ем ip, посколь­ку к сер­ви­су мы все­гда будем обра­щать­ся по име­ни, а не по ip.

Так же сле­ду­ет обра­тить вни­ма­ние на:

type: ClusterIP

По умол­ча­нию созда­ют­ся сер­ви­сы типа ClusterIP.

После опре­де­ле­ния сер­ви­са мы можем обра­тить­ся к нему и полу­чить ответ одно­го из tomcat. Да, тут мы обра­ща­ем­ся к сер­ви­су по его ip адре­су, но толь­ко пото­му, что дела­ем это в кон­со­ли маши­ны Linux, а не внут­ри како­го-либо пода Kubernetes. Linux маши­на не исполь­зу­ет DNS Kubernetes и её кли­ент не может раз­ре­шать внут­рен­ние име­на Kubernetes в ip адре­са.

Endpoint

Каким обра­зом про­ис­хо­дит связь меж­ду сер­ви­сом и пода­ми? «За кад­ром» остал­ся еще один эле­мент – endpoint.

Посмот­рим на него подробнее.

# kubectl get ep tomcat-main -o yaml

В слу­чае сер­ви­сов, исполь­зу­ю­щих селек­тор, такие endpoints созда­ют­ся авто­ма­ти­че­ски. Систе­ма сама нахо­дит ip адре­са подов, име­ю­щих соот­вет­ству­ю­щие мет­ки, и фор­ми­ру­ет запи­си в endpoint.

Как такие свя­зи будут выгля­деть с точ­ки зре­ния Linux маши­ны, зави­сит от режи­ма рабо­ты kube-proxy. Ведь имен­но он управ­ля­ет сер­ви­са­ми. Обыч­но исполь­зу­ют iptables или ipvs. С точ­ки зре­ния быст­ро­дей­ствия пред­по­чти­тель­нее исполь­зо­вать режим ipvs.

По сво­ей сути перед нами nat преобразование.

Сервисы без селекторов.

Сер­ви­сы без селек­то­ров обыч­но исполь­зу­ют­ся для обра­ще­ния за пре­де­лы кла­сте­ра по ip адре­су к како­му либо приложению.

В каче­стве при­ме­ра возь­мем mail.ru. Конеч­но, луч­ше в каче­стве при­ме­ра исполь­зо­вать какую либо базу дан­ных, но у тако­вой базы не ока­за­лось под рукой

# host mail.ru
mail.ru has address 217.69.139.200
mail.ru has address 94.100.180.200
mail.ru has address 217.69.139.202
mail.ru has address 94.100.180.201

Опре­де­ле­ние сервиса:

Файл 03-service-mail-ru.yaml

Порт 8080 у сер­ви­са был постав­лен в каче­стве экс­пе­ри­мен­та. Мож­но оста­вить его зна­че­ние рав­ным 80.

Опре­де­ле­ние endpoint. Имя сер­ви­са и endpoint долж­ны совпадать.

 

Файл 04-endpoint-mail-ru.yaml

IP-адре­са конеч­ных точек не долж­ны быть: loopback (127.0.0.0/8 для IPv4, :: 1/128 для IPv6) или локаль­ны­ми для ссыл­ки (169.254.0.0/16 и 224.0.0.0/24 для IPv4, fe80 :: / 64 для IPv6).

IP-адре­са конеч­ных точек не могут быть IP-адре­са­ми дру­гих служб кла­сте­ра kubernetes, пото­му что kubeproxy не под­дер­жи­ва­ет вир­ту­аль­ные IP-адре­са в каче­стве пунк­та назначения.

 

Про­ве­рим, можем ли мы обра­щать­ся к ново­му сервису:

 

Рабо­тать с ip адре­са­ми не удоб­но. Запу­стим в кла­сте­ре какой ни будь под и обра­тим­ся к сер­ви­сам по их именам.

 

EndpointSlice

У связ­ки сер­вис + endpoint суще­ство­ва­ла боль­шая про­бле­ма: все endpoint кла­сте­ра – это объ­ек­ты API. Все объ­ек­ты API хра­нят­ся в базе дан­ных etcd. И самое страш­ное, что запи­си объ­ек­тов endpoints хра­ни­лись в базе в одном ресур­се. При уве­ли­че­нии коли­че­ства endpoints (чита­ем подов), ресурс при­об­ре­тал про­сто огром­ные раз­ме­ры. Пред­ставь­те себе что в одной запи­си в БД хра­нят­ся ip адре­са почти всех подов кластера!

Про­бле­мы воз­ник­ли в боль­ших кла­сте­рах. При изме­не­нии endpoint, при­хо­ди­лось пере­чи­ты­вать весь объ­ект из базы, что при­во­ди­ло к боль­шо­му сете­во­му трафику.

Начи­ная с вер­сии 1.17 в Kubernetes доба­ви­ли EndpointSlice. При помо­щи него объ­ект содер­жа­щий endpoint систе­мы раз­би­ли на кус­ки (слай­сы). Теперь endpoints хра­нят­ся в EndpointSlice. Раз­би­е­ние про­ис­хо­дит авто­ма­ти­че­ски. По умол­ча­нию в одном slice хра­нит­ся око­ло 100 endpoints.

# kubectl get endpointslice tomcat-main-jf6dq -o yaml

Вывод команд я немно­го сократил.

Как вид­но из опи­са­ния, EndpointSlice содер­жит набор пор­тов, кото­рые при­ме­ня­ют­ся ко всем endpoints. Мы можем в одном сер­ви­се опре­де­лить несколь­ко пор­тов. В резуль­та­те на один сер­вис может быть созда­но несколь­ко EndpointSlices.

 

Headless Service

нек­сус я запус­каю сле­ду­ю­щим образом:

 

Выше мы запу­сти­ли Nexus в нашем кла­сте­ре. В фай­ле мани­фе­ста было опре­де­ле­но два сервиса

Нас инте­ре­су­ет сер­вис под назва­ни­ем nexus

Обра­ти­те вни­ма­ние на послед­нюю стро­ку: clusterIP: None. Мы гово­рим систе­ме, что у дан­но­го сер­ви­са не будет вир­ту­аль­но­го ip адре­са. Таким обра­зом, мы опре­де­ля­ем «без­го­ло­вый» сервис.

Вот так он отоб­ра­жа­ет­ся в команд­ной стро­ке при запро­се спис­ка сервисов.

В систе­ме для дан­но­го сер­ви­са созда­ёт­ся endpoint. Без это­го никак.

Что же такое headless сер­вис? Это про­сто запись А в систе­ме DNS. Т.е. имя сер­ви­са пре­об­ра­зу­ет­ся не в вир­ту­аль­ный ip, его (ip) у нас нет, а сра­зу в ip пода.

Уве­ли­чим коли­че­ство подов nexus:

# kubectl scale --replicas=2 statefulset nexus

Немно­го подо­ждем. Запи­си в систе­ме DNS появ­ля­ют­ся с неболь­шой задержкой.

Посмот­рим спи­сок endpoints.

Запу­стим dnstools и пошлем запрос к DNS кластера.

Мы видим, что у нас появи­лись две запи­си типа А, ука­зы­ва­ю­щие на ip адре­са подов. Посколь­ку не созда­ёт­ся вир­ту­аль­ный ip сер­ви­са, т.е. не созда­ёт­ся NAT пре­об­ра­зо­ва­ние. Мы можем по име­ни сер­ви­са напря­мую обра­тить­ся к поду (подам) без затрат вре­ме­ни на лиш­ние преобразования.

Таким обра­зом, если рабо­те ваше­го при­ло­же­ния про­ти­во­по­ка­за­ны NAT пре­об­ра­зо­ва­ния – исполь­зуй­те headless сер­ви­сы. Прав­да рав­но­мер­но­го рас­пре­де­ле­ния нагруз­ки вы не полу­чи­те, DNS не уме­ет это­го делать. Но чем-то при­хо­дит­ся жертвовать.

ExternalName

В преды­ду­щих при­ме­рах у нас немно­го неудач­но про­ис­хо­ди­ла ссыл­ка на mail.ru. В общем то была постав­ле­на про­стая зада­ча: обра­тить­ся внут­ри кла­сте­ра к mail.ru не по его име­ни, а при помо­щи сер­ви­са mail-ru. Что-то типа: http://mail-ru

Решим зада­чу пра­виль­но. Сна­ча­ла уда­ли­те ста­рый сервис.

# kubectl delete svc mail-ru
При­ме­ним мани­фест из фай­ла 06-external-name.yaml.

Сер­вис типа ExternalName добав­ля­ет запись типа CNAME во внут­рен­ний DNS сер­вер Kubernetes.

Для это­го сер­ви­са не созда­ёт­ся endpoint. Поэто­му сра­зу пере­хо­дим к запро­сам к DNS.

 

External IPs

Не сове­тую вам экс­пе­ри­мен­ти­ро­вать с полем externalIPs. Вни­ма­тель­но про­чи­тай­те что будет напи­са­но ниже. Если реши­тесь на экс­пе­ри­мен­ты — не под­став­ля­е­те ip адре­са, на кото­рых висит API сер­вер кла­сте­ра. Исполь­зуй­те multimaster уста­нов­ку кла­сте­ра, если оши­бё­тесь, мож­но пере­клю­чить­ся на дру­гую ноду и испра­вить ошиб­ку. И не гово­ри­те, что я вас не предупреждал.

Почти в любом опре­де­ле­ние сер­ви­са мож­но доба­вить поле externalIPs, в кото­ром мож­но ука­зать ip маши­ны кла­сте­ра. При обра­ще­нии на этот ip и ука­зан­ный в сер­ви­се порт, запрос будет пере­бро­шен на соот­вет­ству­ю­щий сервис.

В каче­стве при­ме­ра добавь­те али­ас на сете­вой интер­фейс. Напри­мер вот так:

# ifconfig ens33:ext 192.168.218.178

оста­вим один под nexus.

# kubectl scale --replicas=1 statefulset nexus
При­ме­ним мани­фест из фай­ла 07-external-ip.yaml.

Посмот­рим спи­сок сервисов.

Посмот­рим таб­ли­цу преобразований.

Попро­бу­ем обра­тить­ся по ука­зан­но­му ip.

# curl 192.168.218.178:8888
Тут будет боль­шой ответ nexus

Очень похо­же на NodePort. Но толь­ко похоже.

 

NodePort

Сер­ви­сы типа NodePort откры­ва­ют порт на каж­дой ноде кла­сте­ра на сете­вых интер­фей­сах хоста. Все запро­сы, при­хо­дя­щие на этот порт, будут пере­сы­лать­ся на endpoints, свя­зан­ные с дан­ным сервисом.

Диа­па­зон пор­тов, кото­рый мож­но исполь­зо­вать в NodePort — 30000-32767. Но его мож­но изме­нить при кон­фи­гу­ра­ции кластера.

При­мер сер­ви­са типа NodePort.

При опи­са­нии сер­ви­са необ­хо­ди­мо явно ука­зать его тип. Если не ука­зать зна­че­ние пор­та, при помо­щи пара­мет­ра spec.ports.nodePort, порт при­сва­и­ва­ет­ся авто­ма­ти­че­ски из стан­дарт­но­го диапазона.

Порт откры­ва­ет­ся на всех нодах кластера.

Посмот­рим ноду из control plane кластера.

На осталь­ных нодах кла­сте­ра ситу­а­ция с пор­том 31504 будет аналогичной.

Суще­ству­ет воз­мож­ность открыть порт толь­ко на тех нодах, на кото­рых запу­ще­ны поды на кото­рые ссы­ла­ет­ся дан­ный сер­вер. Для это­го в опи­са­нии сер­ви­са необ­хо­ди­мо доба­вить пара­метр externalTrafficPolicy: Local.