Post

Designing Distributed Systems - Single Node Patterns

Designing Distributed Systems - Single Node Patterns

Containers and container orchestrators have introduced a great deal of flexibility in designing distributed systems. Earlier the programs were developed to be distributed over a handful of nodes, however, with the help of container orchestrators, scaling has become a quite straightforward activity, and within a couple of seconds, the program can be scaled to ten of thousands of instances. And with this, certain specific patterns and practices, which has found usage across organizations. Identifying such patterns provide a common vocabulary to discuss repetitive set of problems, and encourage reuse. Here in this blog post, we would discuss the single node patterns, where multiple containers are needed to be co-scheduled in the same node, with the understanding that the containers need to share resources between containers. Different orchestrators have different names for this type of tightly grouped container. In Kubernetes, it’s called a Pod, other orchestrators also have native support, though the term would be different.

Sidecar Pattern:

A sidecar container is attached to the application container to add functionalities that might otherwise be difficult to improve.

Sidecar Pattern: 01.Sidecar Pattern Sidecar Pattern: 01.Sidecar Pattern Sidecar Pattern: 01.Sidecar Pattern

Example 01: Adding HTTPS layer to a legacy service:

New development on legacy applications is significantly more challenging compared to adding an NGINX ingress which terminates the SSL connectivity and redirects the request to the legacy application.

Example 01: Adding HTTPS layer to a legacy service: 02.HTTPS Sidecar Example 01: Adding HTTPS layer to a legacy service: 02.HTTPS Sidecar Example 01: Adding HTTPS layer to a legacy service: 02.HTTPS Sidecar

Example 02: Dynamic configuration:

Earlier applications were written with the understanding that the configuration file would reside on the filesystem. However, with the advent of cloud computing, it’s easier to use API to update the configuration. This provides a way to do a dynamic push of configuration without the need to log into each node and then update the configuration file through some command.

1
2
docker run --pid:container:${APP_ID} -p 8080:8080 brendanburns/topz:db0fa58 \
        /server -address=0.0.0.0:8080

Example 02: Dynamic configuration: 03.Dynamic Configuration Example 02: Dynamic configuration: 03.Dynamic Configuration Example 02: Dynamic configuration: 03.Dynamic Configuration

Example 03: Building Simple PaaS:

Example 03: Building Simple Paa S: 04.Sidecar Based Paa S Example 03: Building Simple Paa S: 04.Sidecar Based Paa S Example 03: Building Simple Paa S: 04.Sidecar Based Paa S

The sidecar container should be reusable across a wide variety of applications and deployments. During the development, focus on the following areas:

  • Parametrizing the containers
  • Creating the API surface of the container: Take special care not to break existing operations
  • Documenting the operation of the container: User proper labels, expose the ports even if it’s optional, also provide comments explaining the use of ports and environment variables

Ambassadors:

Ambassador container brokers interactions between the application container and the rest of the world.

Ambassadors: 05.ambassador Pattern Ambassadors: 05.ambassador Pattern Ambassadors: 05.ambassador Pattern

Example 01: Shard a service:

Sharding splits a layer into multiple disjoint places, each hosted by a different node. Generally, the sharding logic is built into the sharding service itself, and the sharding service uses a stateless load balancer to route the request to the appropriate shard

Example 01: Shard a service: 06.generic Sharded Service Example 01: Shard a service: 06.generic Sharded Service Example 01: Shard a service: 06.generic Sharded Service

Example 02: Service brokering:

Service discovery is a way for Microservices to discover each other over a network. The process is called service discovery, and the service that performs this discovery and links microservices is called service broker.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
controlplane $ cat prod-deploy.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: prod
  name: prod
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: prod-pod
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: prod-pod
    spec:
      containers:
      - image: nginx
        name: nginx
        resources: {}
status: {}
controlplane $ cat svc-prod.yaml 
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: svc-prod
  name: svc-prod
  namespace: default
spec:
  ports:
  - name: 8082-80
    port: 8082
    protocol: TCP
    targetPort: 80
  selector:
    app: prod-pod
  type: NodePort
status:
  loadBalancer: {}
