10.4 在Statefulset中发现伙伴节点

10.4 在Statefulset中发现伙伴节点

我们仍然需要弄清楚一件很重要的事情。集群应用中很重要的一个需求是伙伴节点彼此能发现——这样才可以找到集群中的其他成员。一个Statefulset中的成员需要很容易地找到其他的所有成员。当然它可以通过与API服务器通信来获取,但是Kubernetes的一个目标是设计功能来帮助应用完全感觉不到Kubernetes的存在。因此让应用与API服务器通信的设计是不允许的。

那如何使得一个pod可以不通过API与其他伙伴通信呢?是否有已知的广泛存在的技术来帮助你达到目的呢?那使用域名系统(DNS)如何?这依赖于你对DNS系统有多熟悉,你可能理解什么是A、CNAME或MX记录的用处是什么。DNS记录里还有其他一些不是那么知名的类型,SRV记录就是其中的一个。

介绍SRV记录

SRV记录用来指向提供指定服务的服务器的主机名和端口号。Kubernetes通过一个headless service创建SRV记录来指向pod的主机名。

可以在一个临时pod里运行DNS查询工具——dig命令,列出你的有状态pod的SRV记录。示例命令如下:

$ kubectl run -it srvlookup --image=tutum/dnsutils --rm --restart=Never -- dig SRV kubia.custom.svc.cluster.local

上面的命令运行一个名为srvlookup的一次性pod(--restart=Never),它会关联控制台(-it)并且在终止后立即删除(--rm)。这个pod依据tutum/dnsutils镜像启动单独的容器,然后运行下面的命令:

dig SRV kubia.default.svc.cluster.local

下面的代码清单显示了这个命令的输出结果。

代码清单10.8 列出你的headless Service的DNS SRV记录

....
;; ANSWER SECTION:
kubia.custom.svc.cluster.local. 30 IN   SRV     0 50 80 kubia-1.kubia.custom.svc.cluster.local.
kubia.custom.svc.cluster.local. 30 IN   SRV     0 50 80 kubia-0.kubia.custom.svc.cluster.local.

;; ADDITIONAL SECTION:
kubia-1.kubia.custom.svc.cluster.local. 30 IN A 172.18.0.2
kubia-0.kubia.custom.svc.cluster.local. 30 IN A 172.18.0.4

上面的ANSWER SECTION显示了两条指向后台headless service的SRV记录。同时如ADDITIONAL SECTION所示,每个pod都拥有独自的一条记录。

当一个pod要获取一个Statefulset里的其他pod列表时,你需要做的就是触发一次SRV DNS查询。例如,在Node.js中查询命令为:

dns.resolveSrv("kubia.custom.svc.cluster.local", callBackFunction);

可以在你的应用中使用上述命令让每个pod发现它的伙伴pod。

注意 返回的SRV记录顺序是随机的,因为它们拥有相同的优先级。所以不要期望总是看到kubia-0会排在kubia-1前面。

10.4.1 通过DNS实现伙伴间彼此发现

原始的数据存储服务还不是集群级别的,每个数据存储节点都是完全独立于其他节点的——它们彼此之间没有通信。下一步你要做的就是让它们彼此通信。

客户端通过kubia-public Service连接你的数据存储服务,并且会到达集群里随机的一个节点。集群可以存储多条数据项,但是客户端当前却不能看到所有的数据项。因为服务把请求随机地送达一个pod,所以若客户端想获取所有pod的数据,必须发送很多次请求,一直到它的请求发送到所有的pod为止。

可以通过让节点返回所有集群节点数据的方式来改进这个行为。为了达到目的,节点需要能找到它所有的伙伴节点。可以使用之前学习到的Statefulset和SRV记录来实现这个功能。

可以如下面的代码清单所示修改你的应用源码(完整的代码在本书的代码附件中,这里仅展示其中重要的一段)。

代码清单10.9 在简单应用中发现伙伴节点:kubia-pet-peers-image/app.js

const http = require('http');
const os = require('os');
const fs = require('fs');
const dns = require('dns');

const dataFile = "/var/data/kubia.txt";
const serviceName = "kubia.default.svc.cluster.local";
const port = 8080;


function fileExists(file) {
  try {
    fs.statSync(file);
    return true;
  } catch (e) {
    return false;
  }
}

function httpGet(reqOptions, callback) {
  return http.get(reqOptions, function(response) {
    var body = '';
    response.on('data', function(d) { body += d; });
    response.on('end', function() { callback(body); });
  }).on('error', function(e) {
    callback("Error: " + e.message);
  });
}

