20 Service the Corresponding Route in Microservice Architecture

20 Service The Corresponding Route in Microservice Architecture #

Hello, I’m Chrono.

In the previous lessons, we learned about two API objects: Deployment and DaemonSet. They are both used to deploy applications in different strategies. Deployment creates multiple instances, while DaemonSet creates an instance for each node.

These two API objects can deploy applications in various forms. In the era of cloud-native, microservices undoubtedly have become the mainstream form of applications. In order to better support microservices and application architectures like service mesh, Kubernetes has specifically defined a new object called Service. It serves as the load balancing mechanism within the cluster, solving the critical issue of service discovery.

Today, we will take a look at what Service is, how to define Service using YAML, and how to make the most of Service in Kubernetes.

Why Do We Need Service? #

With Deployment and DaemonSet, our work of deploying applications in the cluster has become much easier. With the powerful automation capabilities of Kubernetes, we can increase the frequency of updating and releasing applications from monthly or weekly to daily or hourly, improving the quality of service.

However, along with the rapid iteration of application versions, another problem gradually arises, which is “service discovery”.

In a Kubernetes cluster, the lifecycle of a Pod is relatively short. Although Deployment and DaemonSet can maintain the overall number of Pods, there will inevitably be situations where Pods are destroyed and rebuilt, leading to dynamic changes in the Pod set.

This “dynamic stability” is fatal for the popular microservices architecture. Just imagine, if the IP addresses of the backend Pods keep changing, how can the clients access them? If this problem is not handled properly, Deployment and DaemonSet would be useless no matter how well they manage the Pods.

Actually, this problem is not difficult to solve. The industry has long had solutions to deal with such “unstable” backend services, such as “load balancing”, with typical applications like LVS and Nginx. They introduce an “intermediate layer” between the frontend and the backend, shielding the backend changes and providing a stable service to the frontend.

However, LVS and Nginx are not native cloud technologies, so Kubernetes has followed this idea and defined a new API object called “Service”.

So, it is conceivable that the working principle of Service is similar to LVS and Nginx. Kubernetes assigns it a static IP address, and it automatically manages and maintains the dynamically changing Pod set behind it. When clients access the Service, it forwards the traffic to one of the backend Pods based on some strategy.

The following diagram from the Kubernetes official documentation illustrates the working principle of Service clearly:

Image

As you can see, Service uses iptables technology here. The kube-proxy component on each node automatically maintains iptables rules. Clients no longer need to care about the specific addresses of the Pods. As long as they access the fixed IP address of the Service, the Service will forward the requests to the multiple Pods it manages according to the iptables rules. This is a typical load balancing architecture.

However, Service is not limited to using iptables for load balancing. It also has two other implementation technologies: the less efficient userspace and the more efficient ipvs. But these are all low-level details that we don’t need to deliberately focus on.

How to Describe Services Using YAML #

Now that we understand the basic workings of a Service, let’s see how to write a YAML description file for it.

As usual, we can use the kubectl api-resources command to view its basic information. We can see that its abbreviation is svc and the apiVersion is v1. Note that this indicates that, like Pods, Services are core objects in Kubernetes and are not associated with business applications like Jobs and Deployments.

Now, I believe you can easily write the YAML file header for the Service:

apiVersion: v1
kind: Service
metadata:
  name: xxx-svc

Similarly, can Kubernetes automatically create a YAML template for our Service? Do we use the kubectl create command?

Here, Kubernetes behaves inconsistently. Although it can create the YAML template automatically, it does not use the command kubectl create. Instead, it uses another command called kubectl expose. Perhaps Kubernetes believes that “expose” better expresses the idea of “exposing” the service address.

Because Pods provide services in Kubernetes and Pods can be deployed using Deployment/DaemonSet objects, kubectl expose supports creating services from multiple objects: Pods, Deployments, and DaemonSets.