controlplane $ cat exp-deploy.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: exp
  name: exp
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: exp-pod
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: exp-pod
    spec:
      containers:
      - image: nginx
        name: nginx
        resources: {}
status: {}
controlplane $ cat svc-exp.yaml 
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: svc-exp
  name: svc-exp
  namespace: default
spec:
  ports:
  - name: 8083-80
    port: 8083
    protocol: TCP
    targetPort: 80
  selector:
    app: exp-pod
  type: NodePort
status:
  loadBalancer: {}
controlplane $ cat nginx.conf 

worker_processes 5;

events{
  worker_connections 1024;
}

http {
  upstream backend {
    ip_hash;
    server svc-prod:8082 weight=9;
    server svc-exp:8083;
  }

  server {
    listen localhost:80;
    location / {
      proxy_pass http://backend;
    }
  }
}
controlplane $ cat splitter.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: splitter
  name: splitter
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: splitter-pod
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: splitter-pod
    spec:
      containers:
      - image: nginx
        name: nginx
        volumeMounts:
        - name: config-volume
          mountPath: /etc/nginx/
      volumes:
      - name: config-volume
        configMap:
          name: config-volume 
status: {}
controlplane $ 

Adapters:

Adapter container transforms the output of the application container so that it conforms to the standard other containers are expecting it to be.

Adapters: 08.Adapter Pattern Adapters: 08.Adapter Pattern Adapters: 08.Adapter Pattern

Example 01: Monitoring:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
root@log-example-6dd84f84dd-bhssj:/# nginx -V
nginx version: nginx/1.25.2
built by gcc 12.2.0 (Debian 12.2.0-14) 
built with OpenSSL 3.0.9 30 May 2023
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-http_v3_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-g -O2 -ffile-prefix-map=/data/builder/debuild/nginx-1.25.2/debian/debuild-base/nginx-1.25.2=. -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie'
root@log-example-6dd84f84dd-bhssj:/# 

root@log-example-6dd84f84dd-sp86t:/# cat /etc/nginx/nginx.conf 

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}
root@log-example-6dd84f84dd-sp86t:/# cat /etc/nginx/conf.d/default.conf 
server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    location /status {
        stub_status;
    }

    location /metrics {
        stub_status;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}

root@log-example-6dd84f84dd-sp86t:/# 

controlplane $ ls -l
total 8
lrwxrwxrwx 1 root root   1 Aug  8 10:39 filesystem -> /
-rw-r--r-- 1 root root 289 Aug 18 14:54 log-svc.yaml
-rw-r--r-- 1 root root 429 Aug 18 14:50 log_example.yaml
controlplane $ cat log-svc.yaml 
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: log-svc
  name: log-svc
  namespace: default
spec:
  ports:
  - name: 80-80
    port: 8090
    protocol: TCP
    targetPort: 80
  - name: 91-91
    port: 8091
    protocol: TCP
    targetPort: 9113
  selector:
    app: log-example
  type: NodePort
status:
  loadBalancer: {}
controlplane $ cat log_example.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: log-example
  name: log-example
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: log-example
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: log-example
    spec:
      containers:
      - image: nginx
        name: nginx
        resources: {}
        ports:
        - containerPort: 80
        volumeMounts:
          - name: nginx-configure
            mountPath: /etc/nginx/nginx.conf
            subPath: nginx.conf
          - name: nginx-configure
            mountPath: /etc/nginx/conf.d/default.conf
            subPath: default.conf
      - image: nginx/nginx-prometheus-exporter
        name: nginx-prometheus-exporter
        args:
          - '-nginx.scrape-uri=http://localhost:80/status'
        ports:
        - containerPort: 9113
      volumes:
        - name: nginx-configure
          configMap:
            name: nginx-conf
status: {}
controlplane $ 
Example 02: Logging:

Different application container might log information in different format. The adapter container is useful in transforming them into a structured pattern that can be consumed by log aggregator. Fluentd is one such popular logging agents available.

References:

  1. Designing Distributed Systems: Patterns & Paradigms for Scalable, Reliable Services
This post is licensed under CC BY 4.0 by the author.