var handler = function(request, response) {
  if (request.method == 'POST') {
    var file = fs.createWriteStream(dataFile);
    file.on('open', function (fd) {
      request.pipe(file);
      response.writeHead(200);
      response.end("Data stored on pod " + os.hostname() + "\n");
    });
  } else {
    response.writeHead(200);
    if (request.url == '/data') {
      var data = fileExists(dataFile) ? fs.readFileSync(dataFile, 'utf8') : "No data posted yet";
      response.end(data);
    } else {
      response.write("You've hit " + os.hostname() + "\n");
      response.write("Data stored in the cluster:\n");
      dns.resolveSrv(serviceName, function (err, addresses) {  # 通过DNS查询SRV记录
        if (err) {
          response.end("Could not look up DNS SRV records: " + err);
          return;
        }
        var numResponses = 0;
        if (addresses.length == 0) {
          response.end("No peers discovered.");
        } else {
          addresses.forEach(function (item) { #与每个SRV记录的pod通信
            var requestOptions = {
              host: item.name,
              port: port,
              path: '/data'
            };
            httpGet(requestOptions, function (returnedData) {
              numResponses++;
              response.write("- " + item.name + ": " + returnedData + "\n");
              if (numResponses == addresses.length) {
                response.end();
              }
            });
          });
        }
      });
    }
  }
};

var www = http.createServer(handler);
www.listen(port);

图10.12展示了一个GET请求到达你的应用后的处理过程。首先收到请求的服务器会触发一次headless kubia服务的SRV记录查询,然后发送GET请求到服务背后的每一个pod(也会发送给自己,虽说没有必要,这里只是为了保证代码简单易懂),然后返回所有节点和它们的数据信息的列表。

[图片上传失败...(image-7c7606-1627739427880)]

图10.12 简单的分布式数据存储服务的操作流程

包含最新版本内容的应用对应的容器镜像链接为:docker.io/luksa/kubia-petpeers

10.4.2 更新Statefulset

现在你的Statefulset已经运行起来,那让我们看一下如何更新它的pod模板,让它使用新的镜像。同时你也会修改副本数为3。通常会使用kubectl edit命令来更新Statefulset(另一个选择是patch命令)。

$ kubectl edit statefulset kubia

上面的命令会使用默认的编辑器打开Statefulset的定义。在定义中,修改 spec.replicas 为3,修改 spec.template.spec.containers.image 属性指向新的镜像(使用luksa/kubia-pet-peers替换 luksa/kubia-pet)。然后保存文件并退出,Statefulset就会更新。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: kubia
spec:
  serviceName: kubia
  replicas: 3
  selector:
    matchLabels:
      app: kubia
  template:
    metadata:
      labels:
        app: kubia
    spec:
      containers:
      - name: kubia
        image: luksa/kubia-pet-peers
        ports:
        - name: http
          containerPort: 8080
        volumeMounts:
        - name: data
          mountPath: /var/data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      resources:
        requests:
          storage: 1Mi
      accessModes:
      - ReadWriteOnce

之前Statefulset有两个副本,现在应该可以看到一个新的名叫kubia-2的副本启动了。通过下面的代码列出pod来确认:

$ kubectl get po

新的pod实例会使用新的镜像运行,那已经存在的两个副本呢?Statefulset支持与 Deployment和DaemonSet一样的滚动升级。 Statefulset会依据新的模板重新调度启动它们。

$ watch kubectl get pod

10.4.3 尝试集群数据存储

当两个pod都启动后,即可测试你的崭新的新石器时代的数据存储是否按预期一样工作了。如下面的代码清单所示,发送一些请求到集群。

代码清单10.10 通过service往集群数据存储中写入数据

$ curl -X POST -d "The sun is shining" \localhost:8001/api/v1/namespaces/custom/services/kubia-public/proxy/$ curl -X POST -d "The weather is sweet" \localhost:8001/api/v1/namespaces/custom/services/kubia-public/proxy

现在,读取存储的数据,如下面的代码清单所示。

代码清单10.11 从数据存储中读取数据

$ curl localhost:8001/api/v1/namespaces/custom/services/kubia-public/proxy/

非常棒!当一个客户端请求到达集群中任意一个节点后,它会发现它的所有伙伴节点,然后通过它们收集数据,然后把收集到的所有数据返回给客户端。即使你扩容或缩容Statefulset,服务于客户端请求的pod都会找到所有的伙伴节点。

这个应用本身也许没太多用处,但笔者希望你觉得这是一种有趣的方式,一个多副本Statefulset应用的实例如何发现它的伙伴,并且随需求做到横向扩展。

你可能感兴趣的:(10.4 在Statefulset中发现伙伴节点)