When using the kubectl expose command, you also need to specify the mapping port and container port using the --port and --target-port parameters respectively. The IP address for the Service itself and the IP address for the backend Pods can be generated automatically. The usage is similar to the -p command-line parameter in Docker, but a little more cumbersome.

For example, if we want to generate a Service for the ngx-dep object from the previous lesson, the command would be written as follows:

export out="--dry-run=client -o yaml"
kubectl expose deploy ngx-dep --port=80 --target-port=80 $out

The generated Service YAML would look something like this:

apiVersion: v1
kind: Service
metadata:
  name: ngx-svc

spec:
  selector:
    app: ngx-dep

  ports:
  - port: 80
    targetPort: 80
    protocol: TCP

You will notice that the definition of the Service is very simple, with only two key fields in the “spec” section: selector and ports.

The selector has the same purpose as in Deployment/DaemonSet. It is used to filter out the Pods to be proxied. Since we specified the Deployment to be proxied, Kubernetes automatically filled in the label for ngx-dep, selecting all the Pods deployed by this Deployment object.

From this, you can see that Kubernetes’ label mechanism, although simple, is very powerful and effective, easily associating with Pods from the Deployment.

The ports field is self-explanatory. The three fields inside represent the external port, internal port, and protocol used. In this case, both the internal and external ports are set to 80, and the protocol is TCP.

Of course, you can change the ports field to other ports like “8080”, so that the external service sees the port provided by the Service rather than the actual service port of the Pod.

To help you see the relationship between the Service and the Pods it references, I have drawn these two YAML objects in the diagram below. Pay special attention to the selector, targetPort, and the association with Pods:

Image

How to use Service in Kubernetes #

Before creating a Service object using YAML, let’s make some modifications to the Deployment discussed in lesson 18 to observe the effect of Service.

First, we create a ConfigMap that defines an Nginx configuration snippet. It will output basic information about the server’s address, hostname, and requested URI:

apiVersion: v1
kind: ConfigMap
metadata:
  name: ngx-conf

data:
  default.conf: |
    server {
      listen 80;
      location / {
        default_type text/plain;
        return 200
          'srv : $server_addr:$server_port\nhost: $hostname\nuri : $request_method $host $request_uri\ndate: $time_iso8601\n';
      }
    }    

Then, in the Deployment’s " template.volumes “, we define a volume and use " volumeMounts " to load the configuration file into the Nginx container:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ngx-dep

spec:
  replicas: 2
  selector:
    matchLabels:
      app: ngx-dep

  template:
    metadata:
      labels:
        app: ngx-dep
    spec:
      volumes:
      - name: ngx-conf-vol
        configMap:
          name: ngx-conf

      containers:
      - image: nginx:alpine
        name: nginx
        ports:
        - containerPort: 80

        volumeMounts:
        - mountPath: /etc/nginx/conf.d
          name: ngx-conf-vol

These two modifications make use of the knowledge discussed in lesson 14. If you have not mastered it yet, you can go back and review it.

After deploying this Deployment, we can create the Service object using kubectl apply command:

kubectl apply -f svc.yml

Once created, you can check the status of the Service using the kubectl get command:

图片

You can see that Kubernetes automatically assigns an IP address of “10.96.240.115” to the Service object. This IP address range is separate from the Pod address range (e.g., the “10.10.xx.xx” range mentioned in lesson 17). Furthermore, the IP address of the Service object has a specific characteristic - it is a “virtual address” that does not exist physically and can only be used for traffic forwarding.

To determine which back-end Pod the Service is proxying, you can use the kubectl describe command:

kubectl describe svc ngx-svc

图片

In the screenshot, it shows that the Service object manages two endpoints, “10.10.0.232:80” and “10.10.1.86:80”. By comparing this information with the definition of the Service and Deployment, we can confirm that these two IP addresses are indeed the actual addresses of the Nginx Pods.

Now, how do we test the load balancing effect of the Service?

