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应用的实例如何发现它的伙伴,并且随需求做到横向扩展。