Since the IP addresses of the Service and Pods are internal to the Kubernetes cluster, we need to use kubectl exec to enter the Pod (or ssh into the cluster node) and then use tools like curl to access the Service:

kubectl exec -it ngx-dep-6796688696-r2j6t -- sh

图片

Within the Pod, when we use curl to access the IP address of the Service, we will see that it forwards the data to the back-end Pod. The output will indicate which Pod responded to the request, confirming that the Service has indeed accomplished its load balancing task.

Let’s try deleting a Pod and see if the Service updates the information of the back-end Pods, achieving automated service discovery:

kubectl delete pod ngx-dep-6796688696-r2j6t

图片

Since the Pods are managed by the Deployment object and will be automatically recreated after deletion, the Service will continuously monitor the changes in Pod status through the controller-manager. Consequently, it will immediately update the IP addresses it proxies. In the screenshot, you can see that one IP address, “10.10.1.86”, disappears and is replaced with the new address “10.10.1.87”, which belongs to the newly created Pod.

You can also try using “ping” to test the IP address of the Service:

图片

You will find that ping is not successful. That’s because the IP address of the Service is “virtual” and used solely for traffic forwarding. Therefore, ping cannot receive response packets, resulting in failure.

How to Use Service with Domain Names #

Here we have covered the basic usage of Service, but there are some advanced features worth knowing.

Let’s first take a look at DNS domain names.

The IP address of a Service object is static and remains stable, which is important in microservices. However, it is not very convenient to use IP addresses in numeric form. This is where Kubernetes’ DNS plugin comes in handy. It can create easy-to-write and easy-to-remember domain names for Services, making them easier to use.

Before using DNS domain names, we need to understand a new concept: Namespace.

Note that this is completely different from the Linux namespace technology we mentioned in the [Lesson 2] for resource isolation. Kubernetes simply borrows the term, but the goal is similar, which is to achieve isolation and grouping of API objects in the cluster.

The abbreviation for namespace is “ns”. You can use the command kubectl get ns to see all the namespaces in the current cluster, that is, the groups to which API objects belong:

kubectl get ns

Image

Kubernetes has a default namespace called “default”. If not explicitly specified, API objects will be in this “default” namespace. Other namespaces have their own purposes. For example, the “kube-system” namespace contains the core components’ Pods such as apiserver and etcd.

Because DNS is a hierarchical structure, in order to avoid too many domain names causing conflicts, Kubernetes includes the namespace as part of the domain name, reducing the possibility of duplication.

The fully qualified domain name (FQDN) of a Service object is “object.namespace.svc.cluster.local”. However, in many cases, the latter part can be omitted, and you can directly write “object.namespace” or even “object”, defaulting to the namespace where the object is located (e.g., default).

Now let’s experiment with the usage of DNS domain names. Again, let’s use kubectl exec to enter the Pod, and then use curl to access the domains such as ngx-svc, ngx-svc.default, etc.:

Image

As you can see, we no longer need to care about the IP address of the Service object. We only need to know its name and can use DNS to access the backend service.

Compared to Docker, this is undoubtedly a huge improvement, and compared to other microservices frameworks (such as Dubbo, Spring Cloud), because the service discovery mechanism is integrated into the infrastructure, it will also make application development more convenient.

(By the way, Kubernetes also assigns a domain name to each Pod in the form of “IP-address.namespace.pod.cluster.local”. However, you need to replace the . in the IP address with -. For example, for the IP address 10.10.1.87, its corresponding domain name is 10-10-1-87.default.pod.)

How to expose a service externally #

As a load balancing technology, a Service in Kubernetes not only manages services within the Kubernetes cluster but also assumes the responsibility of exposing services externally to the cluster.

The Service object has a key field called “type”, which indicates the type of load balancing for the Service. In the previous examples, we saw the usage of load balancing for Pods within the cluster, so the value of this field was the default “ClusterIP”. A Service with ClusterIP provides a static IP address that can only be accessed within the cluster.

Apart from “ClusterIP”, the Service also supports three other types: “ExternalName”, “LoadBalancer”, and “NodePort”. However, the first two types are generally provided by cloud service providers, which are not necessary for our experimental environment. Therefore, we will focus on the “NodePort” type.

If we add the parameter --type=NodePort when using the kubectl expose command or add the field type: NodePort in the YAML file, the Service not only performs load balancing for the backend Pods but also creates a unique port on each node within the cluster to provide the service externally. This is why it is called “NodePort”.

Let’s modify the YAML file for the Service and add the “type” field:

apiVersion: v1
...
spec:
  ...
  type: NodePort

Then create the object and check its status:

kubectl get svc

Image

You will see that the “TYPE” has become “NodePort”, and in the “PORT” column, there is a different port number. Besides the “80” port used internally within the cluster, there is also a “30651” port, which is the dedicated mapped port created by Kubernetes on each node for the Service.

Since this port belongs to the node and can be accessed directly from the outside, we can now access the Service and the backend services it proxies without having to log into the cluster node or enter the Pod. We can simply use the IP address of any node externally. For example, if my server’s IP address is “192.168.10.208”, I can access the two nodes of the Kubernetes cluster, “192.168.10.210” and “192.168.10.220”, using curl, and receive a response from the Nginx Pod:

Image

I’ve created a diagram to illustrate the mapping between NodePort and the Service and Deployment. It should help you better understand how it works:

Image

Now that you’ve learned this, you may find that the NodePort type of Service is very convenient.

However, it also has some disadvantages.

The first drawback is that the number of available ports is limited. To avoid port conflicts, Kubernetes only randomly assigns ports within the range of “30000~32767”. There are only over 2000 ports, and they are not even standard port numbers, which is insufficient for systems with a large number of business applications.

The second drawback is that it opens a port on each node and uses kube-proxy to route to the actual backend Service. For large clusters with many compute nodes, this incurs some network communication overhead, which is not particularly cost-effective.

The third drawback is that it requires exposing the IP addresses of the nodes to the outside world, which is often not feasible. To ensure security, an additional reverse proxy is also needed outside the cluster, increasing the complexity of the solution.

Despite these drawbacks, NodePort is still a simple and viable way for Kubernetes to provide services externally. Until better alternatives emerge, we can only use it.

Summary #

Alright, today we learned about the Service object, which implements load balancing and service discovery technologies. It is a solution provided by Kubernetes to address modern popular application architectures such as microservices and service mesh.

Let me summarize today’s main points:

  1. The lifecycle of Pods is brief, as they are constantly created and destroyed. Therefore, a Service is needed to implement load balancing. Kubernetes assigns a fixed IP address to the Service, which can shield the changes in backend Pods.
  2. The Service object uses the same “selector” field as Deployment and DaemonSet, enabling a loose coupling relationship to select the backend Pods to be proxied.
  3. With the DNS plugin, we can access Services using domain names, which is more convenient than using static IP addresses.
  4. Namespaces are a way for Kubernetes to isolate objects and achieve logical grouping of objects. The Service’s domain name includes the namespace scope.
  5. The default type of a Service is “ClusterIP” which can only be accessed within the cluster. If it is changed to “NodePort”, a random port number will be opened on the nodes, allowing external access to the internal services.

Homework #

Finally, it’s time for homework. I’ll leave you with two questions to think about:

  1. Why are Service IP addresses static and virtual? What is the purpose and benefit of this?
  2. Do you know about load balancing technology? What are the different algorithms used, and which one does Service use?

Feel free to share your thoughts in the comments section. Output promotes input. Up until now, you have completed 2/3 of the column’s learning. Look back at what we have learned together and consider what you have gained.

If you find this helpful, feel free to share it with your friends around you and let’s continue in the next class.