Kubernetes 高级平台开发(四)
九、路由和转换
前面的章节介绍了数据库、数据湖和数据仓库的构建,这些都是数据平台的基础元素,全部来自 Kubernetes,展示了一个健壮的分布式服务平台。本章着重于数据的收集、提取、移动和处理,完善了任何以数据为中心的应用所需的大部分功能。从一个不同的系统高效地提取、转换和加载数据到另一个系统的能力对于利用消费者和工业物联网、社交媒体和许多组织中发生的数字转换的数据爆炸式增长至关重要。快速构建移动、转换和处理数据的路径的能力对于利用不断进步的数据驱动趋势至关重要,例如基于机器学习的人工智能,这些技术特别渴望大量处理的数据。一个有效的数据平台提供了跨数据管理系统提取、转换和加载数据所需的所有通用机制,并提供了支持专门处理和定制业务逻辑的应用层。
本章使用 ETL/ELT(提取、转换、加载)和 FaaS(功能即服务,也称为无服务器)功能扩展了基于 Kubernetes 的数据平台,该平台是在前面章节的基础上构建的。专注于 ETL 的技巧和技术已经成熟很多年了。数据工程师现在可以将 ETL 与无服务器平台相结合,快速开发、集成并直接部署到数据流水线中。
ETL 和数据处理
本章介绍了 Kubernetes 中的两项新技术:Apache NiFi 和 OpenFaaS,展示了一种提取、加载、转换和处理数据的纯开源方法,不需要特定领域的语言和复杂的配置文件。Apache NiFi 提供了数百个预构建的数据“处理器”,用于从几乎任何标准网络协议或 API 实现中提取和加载数据,并能够将其转换为几乎任何所需的形式。OpenFaaS 是对无服务器/FaaS(作为服务的功能)概念的厂商中立的方法;在这种情况下,FaaS 通过用任何语言开发的定制的、高度集中的代码,允许 ETL 流水线的无限可扩展性。
图 9-1 描述了贯穿本章的 ETL、数据处理和可视化演示。最终目标是可视化一段时间内在一组特定的 Twitter 消息上表达的情绪范围。下面几节安装 OpenFaaS 并部署一个预打包的情绪分析功能。后面的部分介绍了 Apache NiFi,并配置了从 Twitter 到 Kafka 以及从 Kafka 到 OpenFaaS 情绪分析函数的数据路由和转换的有向图,最后将结果记录在 Elasticsearch 中,供 Jupyter 笔记本进行分析。虽然这些技术都不需要 Kubernetes,但本演示旨在阐明统一控制面板、网络、监控的优势及其基于容器的应用平台功能的可扩展性。
图 9-1
NiFi 和 OpenFaaS 演示架构
本书中的一些技术,即数据库,在抽象的基础设施上运行时可能会牺牲一定程度的性能;然而,一些应用会发现这是一种可接受的折衷,可以减少管理许多具有完全不同的依赖关系的系统时所产生的技术负担。根据组织的规模或项目的预算问题,将基础设施专业知识应用于每项企业级技术(如 Apache NiFi、Kafka 和 Elasticsearch)可能不可行。这本书希望通过利用 Kubernetes 在几乎任何项目都可能利用的规模上展示这些技术,从概念的启动证明到网络规模的社交网络。
发展环境
以下练习继续利用第六章中提到的廉价 Hetzner 集群,包括一个用于 Kubernetes 主节点的 CX21 (2 个 vCPU/8G RAM/40G SSD)和四个用于 worker 节点的 CX41 (4 个 vCPU/16G RAM/160G SSD)实例,但任何等效的基础架构都可以容纳。此外,本章还利用了第 3 、 5 和 6 章中安装的应用和集群配置;参见第七章中的表 7-1 。本章要求在第三章中配置入口、证书管理器、存储和监控;第五章中的命名空间、动物园管理员和 Kafka;以及第六章的 Elasticsearch、Kibana、Keycloak、JupyterHub。
本章在项目文件夹cluster-apk8s-dev5下组织名为dev5的集群的配置清单。
无服务器
无服务器的概念,也称为 FaaS (functions as a service),已经不断成熟,满足了小单元功能代码的简化部署需求。单一应用、微服务和(无服务器)功能之间的区别在于实现、操作基础设施和更广泛架构的上下文。例如,许多微服务和基于功能的架构仍然依赖于单片数据库。术语“无服务器”意味着开发人员应该很少或根本不需要关心服务器端的实现。无服务器或 FaaS (functions as a service)旨在抽象出集成、部署和运行时操作的几乎所有方面,将功能性业务逻辑作为开发人员的唯一职责。
云供应商推销无服务器技术的核心吸引力,即允许开发人员专注于业务逻辑、抽象和管理基础设施、操作系统、运行时和应用层的能力。主要云厂商的产品包括亚马逊的 AWS Lambda、 1 微软的 Azure 函数、 2 谷歌云函数、 3 和 IBM 云函数。 4 这些产品可以显著缩短上市时间,减少许多组织的技术债务,尽管是以锁定供应商为代价。然而,已经投资 Kubernetes 的组织可以利用越来越多的开源、厂商中立的无服务器平台,如 Apache OpenWhisk、 5 Kubeless、 6 和 OpenFaaS。7Knative8对于那些寻求开发定制无服务器平台的人来说是一个流行的选择。
本章展示了 OpenFaaS 在一个示例 ETL 应用中的使用,以及无服务器/功能即服务如何对任何数据平台进行出色的补充。
OpenFaaS
OpenFaaS 是一个稳定的、维护良好的、无服务器的应用平台,被越来越多的组织所使用。OpenFaaS 几乎可以在任何地方运行,但可以与 Kubernetes 很好地集成,支持 Go、Java、Python、PHP、Rust、Perl、C#和 Ruby 等语言,以及包括 Express.js、Django 和 ASP.NET 核心在内的应用平台。OpenFaaS 支持定制容器来包装强大的二进制文件,如 FFmpeg 9 和 ImageMagick。 10
管理容器化的工作负载是 Kubernetes 的核心能力;然而,像 OpenFaaS 这样的平台提供了一个正式的工具和操作框架,用于开发、编目、集成、部署、扩展和监控用函数表示的工作负载。
无服务器/功能即服务是 ETL 和数据处理流水线的天然选择,这一点将在本章的后面介绍。
安装 openafs
用 OpenFaaS 库更新 Helm:
$ helm repo add openfaas \
https://openfaas.github.io/faas-netes/
$ helm repo update
接下来,使用 Helm 将 OpenFaaS 网关安装到带有参数--namespace data的data名称空间中。OpenFaaS 网关可以设置函数在另一个名称空间中运行,但是对于这个例子,通过设置functionNamespace=data来使用data名称空间。OpenFaaS 能够利用 Kubernetes 节点端口和负载均衡器; 11 然而,这个例子通过设置exposeServices=false和ingress.enabled=true使用 Ingress 来公开部署的函数。最后,设置选项generateBasicAuth=true以使用基本认证保护入口暴露网关用户界面:
$ helm upgrade apk8s-data-openfaas –install \
openfaas/openfaas \
--namespace data \
--set functionNamespace=data \
--set exposeServices=false \
--set ingress.enabled=true \
--set generateBasicAuth=true
创建目录cluster-apk8s-dev5/003-data/120-openfaas。在新的120-openfaas目录中,从清单 9-1 中创建一个名为50-ingress.yml的文件。
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: faas
namespace: data
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
spec:
rules:
- host: faas.data.dev5.apk8s.dev
http:
paths:
- backend:
serviceName: gateway
servicePort: 8080
path: /
tls:
- hosts:
- faas.data.dev5.apk8s.dev
secretName: faas-data-production-tls
Listing 9-1OpenFaaS Ingress
应用 OpenFaaS 入口配置:
$ kubectl apply -f 50-ingress.yml
成功应用 Ingress 后,使用以下命令检索由 OpenFaaS Helm 安装生成的基本身份验证凭据:
$ echo $(kubectl -n data get secret basic-auth -o jsonpath="{.data.basic-auth-password}" | base64 --decode)
前面的命令返回用于登录 OpenFaaS UI 门户的基本 Auth 密码;用户名是admin。在本例中,浏览至入口 URL(见图 9-2 ):
https://faas.data.dev5.apk8s.dev 。
图 9-2
openfans 用户界面门户
OpenFaaS UI 门户是一个方便的基于 web 的可视化界面,用于安装和管理功能。然而,CLI 实用程序 faas-cli 通常是与 OpenFaaS 交互的首选方法,能够支持开发、测试、部署、管理和自动化功能即服务的所有方面。在本地工作站上安装 OpenFaaS CLI:
$ curl -sLSf https://cli.openfaas.com | sudo sh
执行 OpenFaaS CLI 获取顶级命令列表:
$ faas-cli
虽然本书推荐将 CLI 实用程序用于常规用途,但以下部分使用基于 web 的界面作为安装 OpenFaaS 功能并与之交互的简单直观演示。
安装情感分析
情感分析, 12 又称为情感识别 13 或观点挖掘, 14 是自然语言处理(NLP)的一种形式。NLP 将语言学、人工智能和信息工程应用于自然(人类)语言。本节部署一个预建的 OpenFaaS 函数容器, 15 实现 Python 库 TextBlob 16 对原始文本的一个或多个句子进行情感分析。本章稍后将使用部署的情感分析功能来分析实时的 Twitter 消息流,这些消息都标有与新冠肺炎相关的关键词。
浏览到上一节设置的 OpenFaaS UI 门户( https://faas.data.dev5.apk8s.dev ),点击屏幕中央的DEPLOY NEW FUNCTION按钮。接下来,使用Search for Function功能,搜索如图 9-3 所示的术语SentimentAnalysis,选择功能SentimentAnalysis,点击对话框左下方的DEPLOY。
图 9-3
openfans 部署预建的情绪分析功能
部署 OpenFaaS 情绪分析功能后,从左侧导航中选择它。UI 门户在页面的上半部分显示功能的状态、副本、调用计数、映像和 URL(参见图 9-4 )。URL 是公开的端点。OpenFaaS 网关默认不保护函数端点; 17 安全是功能本身的责任。OpenFaaS 文档通过使用 Kubernetes Secrets 实现 HMAC 安全性来指导开发自定义函数身份验证。 18
OpenFaaS UI 门户提供了一个方便的 web 表单,用于测试图 9-4 中显示的调用功能部分下部署的功能。或者,使用上一节中安装的faas-cli实用程序调用情绪分析功能:
图 9-4
使用 OpenFaaS UI 门户测试情感分析功能
$ echo "Kubernetes is easy" | faas-cli invoke \
sentimentanalysis -g https://faas.data.dev5.apk8s.dev/
最后,用 cURL 测试新函数的公共访问:
$ curl -X POST -d "People are kind" \
https://faas.data.dev5.apk8s.dev/function/sentimentanalysis
示例输出:
{"polarity": 0.6, "sentence_count": 1, "subjectivity": 0.9}
OpenFaaS 情绪分析功能是一个很好的例子,它是由 OpenFaaS 在 Kubernetes 上部署和管理的一个集中的、独立的处理逻辑。OpenFaaS 文档包含一组编写良好的关于构建、测试和实现函数的教程。 19 函数是持续扩展本书中开发的数据平台的一个伟大方式。下一节将介绍用于 ETL 类型操作的 Apache NiFi,并将情感分析功能的使用作为示例数据处理流程的一部分(参见图 9-5 )。
抽取、转换、加载至目的端(extract-transform-load 的缩写)
ETL(提取、转换、加载)的实践可以追溯到 20 世纪 70 年代。从一个来源提取数据并转换为另一个来源使用的需求是一个永恒的问题,今天有大量的现有技术来解决这个问题。Pentaho、 21 Talend、 22 CloverETL、 23 和 JasperETL 24 是少数几个开源、社区驱动选项有限的商业产品。然而,ETL 是一个如此普遍的问题,以至于新的方法和通用的解决方案,如开源(厂商中立)Apache NiFi,因为易于使用和简单、直观的数据收集、路由和转换方法而越来越受欢迎。
Apache 尼菲
Apache NiFi 是本书中描述的以数据为中心的平台的数据接收前端选择。" Apache NiFi 支持强大且可伸缩的数据路由、转换和系统中介逻辑的有向图." 25
NiFi 附带了近 300 个独特的数据处理器,可用于收集、转换、处理和建模来自 Twitter、SMTP、HDFS、Redis、UDP、HBase 和 HTTP API 端点等不同来源的数据。 26
在本书出版时,在 Kubernetes 中运行 NiFi 的官方文档和支持很少。然而,NiFi 维护者意识到对一流 Kubernetes 支持需求的快速增长,读者应该期待在未来几年中对这一努力的重大贡献。
以下部分在 Kubernetes 中安装了一个多节点 Apache NiFi 集群,并演示了从 Twitter 中提取数据的流程,包括转换、处理和利用 OpenFaaS、Kafka 和 Elasticsearch,如图 9-5 所示。
图 9-5
Apache NiFi 流概述
安装 Apache NiFi
本节安装 Apache NiFi 和三个 Kubernetes 资源,包括一个 Headless 服务、StatefulSet 和 Ingress。回顾本章前面的“开发环境”部分,了解需求,包括入口 Nginx、Ceph 存储、证书管理器和 Apache Zookeeper。
虽然这本书有时会避免使用 Helm 安装,而是通过手工制作的清单来更详细地描述概念,但读者也应该考虑一下由 Cetic 编写的 Apache NiFi Helm 图表。 27
创建目录cluster-apk8s-dev5/003-data/060-nifi。在新的060-nifi目录中,从清单 9-2 中创建一个名为10-service-headless.yml的文件。下面定义的 StatefulSet 调用运行 apache/nifi:1.9.2 容器的 Pod nifi的两个副本。集群中的每个 NiFi 实例在引导时都需要一些定制配置。command:部分不允许运行标准的启动脚本,而是调用脚本中的 Bash 和 pipes 来根据 Kubernetes 分配给 Pod 的主机名定制一些属性。
apiVersion: v1
kind: Service
metadata:
name: nifi
namespace: data
labels:
app: nifi
annotations:
service.alpha.kubernetes.io/tolerate-unready-endpoints: "true"
spec:
type: ClusterIP
clusterIP: None
selector:
app: nifi
ports:
- port: 8080
name: http
- port: 6007
name: cluster
Listing 9-2NiFi Headless Service
应用 NiFi Headless 服务配置:
$ kubectl apply -f 10-service-headless.yml
接下来,在清单 9-3 中的文件40-statefulset.yml中为 NiFi 创建一个 StatefulSet 配置。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nifi
namespace: data
labels:
app: nifi
spec:
replicas: 2
revisionHistoryLimit: 1
selector:
matchLabels:
app: nifi
serviceName: nifi
template:
metadata:
labels:
app: nifi
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: "app"
operator: In
values:
- "nifi"
topologyKey: "kubernetes.io/hostname"
containers:
- name: nifi
imagePullPolicy: IfNotPresent
image: apache/nifi:1.9.2
command:
- bash
- -ce
- |
FQDN=$(hostname -f)
PROP_FILE=${NIFI_HOME}/conf/nifi.properties
p_repl () {
echo "setting ${1}=${2}"
sed -i -e "s|^$1=.*$|$1=$2|" ${PROP_FILE}
}
p_repl nifi.remote.input.host ${FQDN}
p_repl nifi.cluster.is.node true
p_repl nifi.cluster.node.protocol.port 6007
p_repl nifi.cluster.node.address ${FQDN}
p_repl nifi.cluster.protocol.is.secure false
p_repl nifi.security.user.authorizer managed-authorizer
p_repl nifi.web.http.host ${FQDN}
p_repl nifi.web.http.port 8080
p_repl nifi.zookeeper.connect.string ${NIFI_ZOOKEEPER_CONNECT_STRING}
p_repl nifi.cluster.flow.election.max.wait.time "1 mins"
tail -F "${NIFI_HOME}/logs/nifi-app.log" & exec bin/nifi.sh run
env:
- name: NIFI_ZOOKEEPER_CONNECT_STRING
value: "zookeeper-headless:2181"
ports:
- containerPort: 8080
name: http
protocol: TCP
- containerPort: 6007
name: cluster
protocol: TCP
Listing 9-3NiFi StatefulSet
应用 NiFi StatefulSet 配置:
$ kubectl apply -f 40-statefulset.yml
最后,在清单 9-4 中的文件50-ingress.yml中为 NiFi 创建一个入口配置。Apache NiFi 支持认证; 28 然而,这需要它在 SSL 模式下运行,并且需要额外的组件配置来管理证书和入口。为了保持演示的简洁,入口配置使用存储在 Kubernetes Secret sysop-basic-auth中的基本 Auth 凭证来保护 NiFi。
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: nifi
namespace: data
annotations:
cert-manager.io/cluster-issuer: letsencrypt-production
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: sysop-basic-auth
nginx.ingress.kubernetes.io/auth-realm: "Authentication Required"
spec:
rules:
- host: nifi.data.dev3.apk8s.dev
http:
paths:
- backend:
serviceName: nifi
servicePort: 8080
path: /
tls:
- hosts:
- nifi.data.dev3.apk8s.dev
secretName: data-production-tls
Listing 9-4NiFi Ingress
应用 NiFi 入口配置:
$ kubectl apply -f 50-ingress.yml
浏览至 https://nifi.data.dev3.apk8s.dev/nifi ,打开用户界面左上角的全局菜单,选择项目集群,验证新的 NiFi 集群已启动并正在运行。查看运行节点列表,如图 9-6 所示。
图 9-6
Apache NiFi 集群状态
下一节展示了一个 ETL/ELT 数据流水线的例子,它使用了本章前面安装的 OpenFaaS,以及前面章节介绍的 Apache Kafka、Elasticsearch 和 JupyterLab。
ETL 数据流水线示例
ETL(提取、转换、加载)操作的传统示例可能会展示在大数据系统中收集和存储的数据的提取,如 HDFS 或任何种类的商业和开源数据湖以及传统或现代企业数据管理系统。虽然下面的例子演示了从 Twitter 中提取数据,但是应该很容易理解 NiFi 的各种预构建数据处理器的灵活性,并将这些基本功能应用于几乎所有 ETL 挑战。
这个例子需要 OpenFaaS 和本章前面安装的 SentimentAnalysis 函数;ApacheKafka,配置在第五章;以及在第六章中介绍的 Elasticsearch、Kibana、Keycloak 和 JupyterHub。
以下示例 ETL 数据流水线使用 NiFi Twitter 处理器从 Twitter 中提取消息,并将它们发布到 Apache Kafka 主题。随后,Kafka 处理器使用主题中的消息,准备并将其发送到 OpenFaaS SentimentAnalysis 函数,最后将结果存储在 Elasticsearch 索引中,以便在 JupyterLab 环境中进行分析。这个例子展示了 Kubernetes 如何在一个分布式、高可用、受监控和统一的控制面板中轻松管理所有必需的工作负载(见图 9-1 和 9-5 )。
NiFi 模板
Apache NiFi 为希望扩展其功能的用户、管理员和开发人员提供了详细的文档。 29 因此,为了快速演示其用法,以及利用本书中配置和安装的组件,可以在 https://github.com/apk8s/nifi-demo 找到一个预构建的模板。
克隆apk8s/nifi-demo存储库:
git clone git@github.com:apk8s/nifi-demo.git
在 https://nifi.data.dev3.apk8s.dev/nifi 浏览到正在运行的 NiFi 集群后,点击屏幕左侧操作面板中显示的模板上传按钮(见图 9-7 )。当出现提示时,上传在apk8s/nifi-demo库的templates目录中找到的文件Twitter_Sentiment.xml。
图 9-7
Apache NiFi 上传模板
上传模板后,将模板图标(三个相连的方框)从组件工具栏(顶部导航)拖动到画布(网格)中,如图 9-8 所示。在完成之前,模板组件向用户提示可用模板的列表;选择“Twitter 情绪 v2”并点击添加按钮。
图 9-8
Apache NiFi 添加模板
添加模板后,画布现在包含 10 个 NiFi 处理器,如图 9-5 所示。模板提供的处理器被预先配置为利用前面章节中安装的组件,比如 Apache Kafka 和 Elasticsearch。双击任意处理器,并选择属性选项卡以查看其配置。
在激活新数据流之前,处理器get witter需要 Twitter 提供的消费者密钥、消费者秘密、访问令牌和访问令牌秘密。通过创建 Twitter 帐户、访问门户网站,然后从下拉导航中选择应用来生成这些值。 30 在 Twitter 开发者应用页面上, 31 点击创建应用按钮,完成所需步骤。一旦 Twitter 批准了新的应用,检索图 9-9 所示的令牌、密钥和秘密。
图 9-9
Apache NiFi Twitter 密钥和令牌
填充图 9-10 所示get witter处理器所需的值。
图 9-10
配置 Apache NiFi GetTwitter 处理器
新数据流已准备好运行。然而,Elasticsearch 是最终的端点,需要一个索引模板来适当地存储数据字段。下一节将向 Elasticsearch 添加一个索引模板。
准备弹性搜索
模板提供的 NiFi 处理器PutElasticsearchHttp将最终处理的数据放入一个 Elasticsearch 索引中,匹配模式sentiment-${now():format('yyyy-MM')},为一年中的每个月创建一个新的索引。PutElasticsearchHttp接收并放置由之前的处理器组装的 JSON 数据。这个 JSON 数据结构包含文本、数字和日期值。Elasticsearch 可以检测并自动设置数据类型,但它并不完美,容易被各种日期格式混淆。Elasticsearch 自然无法确定诸如数字零这样的值是整数还是浮点数。通过为 Elasticsearch 提供一个索引模板来实现正确的索引。 32
索引模板由基于 JSON 的配置组成,定义了一个或多个字段应该如何被索引。以下命令将端口转发弹性搜索并发布一个与处理后的数据所产生的数据类型相匹配的索引模板。
打开终端和端口转发弹性搜索:
$ kubectl port-forward elasticsearch-0 9200:9200 -n data
打开另一个终端,通过发出清单 9-5 中的命令发布索引模板。
cat <<EOF | curl -X POST \
-H "Content-Type: application/json" \
-d @- http://localhost:9200/_template/all
{
"index_patterns": ["sentiment-*"],
"settings": {
"number_of_shards": 1
},
"mappings": {
"_source": {
"enabled": true
},
"properties": {
"polarity": {
"type": "float"
},
"subjectivity": {
"type": "float"
},
"sentence_count": {
"type": "integer"
},
"Content-Length": {
"type": "integer"
},
"X-Start-Time": {
"type": "date",
"format": "epoch_millis"
},
"X-Duration-Seconds": {
"type": "float"
},
"twitter.created_at": {
"type": "date",
"format": "EEE MMM dd HH:mm:ss Z yyyy",
"null_value": ""
},
"Date": {
"type": "date",
"format": "EEE, dd MMM yyyy HH:mm:ss z"
}
}
}
}
EOF
Listing 9-5HTTP post an Elasticsearch index template
Elasticsearch 现在能够正确地索引来自数据流的处理过的数据,该数据流由前一节中加载的 NiFi 模板定义。下面的部分启动数据流并查询处理后的数据。
#### 数据流
本章前面作为模板加载的示例 ETL 数据流水线从 Twitter 提取数据,并将其发布到 Apache Kafka 主题。另一组处理器消耗来自 Kafka 主题的数据,获得 Twitter 消息的文本,并将其发送到 OpenFaaS 情感分析功能。最后一组处理器将情感分析的结果与来自原始数据的字段结合起来,并将结果作为 JSON 发布到 Elasticsearch 进行索引。
Twitter 产生了高速、无休止的半结构化数据流,代表了典型的数据处理场景。在这个例子中使用 Apache Kafka 不是必须的,只是用来演示附加的 NiFi 处理器。然而,Kafka 的使用允许外部系统有机会对其数据事件流进行操作,从而为扩展流水线提供了更多的机会。
要启动新的数据流,单击 NiFi 画布(网格)上的任意位置,并通过单击 Operate Palette 提供的 play 按钮启动所有数据处理器。
几分钟后,打开终端,端口转发弹性搜索:
$ kubectl port-forward elasticsearch-0 9200:9200 -n data
打开另一个终端,通过发出清单 9-6 中的命令发布一个 Elasticsearch 查询。以下查询将情绪分析中最后一个小时的极性度量聚合到直方图桶中,从-1 到 1,每 0.5 个间隔一次。Elasticsearch 支持一组强大的聚合功能。<sup>33</sup>
cat <<EOF | curl -X POST
-H "Content-Type: application/json"
-d @- http://localhost:9200/sentiment-*/_search
{
"size": 0,
"aggs": {
"polarity": {
"histogram" : {
"field" : "polarity",
"interval" : 0.5,
"extended_bounds" : {
"min" : -1,
"max" : 1
}
}
}
},
"query": {
"range": {
"Date": {
"gt": "now-1h"
}
}
}
}
EOF
Listing 9-6HTTP post an Elasticsearch Sentiment Analysis query
示例结果(见清单 9-7 )显示,在过去的一个小时里,关于新冠肺炎的负面推特帖子`("doc_count": 40`是正面帖子`("doc_count": 4`的十倍。
{ "took": 5, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 2276, "relation": "eq" }, "max_score": null, "hits": [] }, "aggregations": { "polarity": { "buckets": [ { "key": -1, "doc_count": 40 }, { "key": -0.5, "doc_count": 404 }, { "key": 0, "doc_count": 1718 }, { "key": 0.5, "doc_count": 110 }, { "key": 1, "doc_count": 4 } ] } } }
Listing 9-7Example aggregation output from Elasticsearch Sentiment Analysis query
示例 ETL 数据流试图展示 Apache NiFi 提供的众多特性中的一小部分,以及 Kubernetes 为数据管理、存储和处理系统的近乎无缝的互连提供理想平台的能力。本书中的平台展示了 Kubernetes 对各种各样的应用的处理,从 NiFi、Elasticsearch 和 Kafka 等大型应用到无服务器功能,这些应用被包装在容器中,通过统一的网络和控制面板跨多个服务器进行部署、监控和管理。
下一节利用 JupyterLab 环境,展示实时实验和与平台数据交互的能力。
### 分析和程序控制
在第六章中安装和配置的 JupyterHub 提供了 JupyterLab 环境,方便了一个或多个直接在集群中运行的 Jupyter 笔记本的操作。以下两个练习演示了在 Elasticsearch 中索引数据的简单查询和可视化,以及以编程方式开发 NiFi 数据流的能力。
#### 分析和可视化
这个例子使用了 JupyterHub 提供的基于 Python 的 Jupyter 笔记本(见第六章)。当数据流入 Elasticsearch 时,它会立即被索引,并可通过其所有字段进行搜索。该示例从 Elasticsearch 索引中返回多达 10,000 条记录,以情绪开始,并且日期字段值在最后一个小时内。
打开一个新的基于 Python 的 Jupyter 笔记本,将下面的每个代码块添加到单独的单元格中。
通过向第一个单元添加以下命令来安装 Elasticsearch 包版本 7.6.0:
!pip install elasticsearch==7.6.0
导入`elasticsearch`、`pandas`和`matplotlib`:
from elasticsearch import Elasticsearch import pandas as pd from matplotlib import pyplot
创建一个连接到在 Kubernetes 名称空间数据中运行的 Elasticsearch 服务的 elasticsearch 客户端:
es = Elasticsearch(["elasticsearch.data"])
使用 Elasticsearch 客户端的搜索功能来查询索引模式“情绪-*”,并将结果存储在变量 response 中:
response = es.search( index="sentiment-*", body={ "size": 10000, "query": { "range": { "Date": { "gt": "now-1h" } } }, "_source": [ "Date", "polarity", "subjectivity" ], } )
将 Elasticsearch 的回答映射并转置到 Pandas 数据框架中:
df = pd.concat(map(pd.DataFrame.from_dict, response['hits']['hits']), axis=1)['_source'].T
将日期列转换为 Python 日期时间数据类型:
datefmt = '%a, %d %b %Y %H:%M:%S GMT' df['Date'] = pd.to_datetime(df['Date'], format=datefmt)
将日期字段分配给数据帧索引,并将所有数值转换为浮点数:
df = df.set_index(['Date']) df = df.astype(float)
打印前五条记录(如图 9-11 ):

图 9-11
情感分析数据框架行示例
df.head()
最后,通过调用数据帧的绘图函数绘制情绪,将极性分配给 y 轴(见图 9-12 ):

图 9-12
情感分析数据框架图
df.plot(y=["polarity"], figsize=(13,5))
前面的例子是数据分析和可视化的基本示例。数据科学家或分析师的第一步可能包括类似的任务,以形成对可用数据的粗略了解。机器学习等数据科学活动通常需要不可变/固定的数据集,以促进可重复的实验。将集群内 Jupyter 笔记本电脑与 MinIO 对象存储(在第七章中安装)以及事件队列、数据管理和 ETL 系统相连接的能力,为高效构建和共享这些有价值的数据集提供了许多机会。
Kubernetes 支持的 JupyterLab 环境提供了一个合适的平台,通过内部公开的 API(如 Apache NiFi)对集群资源进行交互式编程控制;下一节将介绍一个简单的例子。
#### 编程 NiFi
Apache NiFi 支持通过用 Java 编写的定制控制器和处理器进行扩展。然而,NiFi 强大的标准处理器意味着许多项目将会找到一套适用于多种情况的处理器。NiFi 功能的另一个扩展是通过 API, <sup>35</sup> 促进自动化和监控。本节包含一个创建 NiFi 进程组并使用单个处理器填充它的简短示例。
打开一个新的基于 Python 的 Jupyter 笔记本,将下面的每个代码块添加到单独的单元格中。
通过向第一个单元添加以下命令,安装 NiPyApi<sup>36</sup>Python 包版本 1.14.3:
!pip install nipyapi==0.14.3
导入包:
import nipyapi
用 API 端点配置 NiFi 客户机,在本例中是在`data`名称空间中的 Kubernetes 服务`nifi`:
api_url = "nifi.data:8080/nifi-api" nipyapi.utils.set_endpoint(api_url)
通过检索群集中第一个节点的信息来测试客户端连接;图 9-13 描述了示例输出:

图 9-13
NiFi Python 客户端节点输出
nodes = nipyapi.system.get_cluster().cluster.nodes nodes[0]
创建一个 NiFi 进程组 <sup>37</sup> ,并将其放置在画布上(在本章前面添加的处理器之上):
pg0id = nipyapi.canvas.get_process_group( nipyapi.canvas.get_root_pg_id(), 'id' )
pg0 = nipyapi.canvas.create_process_group( pg0id, "apk8s_process_group_0", location=(800.0, 200.0) )
在使用前一个示例中的代码执行单元格后,访问 NiFi web 界面,注意前面添加的处理器上方的新`apk8s_process_group_0`进程组,如图 9-14 所示。双击新的流程组会显示一个空白的画布。

图 9-14
Python API 客户端添加的 NiFi 进程组
在之前创建的新 NiFi 进程组中创建一个 GenerateFlowFile<sup>38</sup>处理器,并将其放置在画布上:
gf = nipyapi.canvas.get_processor_type('GenerateFlowFile')
p0 = nipyapi.canvas.create_processor( parent_pg=pg0, processor=gf, location=(250.0, 0.0), name="apk8s_processor_0", config=nipyapi.nifi.ProcessorConfigDTO( scheduling_period='1s', auto_terminated_relationships=['success'] ) )
在使用前面示例中的代码执行单元格后,访问 NiFi web 界面,双击新的流程组,并查看新创建的 GenerateFlowFile,如图 9-15 所示。

图 9-15
Python API 客户端添加的 NiFi 处理器
这个部分自动创建一个 NiFi 进程组和一个 NiFi GenerateFlowFile 处理器。这个例子展示了数据流开发的粒度方法。NiFi API 和 NiPyApi Python 包还支持模板的安装和配置,允许开发人员设计各种完整的数据流并将其保存为模板,如 Twitter 情感 v2(在本章前面添加),使它们可用于编程配置、部署和监控。
## 摘要
本章安装了无服务器平台 OpenFaaS 和数据路由和转换平台 Apache NiFi(参见清单 9-8 ),展示了与前几章安装的其他数据管理组件的互连性,特别是 Apache Kafka、Elasticsearch 和 JupyterLab。本章展示了 Twitter 消息的提取、转换、加载、处理和分析,所有这些都不需要定制代码,但提供了许多代码扩展方法,从为 OpenFaaS 编写定制(无服务器)函数,到通过 JupyterLab 中的 Python 与 Elasticsearch、Kafka 和 NiFi 交互。
本书旨在展示在 Kubernetes 上快速组装、管理和监控数据平台的简易性。与 Kubernetes 的集成程度非常广泛。OpenFaaS 和 JupyterHub 等软件利用 Kubernetes API 本身来部署和扩展 pod,而 NiFi 等其他软件在运行时并不了解 Kubernetes。
本书中开发的 Kubernetes 数据平台运行在一个小规模、资源受限、四节点的开发集群上,每天只需花费几美元。然而,这个小集群涵盖了许多基本的数据处理概念,包括数据事件、索引、处理、数据库、数据湖、数据仓库、分布式查询执行、现代 ETL 操作和数据科学环境。这些能力对于希望收集、处理和分析各种数据的组织来说是必不可少的。物联网和机器学习是对数据管理有大量需求的概念的例子,从高速实时非结构化和半结构化数据的收集到用于训练和提炼机器学习模型的经过处理、标准化、结构良好的数据目录。
./009-cluster-apk8s-dev5 ├── 000-cluster ├── 003-data │ ├── 000-namespace │ ├── 005-keycloak │ ├── 010-zookeeper │ ├── 020-kafka │ ├── 030-elasticsearch │ ├── 032-logstash │ ├── 034-kibana │ ├── 050-mqtt │ ├── 060-cassandra │ ├── 070-minio │ ├── 080-mysql │ ├── 085-hive │ ├── 095-presto │ ├── 100-jupyterhub │ ├── 120-openfaas │ └── 150-nifi └── 005-data-lab └── 000-namespace
Listing 9-8Organization of Kubernetes-based data platform components
<aside aria-label="Footnotes" class="FootnoteSection" epub:type="footnotes">Footnotes 1
[`https://aws.amazon.com/lambda/`](https://aws.amazon.com/lambda/)
2
[`https://azure.microsoft.com/en-us/services/functions/`](https://azure.microsoft.com/en-us/services/functions/)
3
[`https://cloud.google.com/functions/docs/`](https://cloud.google.com/functions/docs/)
4
[`www.ibm.com/cloud/functions`](http://www.ibm.com/cloud/functions)
5
[`https://openwhisk.apache.org/`](https://openwhisk.apache.org/)
6
[`https://kubeless.io/`](https://kubeless.io/)
7
[`www.openfaas.com/`](http://www.openfaas.com/)
8
[`https://knative.dev/`](https://knative.dev/)
9
[`https://ffmpeg.org/`](https://ffmpeg.org/)
10
[`https://imagemagick.org/`](https://imagemagick.org/)
11
[`https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types`](https://kubernetes.io/docs/concepts/services-networking/service/%2523publishing-services-service-types)
12
古普塔沙申克。"情感分析:概念、分析和应用."中等,2018 年 1 月 19 日。 [`https://towardsdatascience.com/sentiment-analysis-concept-analysis-and-applications-6c94d6f58c17`](https://towardsdatascience.com/sentiment-analysis-concept-analysis-and-applications-6c94d6f58c17) 。
13
科萨科夫斯卡、阿加塔、阿格尼耶斯卡·兰多芙斯卡、马留什·什沃奇、维奥莱塔·什沃奇和米哈尔·罗贝尔。"情感识别及其应用."智能系统和计算进展 300(2014 年 7 月 1 日):51–62。 [`https://doi.org/10.1007/978-3-319-08491-6_5`](https://doi.org/10.1007/978-3-319-08491-6_5) 。
14
分析 Vidhya。“使用主题建模挖掘在线评论的 NLP 方法”,2018 年 10 月 16 日。 [`www.analyticsvidhya.com/blog/2018/10/mining-online-reviews-topic-modeling-lda/`](http://www.analyticsvidhya.com/blog/2018/10/mining-online-reviews-topic-modeling-lda/) 。
15
[`https://github.com/openfaas/faas/tree/master/sample-functions/SentimentAnalysis`](https://github.com/openfaas/faas/tree/master/sample-functions/SentimentAnalysis)
16
[`https://textblob.readthedocs.io/en/dev/`](https://textblob.readthedocs.io/en/dev/)
17
[`https://docs.openfaas.com/reference/authentication/#for-functions`](https://docs.openfaas.com/reference/authentication/%2523for-functions)
18
[`https://github.com/openfaas/workshop/blob/master/lab11.md`](https://github.com/openfaas/workshop/blob/master/lab11.md)
19
[`https://github.com/openfaas/workshop`](https://github.com/openfaas/workshop)
20
健康催化剂。“医疗保健信息系统:过去、现在和未来”,2014 年 5 月 20 日。 [`www.healthcatalyst.com/insights/healthcare-information-systems-past-present-future`](http://www.healthcatalyst.com/insights/healthcare-information-systems-past-present-future) 。
21
[`https://wiki.pentaho.com/`](https://wiki.pentaho.com/)
22
[`www.talend.com/products/talend-open-studio`](http://www.talend.com/products/talend-open-studio)
23
[`www.cloverdx.com/`](http://www.cloverdx.com/)
24
[`https://community.jaspersoft.com/project/jaspersoft-etl`](https://community.jaspersoft.com/project/jaspersoft-etl)
25
[`https://nifi.apache.org/`](https://nifi.apache.org/)
26
[`https://nifi.apache.org/docs.html`](https://nifi.apache.org/docs.html)
27
[`https://github.com/cetic/helm-nifi`](https://github.com/cetic/helm-nifi)
28
[`https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html#user_authentication`](https://nifi.apache.org/docs/nifi-docs/html/administration-guide.html%2523user_authentication)
29
[`https://nifi.apache.org/docs.html`](https://nifi.apache.org/docs.html)
30
[`https://developer.twitter.com/en`](https://developer.twitter.com/en)
31
[`https://developer.twitter.com/en/apps`](https://developer.twitter.com/en/apps)
32
[`www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html`](http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html)
33
[`www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html`](http://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html)
34
[`https://medium.com/hashmapinc/creating-custom-processors-and-controllers-in-apache-nifi-e14148740ea`](https://medium.com/hashmapinc/creating-custom-processors-and-controllers-in-apache-nifi-e14148740ea)
35
[`https://nifi.apache.org/docs/nifi-docs/rest-api/index.html`](https://nifi.apache.org/docs/nifi-docs/rest-api/index.html)
36
[`https://github.com/Chaffelson/nipyapi`](https://github.com/Chaffelson/nipyapi)
37
[`https://nifi.apache.org/docs/nifi-docs/html/user-guide.html#process_group_anatomy`](https://nifi.apache.org/docs/nifi-docs/html/user-guide.html%2523process_group_anatomy)
38
[`https://nifi.apache.org/docs/nifi-docs/components/org.apache.nifi/nifi-standard-nar/1.11.4/org.apache.nifi.processors.standard.GenerateFlowFile/index.html`](https://nifi.apache.org/docs/nifi-docs/components/org.apache.nifi/nifi-standard-nar/1.11.4/org.apache.nifi.processors.standard.GenerateFlowFile/index.html)
</aside>
# 十、平台化区块链
区块链是一个概念,能够维护分散和不可信的交易数据库。2009 年发布的比特币网络 <sup>1</sup> 通过实现一种在共享公共账本上生成和交易加密货币的方法,普及了区块链技术。尽管比特币仍然是最受欢迎的加密货币形式,但新的区块链技术已经崛起,将功能扩展到货币兑换之外。
这本书关注以太坊,比特币最成功的替代品之一。虽然比特币对智能合约有一些能力,但以太坊原生支持从 Solidity 编译的分布式应用的存储和执行,Solidity 是其用于智能合约的 DSL(域特定语言)。
以业务为中心的区块链技术越来越多,包括 Hyperledger Fabric、 <sup>3</sup> Corda、 <sup>4</sup> 和 Quorum。 <sup>5</sup> 这些区块链应用直接关注许可或私有的区块链网络,是一些组织的绝佳选择。然而,以太坊 <sup>6</sup> 支持私有和许可的区块链及其巨大成功的公共网络。 <sup>7</sup> 开发基于 Solidity 的 <sup>8</sup> 应用意味着可以移植到任何以太坊实现:公共的、私有的或许可的。许可以太网将在本章后面讨论。
这本书实现了一个封闭的、私有的以太坊网络,适合实验和开发。由于一方控制节点的一致性,私有集中式以太网几乎没有生产应用。开发环境如 Truffle Suite 为个人开发者提供了优秀的交钥匙解决方案;然而,运行私有以太网有助于理解公共网络,并提供一个受控的多租户开发环境。
## 私有区块链平台
本章重点介绍构建一个私有以太坊区块链网络,其操作类似于全球公共以太坊网络。在 Kubernetes 中运行 Ethereum 节点,无论是公共的、私有的还是受保护的,都可以提供这个优雅的应用平台的所有优势,包括统一的网络和控制面板、容错和自修复、声明式配置、监控以及跨大量服务器扩展的工作负载的透明分布。
图 10-1 代表了本章的一个高级目标,通过无服务器功能(使用 OpenFaaS)的应用开发和多租户 JupyterHub 提供的 Jupyter 笔记本的实验和开发,使以太坊节点平台化并与之交互。Kubernetes 促进了这些不同应用的混合,并为组装支持新技术的奇异平台提供了许多机会。

图 10-1
区块链网络开发平台
图 10-1 表示在数据平台的背景下汇集在一起的无服务器、区块链和数据科学环境。
## 发展环境
以下区块链开发平台利用了第六章中首次提到的廉价 Hetzner 集群,包括一个用于 Kubernetes 主节点的 CX21 (2 个 vCPU/8G RAM/40G SSD)和四个用于 worker 节点的 CX41 (4 个 vCPU/16G RAM/160G SSD)实例。任何等效的基础设施都将适应以下练习。
本章创建了一个名为`eth`的新 Kubernetes 集群,并利用了第 3 、 5 和 6 章中安装的应用和集群配置(如表 10-1 中所述)。本章将所有配置清单组织在文件夹`cluster-apk8s-eth`下。
表 10-1
从前面章节收集的关键应用和配置
<colgroup><col class="tcol1 align-left"> <col class="tcol2 align-left"> <col class="tcol3 align-left"></colgroup>
| |
资源
|
组织
|
| --- | --- | --- |
| 第三章 | 进入证书管理器仓库监视 | `000-cluster/00-ingress-nginx``000-cluster/10-cert-manager``000-cluster/20-rook-ceph``000-cluster/30-monitoring` |
| 第五章 | 命名空间 | `003-data/000-namespace` |
| 第六章 | 凯克洛克 JupyterHub | `003-data/005-keycloak``005-data-lab/000-namespace``003-data/100-jupyterhub` |
| 第九章 | OpenFaaS | `003-data/120-openfaas` |
## 私有以太网
下面几节组装一个以太坊区块链开发集群(见图 10-2 ),模仿一个公共以太坊网络的一般操作。公共、私有或许可系统的基本组件是相同的。
Miner 节点(也称为 Full 节点)是以太网的核心组件。以太坊节点可以是实现以太坊协议的任何应用。本章使用 Go 中开发的 Geth, <sup>9</sup> 作为以太坊协议最初的三个实现之一。Geth 提供了一个独立的二进制文件和一个开源库,适合构建实现以太坊协议的定制应用/节点。本章使用包装在容器(`ethereum/client-go`)中的 Geth 二进制文件,表示三种类型的节点:boot nodes(Geth 包中的一个单独的二进制文件)、矿工和事务节点。

图 10-2
私有以太网
以太坊是一个点对点的节点网络,使用已建立的启动节点 <sup>10</sup> 将新节点连接到网络。boot nodes<sup>11不挖掘或提交事务;它们只负责初始对等体发现。Geth 附带一个主网络和测试网络 Bootnode 地址列表;但是,本章中的私有以太坊网络要求 Geth 使用本地 Bootnodes。本章使用 Bootnode 注册服务来提供本地 boot node 的地址。</sup>
事务节点是可选的;任何节点,包括矿工,都可以提交预先设计的交易,以纳入区块链。矿工节点使用附属以太坊账户 <sup>12</sup> 来签署他们的交易;允许远程连接到这些节点为任何人提供了提交由矿工签名的事务的访问权。交易节点只能发送由最终用户签名的交易,因为没有附加的私人帐户。
最后,Geth 提供了向 API 端点报告其当前状态和指标的能力。本章利用 Ethstats 项目 <sup>13</sup> 来收集节点指标,并将它们呈现在 web 仪表板上。
### 根节点
以太坊引导节点通过提供一组初始对等点来帮助新节点引导到以太坊网络中。Bootnodes 是以太坊客户端实现的子集,只参与网络节点发现协议。Bootnodes 不实现任何更高级别的以太坊应用协议。
以下配置设置了 eth-bootnode 服务。对这个无头服务的 DNS 请求返回匹配选择器`app: eth-bootnode`的任何 pod 的内部主机名。这个服务可以方便地为 Bootnodes 生成注册表,这将在下一节中介绍。
创建目录`cluster-apk8s-eth/003-data/200-eth/10-bootnode`。在新的`10-bootnode`目录中,从清单 10-1 中创建一个名为`10-service.yml`的文件。
apiVersion: v1 kind: Service metadata: name: eth-bootnode namespace: data labels: app: eth-bootnode spec: selector: app: eth-bootnode clusterIP: None ports: - name: discovery port: 30301 protocol: UDP - name: http port: 8080
Listing 10-1Bootnode Service
应用以太坊 Bootnode 服务配置:
$ kubectl apply -f 10-service.yml
接下来,在清单 10-2 中的文件`30-deployment.yml`中为以太坊引导节点创建一个部署。以下 Bootnode 部署由两个容器和一个用于初始化的容器组成。初始化容器生成一个密钥,并将其存储在与 Pod 中的其他容器共享的卷挂载`data`中。Bootnode 使用该密钥创建其 enode 标识符(以太坊节点的唯一 ID)。
这两个容器中的第一个是 Ethereum Bootnode,它安装在初始化容器中生成的密钥,并在端口 30301/UDP 上通信。第二个容器名为`bootnode-server`,使用 netcat 执行一个小 shell 脚本,通过端口 8080 回显 Bootnode 的完整以太坊地址。 <sup>14</sup> 完整地址由 enode 标识符、IP 地址和端口组成。下一节中的 Bootnode 注册器使用 headless 服务(在上一节中配置)来发现 Bootnode Pods,然后通过 HTTP 端口 8080 检索它们的每个以太网地址,由`bootnode-server`容器提供服务。
apiVersion: apps/v1 kind: Deployment metadata: name: eth-bootnode namespace: data labels: app: eth-bootnode spec: replicas: 2 revisionHistoryLimit: 1 selector: matchLabels: app: eth-bootnode template: metadata: labels: app: eth-bootnode spec: volumes: - name: data emptyDir: {} initContainers: - name: genkey image: ethereum/client-go:alltools-v1.9.13 imagePullPolicy: IfNotPresent command: ["/bin/sh"] args: - "-c" - "bootnode --genkey=/etc/bootnode/node.key" volumeMounts: - name: data
mountPath: /etc/bootnode
containers:
- name: bootnode
image: ethereum/client-go:alltools-v1.9.13
imagePullPolicy: IfNotPresent
resources:
limits:
cpu: ".5"
requests:
cpu: "0.25"
command: ["/bin/sh"]
args:
- "-c"
- "bootnode --nodekey=/etc/bootnode/node.key --verbosity=4"
volumeMounts:
- name: data
mountPath: /etc/bootnode
ports:
- name: discovery
containerPort: 30301
protocol: UDP
- name: bootnode-server
image: ethereum/client-go:alltools-v1.9.13
imagePullPolicy: IfNotPresent
command: ["/bin/sh"]
args:
- "-c"
- "while [ 1 ]; do echo -e \"HTTP/1.1 200 OK\n\nenode://$(bootnode -writeaddress --nodekey=/etc/bootnode/node.key)@$(POD_IP):30301\" | nc -l -v -p 8080 || break; done;"
volumeMounts:
- name: data
mountPath: /etc/bootnode
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
ports:
- containerPort: 8080
Listing 10-2Bootnode Deployment
该集群现在包含两个以太坊启动节点;然而,新的以太坊节点需要完整的 enode 地址(`enode://ENODE_IDENTIFIER@POD_IP:30301`)才能使用。如前所述,Kubernetes headless 服务 eth-bootnode 提供各个节点的主机名,而容器`bootnode-server`通过 HTTP 在端口 8080 上报告其 enode 地址。下一节中定义的 Bootnode 注册器结合了这两个操作。
### bootmode registrar(引导模式注册)
Bootnode 注册器是一个小型 Golang 应用,用于查找由`eth-bootnode` headless 服务公开的 DNS 条目,查询每个 Pod 的 enode 地址,并通过一个简单的 HTTP 请求返回一个逗号分隔的字符串。后面的部分用这个 Bootnode 注册器的 enode 地址串配置每个 Geth 节点。
Note
潘石屹 <sup>15</sup> 写了 Bootnode 注册商申请,<sup>16</sup>连同 Kubernetes 以太坊的掌 Helm 图。 <sup>17</sup> 本章的概念和灵感来自这些项目和微软开发者博客文章*构建私有以太坊联盟*。 <sup>18</sup>
创建目录`cluster-apk8s-eth/003-data/200-eth/20-bootnode-reg`。在新的`20-bootnode-reg`目录中,从清单 10-3 中创建一个名为`10-service.yml`的文件。
apiVersion: v1 kind: Service metadata: name: eth-bootnode-registrar namespace: data labels: app: eth-bootnode-registrar spec: selector: app: eth-bootnode-registrar type: ClusterIP ports: - port: 80 targetPort: 9898
Listing 10-3Bootnode Registrar Service
应用以太坊 Bootnode Regis.trar 服务配置:
$ kubectl apply -f 10-service.yml
接下来,在清单 10-4 中名为`30-deployment.yml`的文件中为以太坊 Bootnode 注册器创建一个部署。
apiVersion: apps/v1 kind: Deployment metadata: name: eth-bootnode-registrar namespace: data labels: app: eth-bootnode-registrar spec: replicas: 1 revisionHistoryLimit: 1 selector: matchLabels: app: eth-bootnode-registrar template: metadata: labels: app: eth-bootnode-registrar spec: containers: - name: bootnode-registrar image: jpoon/bootnode-registrar:v1.0.0 imagePullPolicy: IfNotPresent env: - name: BOOTNODE_SERVICE value: "eth-bootnode.data.svc.cluster.local" ports: - containerPort: 9898
Listing 10-4Bootnode Deployment
Kubernetes 集群现在包含一个 Bootnode 注册服务。稍后,Geth miner 和事务节点部署在初始化时调用此服务,以提供 Ethereum Bootnode 地址列表。
### Ethstats
Geth 节点在配置了 Ethstats 端点时会发出度量。本节配置的 Ethstats web dashboard<sup>19</sup>摄取以太坊指标并呈现在一个有吸引力的 web 界面 <sup>20</sup> (如图 10-3 )。本节设置稍后在提供给 Geth 节点的命令行参数中使用的 Ethstats 服务、部署和机密。

图 10-3
ethstats.net 以太坊网络统计
创建目录`cluster-apk8s-eth/003-data/200-eth/30-ethstats`。在新的`30-ethstats`目录中,从清单 10-5 中创建一个名为`10-service.yml`的文件。
apiVersion: v1 kind: Service metadata: name: eth-ethstats namespace: data labels: app: eth-ethstats spec: selector: app: eth-ethstats type: ClusterIP ports: - port: 8080 targetPort: http
Listing 10-5Ethstats Service
应用 Ethstats 服务配置:
$ kubectl apply -f 10-service.yml
接下来,在清单 10-6 中的一个名为`15-secret.yml`的文件中为 Ethstats 创建一个秘密。
apiVersion: v1 kind: Secret metadata: name: eth-ethstats namespace: data labels: app: eth-ethstats type: Opaque stringData: WS_SECRET: "uGYQ7lj55FqFxdyIwsv1"
Listing 10-6Ethstats Secret
应用 Ethstats 服务配置:
$ kubectl apply -f 15-secret.yml
接下来,在清单 10-7 中的一个名为`30-deployment.yml`的文件中为 Ethstats 创建一个部署。
apiVersion: apps/v1 kind: Deployment metadata: name: eth-ethstats namespace: data labels: app: eth-ethstats spec: replicas: 1 revisionHistoryLimit: 1 selector: matchLabels: app: eth-ethstats template: metadata: labels: app: eth-ethstats spec: containers: - name: ethstats image: ethereumex/eth-stats-dashboard:v0.0.1 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 3000 env: - name: WS_SECRET valueFrom: secretKeyRef: name: eth-ethstats key: WS_SECRET
Listing 10-7Ethstats Deployment
应用 Ethstats 部署配置:
$ kubectl apply -f 30-deployment.yml
接下来,在清单 10-8 中的一个名为`50-ingress.yml`的文件中为 Ethstats 创建一个入口配置。
apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: eth-ethstats namespace: data labels: app: eth-ethstats annotations: cert-manager.io/cluster-issuer: letsencrypt-production spec: rules: - host: stats.data.eth.apk8s.dev http: paths: - backend: serviceName: eth-ethstats servicePort: 8080 path: / tls: - hosts: - stats.data.eth.apk8s.dev secretName: eth-ethstats-production-tls
Listing 10-8Ethstats Ingress
应用 Ethstats 入口配置:
$ kubectl apply -f 50-ingress.yml
最后,在网络浏览器中访问 [`https://stats.data.eth.apk8s.dev`](https://stats.data.eth.apk8s.dev) 。在 Geth 节点按照以下部分中的配置开始报告之前,应该没有数据。
### Geth 矿工
本书不探究以太坊协议的细节。不过这一节和下一节安装了 Geth 支持的两种以太坊节点:矿工节点 <sup>21</sup> 和事务节点。以太网上的所有节点都进行对等通信,共享网络拓扑、区块链状态和事务。Miner 节点致力于在由任何未决事务组成的区块链上创建新的区块。 <sup>22</sup>
创建目录`cluster-apk8s-eth/003-data/200-eth/40-miner`。在新的`40-miner`目录中,从清单 10-9 中创建一个名为`15-secret.yml`的文件。Geth 矿工需要一个以太坊帐户,用于签署交易和接收采矿奖励。一个初始化容器,稍后在每个 Geth 部署中定义,用清单 10-9 中定义的密码创建一个以太坊帐户。
apiVersion: v1 kind: Secret metadata: name: eth-geth-miner namespace: data labels: app: eth-geth-miner type: Opaque stringData: accountsecret: "strongpassword"
Listing 10-9Geth Secret
应用 Geth Secret 配置:
$ kubectl apply -f 15-secret.yml
按照在线安装文档在本地工作站上安装 Geth。 <sup>23</sup> Geth 提供所有主流操作系统和大部分软件包管理系统的安装程序。例如,装有自制软件的 MAC 可能会发出这样的命令:
$ brew install geth.
创建两个或更多以太坊账户。以下配置图中定义的以太坊起源文件指示新的区块链向这些账户(在第一个区块中)预注入指定数量的以太坊(以太坊加密货币)以供在专用网络中使用。
$ geth account new
使用`geth account new`命令创建多个帐户后,从输出中复制并保存“密钥的公共地址:”。接下来,在清单 10-10 中名为`20-configmap.yml`的文件中为 Geth 创建一个 ConfigMap。用新创建的账户更新`genesis.json`的`alloc`部分。
清单 10-10 中的 ConfigMap 中定义的`genesis.json`文件配置以太坊区块链的第一个区块。任何希望加入私有网络的节点必须首先根据这个以太坊起源文件进行初始化。 <sup>24</sup> 下面描述的 miner 和 transaction 节点都被配置为挂载在 ConfigMap 中定义为 key 的 genesis.json 文件(清单 10-10 )。
apiVersion: v1 kind: ConfigMap metadata: name: eth-geth namespace: data labels: app: eth-geth data: networkid: "27587" genesis.json: |- { "config": { "chainId": 27587, "homesteadBlock": 0, "eip150Block": 0, "eip155Block": 0, "eip158Block": 0 }, "difficulty": "0x400", "gasLimit": "0x8000000", "nonce" : "0x0000000000000000", "alloc": { "0xFa4087D3688a289c9C92e773a7b46cb9CCf80353": { "balance": "100000000000000000000" }, "0x8ab8F3fc6c660d3f0B22490050C843cafd2c0AAC": { "balance": "200000000000000000000" } } }
Listing 10-10Geth ConfigMap
应用 Geth 配置映射配置:
$ kubectl apply -f 20-configmap.yml
接下来,在清单 10-11 中名为`30-deployment.yml`的文件中为 Geth 创建一个部署配置。清单 10-11 中的 Geth miner 部署建立了一个由所有容器挂载的`data`卷,以及一个先前从清单 10-10 中的配置映射应用的配置卷。
第一个初始化容器`init-genesis`针对从 ConfigMap 挂载的 Genesis 文件运行 Geth `init`命令,并在挂载为`/root/.ethereum`的共享`data`卷中创建新的以太坊区块链数据库。
第二个初始化容器`create-account`使用清单 10-9 中应用的秘密`eth-geth-miner`中定义的密码为 Geth miner 创建一个唯一的以太坊账户。Geth 将新的以太坊账户存储在`/root/.ethereum`中,挂载为共享的`data`卷。
最后的初始化容器`get-bootnodes`运行一个小的 shell 脚本,试图通过调用本章前面配置的 Bootnode 注册器的`curl`来检索 boot node 列表。如果成功,`curl`调用的输出将返回的(用逗号分隔的)bootnodes 列表写入安装在共享卷`data`中的文件`/geth/bootnodes`。
初始化后,`eth-geth-miner`部署中定义的 pod 启动`geth-miner`容器并挂载共享卷`data` ( `/root/.ethereum`),其中初始化容器初始化区块链数据库,创建以太坊帐户,并存储一个包含 Bootnode 地址列表的文件。`geth-miner`容器使用以下参数执行`geth`:`--bootnodes`定义初始的 bootnodes 集合来查找对等体。`--mine`指示`geth`作为矿工操作,尝试创建砖块。`--minerthreads`设置并行挖掘线程的数量。`--nousb`禁用 USB 硬件钱包检查。`--miner.etherbase`取一个指向以太坊账户的索引,用来收集采矿奖励;在这种情况下,初始化容器 create-account 生成了第一个(也是唯一一个)帐户(索引为零)。`--networkid`指示`geth`连接到特定网络,在这种情况下,如`eth-geth`配置图中所定义。`--ethstats`接受能够从`geth;`接收指标的端点,在本例中,是前面部分配置的 Ethstats 仪表板。最后,`--verbosity`设置要输出的测井深度。
apiVersion: apps/v1 kind: Deployment metadata: name: eth-geth-miner namespace: data labels: app: eth-geth-miner spec: replicas: 3 revisionHistoryLimit: 1 selector: matchLabels: app: eth-geth-miner template: metadata: labels: app: eth-geth-miner spec: volumes: - name: data emptyDir: {} - name: config configMap: name: eth-geth initContainers: - name: init-genesis image: ethereum/client-go:v1.9.13 imagePullPolicy: IfNotPresent args: - "init" - "/var/geth/genesis.json" volumeMounts: - name: data mountPath: /root/.ethereum - name: config mountPath: /var/geth - name: create-account image: ethereum/client-go:v1.9.13 imagePullPolicy: IfNotPresent command: ["/bin/sh"] args: - "-c" - "printf '(ACCOUNT_SECRET)\n' | geth account new" env: - name: ACCOUNT_SECRET valueFrom: secretKeyRef: name: eth-geth-miner key: accountsecret volumeMounts:
- name: data
mountPath: /root/.ethereum
- name: get-bootnodes
image: ethereum/client-go:v1.9.13
imagePullPolicy: IfNotPresent
command: ["/bin/sh"]
args:
- "-c"
- |-
apk add --no-cache curl;
CNT=0;
echo "retrieving bootnodes from $BOOTNODE_REGISTRAR_SVC"
while [ $CNT -le 90 ]
do
curl -m 5 -s $BOOTNODE_REGISTRAR_SVC | xargs echo -n >> /geth/bootnodes;
if [ -s /geth/bootnodes ]
then
cat /geth/bootnodes;
exit 0;
fi;
echo "no bootnodes found. retrying $CNT...";
sleep 2 || break;
CNT=$((CNT+1));
done;
echo "WARNING. unable to find bootnodes.";
exit 0;
env:
- name: BOOTNODE_REGISTRAR_SVC
value: eth-bootnode-registrar
volumeMounts:
- name: data
mountPath: /geth
containers:
- name: geth-miner
image: ethereum/client-go:v1.9.13
imagePullPolicy: IfNotPresent
command: ["/bin/sh"]
args:
- "-c"
- "geth --bootnodes=\"`cat /root/.ethereum/bootnodes`\" --mine --minerthreads=1 --nousb --miner.etherbase=0 --networkid=${NETWORK_ID} --ethstats=${HOSTNAME}:${ETHSTATS_SECRET}@${ETHSTATS_SVC} --verbosity=3"
env:
- name: ETHSTATS_SVC
value: eth-ethstats:8080
- name: ETHSTATS_SECRET
valueFrom:
secretKeyRef:
name: eth-ethstats
key: WS_SECRET
- name: NETWORK_ID
valueFrom:
configMapKeyRef:
name: eth-geth
key: networkid
ports:
- name: discovery-udp
containerPort: 30303
protocol: UDP
- name: discovery-tcp
containerPort: 30303
volumeMounts:
- name: data
mountPath: /root/.ethereum
resources:
limits:
cpu: "400m"
requests:
cpu: "400m"
Listing 10-11Geth Deployment
应用 Geth 部署配置:
$ kubectl apply -f 30-deployment.yml
在应用 eth-geth-miner 部署并且三个副本 pod 已经初始化之后,矿工开始生成 DAG(有向非循环图),表示为以太坊的 PoW(工作证明)协议中使用的一个或多个千兆字节的数据。在这个高度受限的开发集群上,预计这些过程需要 20 分钟到一个小时。一旦 geth 矿工完成 DAG 生成,采矿开始,并且随着矿工添加区块,区块链开始增长。
新的采矿池大约每 15 `–` 30 秒向链中添加块,这取决于 CPU 资源和网络校准的以太坊 PoW 难度等级。虽然最初在早期定义的 Genesis 文件中设置为低 0x400,但以太坊网络根据添加到链中的块之间的时间差来校准难度。
最初的一组矿工继续无限期地构建区块链,无论每个区块中是否包含交易。下一节添加了专门用于与私有以太坊区块链交互的节点,包括提交事务。
### 获取事务节点
本节配置启用了 RPC(远程过程调用)管理 API 的 Geth 节点。以下事务节点实现了完整的以太坊协议,但没有启用挖掘,因此不需要以太坊帐户。交易节点可能只提交由外部以太坊账户预先设计的交易,使其成为与区块链进行外部通信的合适网关。稍后,本章将通过 Jupyter 笔记本和无服务器功能演示与区块链的交互。
创建目录`cluster-apk8s-eth/003-data/200-eth/50-tx`。在新的`50-tx`目录中,从清单 10-12 中创建一个名为`10-service.yml`的文件。
apiVersion: v1 kind: Service metadata: name: eth-geth-tx namespace: data labels: app: eth-geth-tx spec: selector: app: eth-geth-tx type: ClusterIP ports: - name: rpc port: 8545 - name: ws port: 8546
Listing 10-12Geth transaction node Service
应用 Geth 事务节点服务配置:
$ kubectl apply -f 10-service.yml
接下来,在清单 10-13 中名为`30-deployment.yml`的文件中为 Geth 事务节点创建一个部署配置。
`eth-geth-tx`部署与前一节中配置的`eth-geth-miner`几乎相同,只有一些关键的不同。Geth `eth-geth-tx`部署不初始化以太坊帐户,也不设置`--mine`、`--minerthreads`和`--miner.etherbase`命令行选项。(事务模式)geth 的新命令行选项包括: **- rpc** 启用 HTTP-RPC <sup>25</sup> 服务器,支持端口 8548 上的远程连接; **- rpcaddr** 设置 HTTP-RPC 服务器监听接口,这里是全部(IP 0 . 0 . 0 . 0); **- rpcapi** 设置 api 以通过 HTTP-RPC 接口启用,在本例中为 eth、net 和 web3<sup>26</sup>**-rpcvhosts**设置允许连接的域列表(由服务器强制执行);并且 **- rpccorsdomain** 为跨来源请求设置(由 web 浏览器强制)允许的域。
apiVersion: apps/v1 kind: Deployment metadata: name: eth-geth-tx namespace: data labels: app: eth-geth-tx spec: replicas: 2 selector: matchLabels: app: eth-geth-tx template: metadata: labels: app: eth-geth-tx spec: volumes: - name: data emptyDir: {} - name: config configMap: name: eth-geth initContainers: - name: init-genesis image: ethereum/client-go:v1.9.13 imagePullPolicy: IfNotPresent args: - "init" - "/var/geth/genesis.json" volumeMounts: - name: data mountPath: /root/.ethereum - name: config mountPath: /var/geth - name: get-bootnodes image: ethereum/client-go:v1.9.13 imagePullPolicy: IfNotPresent command: ["/bin/sh"] args:
- "-c"
- |-
apk add --no-cache curl;
COUNT=0;
echo "calling $BOOTNODE_REGISTRAR_SVC"
while [ $COUNT -le 100 ]
do
curl -m 5 -s $BOOTNODE_REGISTRAR_SVC | xargs echo -n >> /geth/bootnodes;
if [ -s /geth/bootnodes ]
then
cat /geth/bootnodes;
exit 0;
fi;
echo "Attempt $COUNT. No bootnodes found...";
sleep 2 || break;
COUNT=$((COUNT+1));
done;
echo "ERROR: Unable to find bootnodes.";
exit 0;
env:
- name: BOOTNODE_REGISTRAR_SVC
value: eth-bootnode-registrar
volumeMounts:
- name: data
mountPath: /geth
containers:
- name: geth-tx
image: ethereum/client-go:v1.9.13
imagePullPolicy: IfNotPresent
command: ["/bin/sh"]
args:
- "-c"
- "geth --nousb --bootnodes=`cat /root/.ethereum/bootnodes` --rpc --rpcaddr='0.0.0.0' --rpcapi=eth,net,web3 --rpcvhosts='*' --rpccorsdomain='*' --ws --networkid=${NETWORK_ID} --ethstats=${HOSTNAME}:${ETHSTATS_SECRET}@${ETHSTATS_SVC} --verbosity=2"
env:
- name: ETHSTATS_SVC
value: eth-ethstats:8080
- name: ETHSTATS_SECRET
valueFrom:
secretKeyRef:
name: eth-ethstats
key: WS_SECRET
- name: NETWORK_ID
valueFrom:
configMapKeyRef:
name: eth-geth
key: networkid
ports:
- name: rpc
containerPort: 8545
- name: ws
containerPort: 8546
- name: discovery-udp
containerPort: 30303
protocol: UDP
- name: discovery-tcp
containerPort: 30303
volumeMounts:
- name: data
mountPath: /root/.ethereum
Listing 10-13Geth transaction node Deployment
应用 Geth 事务节点部署配置:
$ kubectl apply -f 30-deployment.yml
在这个阶段,现在应该有五个节点向之前配置的 Ethstats 仪表板报告(见图 10-4 ),包括三个矿工和两个事务节点。

图 10-4
Ethstats 私有以太坊节点报告
这个网络是一个高度受限的公共以太网的微型复制品。诸如此类的私有区块链网络对于构建和部署实验节点、智能合约 <sup>27</sup> 开发,以及将区块链运营的任何方面连接到贯穿本书开发的更广泛的数据和应用平台都是有用的。在向第三方节点开放该网络之前,请考虑以下关于专用网络的部分。
### 专用网络
通过外部组织运营的远程节点来扩展这一网络的价值有限。基于电力区块链在小范围内很少有意义。任何能够提供超过 50%的网络挖掘散列率的组织都可以验证一个本来无效的交易,这被称为 51%攻击。希望与一组精选的其他组织一起参与的组织可以将本章中的概念改编为由 Geth 支持的以太坊的新团体共识协议。集团配置节点不挖掘 PoW,而是使用 PoA(权威证明 <sup>29</sup> )。
转换此网络以使用 Clique 涉及创建一个新的 Genesis 块,该块具有允许对块进行签名的节点的初始列表。请参阅 Ethereum 的详细说明指南,了解如何将该网络转换为 Clique consensus 协议。 <sup>三十</sup>
## 区块链互动
通过与节点通信来执行与区块链的交互。全以太坊节点(本书上下文中的 miner 节点)管理区块链的完整副本,并与网络上的其他节点(对等体)进行事务和状态通信。Geth 提供了一个提供外部访问的 HTTP-RPC API。本章前面配置的五个节点中的两个,称为事务节点,提供由服务`eth-geth-tx`公开的 HTTP-RPC 访问。
### 获取附件
Geth 提供了一个交互控制台 <sup>31</sup> 用于与其 API 进行交互。试验该 API 最简单的方法之一是使用 geth 附加到 geth 的另一个本地实例。以下示例在三个 miner 节点之一上执行 geth,并与正在运行的 miner 进行交互:
$ kubectl exec -it -n data eth-geth-miner-789dd75565-gk25b -- geth attach
geth 控制台输出示例:
Welcome to the Geth JavaScript console!
instance: Geth/v1.9.13-stable-cbc4ac26/linux-amd64/go1.14.2 coinbase: 0x284f99f929b49da9d85b2a3dbf606ed38eec393e at block: 1134 (Fri May 08 2020 06:19:03 GMT+0000 (UTC)) datadir: /root/.ethereum modules: admin:1.0 debug:1.0 eth:1.0 ethash:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0
eth.blockNumber 1159
通过端口转发本章前面设置的`eth-geth-tx`服务,从本地工作站与 geth 通信,并将本地`geth`附加到转发的服务。
$ kubectl port-forward svc/eth-geth-tx 8545 -n data Forwarding from 127.0.0.1:8545 -> 8545 Forwarding from [::1]:8545 -> 8545
在本地工作站上打开另一个终端,并连接 geth:
$ geth attach http://localhost:8545
Geth 的交互式 JavaScript 控制台是探索 API 的好方法。但是,以太坊提供了各种成熟的客户端库,用于构建与以太坊区块链交互的应用。接下来的部分将通过 Jupyter Notebook 提供的交互式 Python 环境和在无服务器平台 OpenFaaS 中开发的小函数来研究 Ethereum 的 Web3 Python 库,返回链中最新块的信息。
### 木星环境
针对复杂和精密的云架构进行的软件实验和开发通常会在将开发人员和分析师与服务联系起来方面带来独特的挑战。基于云的架构中的许多(如果不是大多数)应用与其他应用进行系统间通信,而没有明确的外部访问方法。端口转发是从本地工作站访问 Kubernetes 集群内部服务的典型方法;然而,基于 web 的 ide 可以作为平台本身的扩展。
Jupyter 笔记本是一个基于浏览器(或基于 web)的 IDE,需要在 Kubernetes 集群中运行以下示例。第六章描述了 JupyterHub(以及 Keycloak)的配置,该配置用作管理一台或多台 Jupyter 笔记本电脑的 JupyterLab 环境的多租户供应器。从集群中创建新的 Python 3 Jupyter 笔记本;在单个单元格内复制并执行以下代码示例。
导入 Python 库 web3、 <sup>32、</sup> json,时间:
import web3, json, time import pandas as pd from IPython.display import clear_output from web3.contract import ConciseContract from web3 import Web3 from web3.auto.gethdev import w3
连接到 Geth 事务节点:
rpc_ep = "eth-geth-tx.data:8545" web3 = Web3(Web3.HTTPProvider(rpc_ep))
if web3.isConnected(): print(f"Connected: {rpc_ep}") print(f"Peers: {web3.net.peerCount}") print(f"Chain ID: {web3.net.version}") print(f"Last block: {web3.eth.blockNumber}") else: print("Not connected")
示例输出:
Connected: eth-geth-tx.data:8545 Peers: 4 Chain ID: 27587 Last block: 5549
检查在本章前面定义的创世模块中预先注资的账户的 eth 余额:
account_1 = "0xFa4087D3688a289c9C92e773a7b46cb9CCf80353" account_2 = "0x8ab8F3fc6c660d3f0B22490050C843cafd2c0AAC"
a1_bal = web3.eth.getBalance(account_1) a2_bal = web3.eth.getBalance(account_2)
print(f"Account 1: {web3.fromWei(a1_bal, 'ether')} ether") print(f"Account 2: {web3.fromWei(a2_bal, 'ether')} ether")
示例输出:
Account 1: 100 ether Account 2: 200 ether
添加以下代码以创建一个事务,将一个以太网转移到 account_2:
nonce = web3.eth.getTransactionCount(account_1) print(f"Account 1 nonce: {nonce}")
tx = { 'nonce': nonce, 'to': account_2, 'value': web3.toWei(1, 'ether'), 'gas': 2000000, 'gasPrice': web3.toWei('50', 'gwei'), }
tx
示例输出:
{'nonce': 15, 'to': '0x8ab8F3fc6c660d3f0B22490050C843cafd2c0AAC', 'value': 1000000000000000000, 'gas': 2000000, 'gasPrice': 50000000000}
Warning
不要使用本章生成的以太坊账户在公共/主以太坊网络上进行任何交易。本书中的示例没有为保护这些帐户提供足够的安全性。
作为`account_1`签署交易需要私钥文件和密码。在 JupyterLab 环境中,创建一个名为`pass1.txt`的文本文件,并用本章前面创建`account_1`时使用的密码填充该文件,这是在`genesis.json`配置的 alloc 部分中使用的第一个预注资帐户。此外,上传从`geth account new`命令生成的密钥文件(在本章前面执行以创建预注资以太坊账户)。命名密钥`account1.json`(见图 10-5 )。

图 10-5
以太坊账户私钥和密码
加载 account_1 的私钥和密码,并对之前创建的事务进行签名:
with open('pass1.txt', 'r') as pass_file: kf1_pass = pass_file.read().replace('\n', '')
with open("account1.json") as kf1_file: enc_key = kf1_file.read();
p_1 = w3.eth.account.decrypt(enc_key, kf1_pass) signed_tx = web3.eth.account.signTransaction(tx, p_1) signed_tx
示例输出:
AttributeDict({'rawTransaction': HexBytes('0xf86d0f850ba43b7400831e8480948ab8f3fc6c660d3f0b22490050c843cafd2c0aac880de0b6b3a7640000801ca0917ae987a8c808cf01221dad4571fd0b1b8f5429d13c469c72bc13647e9c1744a068507c8542ccdebb96e534d13a140ddcbdaedbfa3ba82dcbf86d4b196cc41b1f'), 'hash': HexBytes('0x9de62dc620274e2c9dba2194d90c245a933af8468ace5f2d38e802da09c06769'), 'r': 65802530150742945852115878650256413649726940478651153584824595116007827969860, 's': 47182743427096773798449059805443774712403275692049277894020390344384483433247, 'v': 28}
将签名的事务发送到事务节点,并检索结果哈希。这个哈希是以太坊区块链上交易的唯一标识符:
signed_tx = signed_tx.rawTransaction tx_hash = web3.eth.sendRawTransaction(signed_tx) web3.toHex(tx_hash)
示例输出:
'0x9de62dc620274e2c9dba2194d90c245a933af8468ace5f2d38e802da09c06769'
一个节点收到事务后,它会传播到所有节点进行验证,并包含到未决事务池中,准备好与下一个块一起挖掘。 <sup>33</sup> 下面的代码每秒查询一次连接的事务节点,直到事务返回一个块号:
%%time blockNumber = None check = 0
while type(blockNumber) is not int: check += 1 tx = web3.eth.getTransaction(tx_hash) blockNumber = tx.blockNumber clear_output(wait=True)
print(f"Check #{check}\n")
if type(blockNumber) is not int:
time.sleep(1)
tx
示例输出:
Check #12
CPU times: user 129 ms, sys: 904 μs, total: 130 ms Wall time: 11.1 s AttributeDict({'blockHash': HexBytes('0x676a24aa8117b51958031a2863b17f91ed3356276036a9de7c596124a6234986'), 'blockNumber': 8050, 'from': '0xFa4087D3688a289c9C92e773a7b46cb9CCf80353', 'gas': 2000000, 'gasPrice': 50000000000, 'hash': HexBytes('0xa3f02c685ff05b13b164afcbe11d2aa83d2dab3ff972ee7008cc931282587cee'), 'input': '0x', 'nonce': 16, 'to': '0x8ab8F3fc6c660d3f0B22490050C843cafd2c0AAC', 'transactionIndex': 0, 'value': 1000000000000000000, 'v': 28, 'r': HexBytes('0x89d052927901e8a7a727ebfb7709d4f9b99362c0f0001f62f37300ed17cb7414'), 's': HexBytes('0x3ea3b4f5f8e4c10e4f30cc5b8a7ff0a833d8714f20744c289dee86006af420c8')})
交易现已完成,其记录永久存储在私有区块链上。网络通过调整所需的难度,每 10 到 15 秒 <sup>34</sup> 尝试创建一个新的块; <sup>35</sup> 然而,这个只有三个矿工的资源受限网络可能会有很大的波动。本练习中的最后一个代码块查询事务节点中的最后 100 个数据块,并绘制数据块时间戳之间的时间增量:
df = pd.DataFrame(columns=['timestamp']) for i in range (0,100): block = web3.eth.getBlock(tx.blockNumber - i) df.loc[i] = [block.timestamp]
df['delta'] = df.timestamp.diff().shift(-1) * -1
df.reset_index().plot(x='index', y="delta", figsize=(12,5))
参见图 10-6 查看块时间戳增量图的输出示例。

图 10-6
块时间戳增量图
本节演示了在 Kubernetes 集群中与专用区块链网络的程序交互性。在 Kubernetes 集群中运行一个或多个以太坊节点,通过简化与现有和定制应用的互连,为扩展和利用区块链概念提供了大量机会。最后一部分部署无服务器功能,创建用于访问区块链数据的公共 API。
### 无服务器/OpenFaaS
本书在第九章中介绍了无服务器平台 OpenFaaS,并安装了预置的情感分析功能。本节构建并部署一个自定义函数,用于将公共 API 公开到私有区块链网络中。关于使用头盔的安装说明,请参见第九章。以下练习使用入口 URL [`https://faas.data.eth.apk8s.dev`](https://faas.data.eth.apk8s.dev) 。
登录并配置 faas-cli,以便在本地工作站上使用新的 eth 区块链集群:
(kubectl -n data get secret basic-auth -o jsonpath="{.data.basic-auth-password}" | base64 --decode)
$ export OPENFAAS_URL=faas.data.eth.apk8s.dev
OPENFAAS_URL
--password=$OPENFAAS_PASS
拉 OpenFaaS 函数模板`python3-http-debian`:
$ faas-cli template store pull python3-http-debian
创建目录`cluster-apk8s-eth/003-data/200-eth/functions`。在新的`functions`目录中,使用`the python3-http-debian`模板创建一个名为 last-block 的新 OpenFaaS 函数:
$ faas-cli new last-block --lang python3-http-debian
`faas-cli`命令创建了文件夹 last-block 和 yaml 文件`last-block.yml`。如果需要,在本地工作站上安装 Python 3。将目录切换到`last-block`并创建一个 Python 虚拟环境。这有助于生成一个`requirements.txt`,稍后用于配置带有所需 Python 包的 OpenFaaS 函数。最后,激活虚拟环境:
python3 -m venv venv $ source ./venv/bin/activate
安装 Python 包`hexbytes`和`web3`:
$ pip install hexbytes==0.2.0 web3==5.9.0
`pip`将把`hexbytes`和`web3`以及所有依赖包安装到虚拟环境中。新的虚拟环境仅包含运行该功能所需的包。使用`pip`在`requirements.txt`文件中创建所需包的列表:
$ pip freeze > requirements.txt
通过用清单 10-14 替换 handler.py 中的内容来创建函数。
#!/usr/bin/env python3 """ handler.py OpenFaaS Blockchain function returning the last block in the chain. """ import os import json import hexbytes from web3 import Web3
def handle(event, context): """ handle a request to the function """ ep_url = "http://eth-geth-tx:8545" ep = os.getenv('GETH_RPC_ENDPOINT', ep_url)
w3 = Web3(Web3.HTTPProvider(ep))
latest_block = w3.eth.getBlock('latest')
lbd = latest_block.__dict__
return {
"statusCode": 200,
"body": json.loads(
json.dumps(lbd, cls=CustomEncoder)
)
}
class CustomEncoder(json.JSONEncoder): """ CustomEncoder decodes HexBytes in Geth response dict. """
def default(self, o):
if isinstance(o, hexbytes.main.HexBytes):
return o.hex()
return json.JSONEncoder.default(self, o)
if name == 'main': """ Run code from command line for testing. Mock event and context. """
print(handle(event={}, context={}))
Listing 10-14OpenFaaS function for returning details on the last block in the Blockchain
通过在一个终端端口转发`eth-geth-tx`服务,并在另一个终端执行 Python 脚本`handler.py`,在本地工作站上测试新功能。打开一个单独的终端和端口转发`eth-geth-tx`:
$ kubectl port-forward svc/eth-geth-tx 8545:8545 -n data
从当前(启用虚拟环境)终端执行 Python 脚本`handler.py`:
python3 ./handler.py
示例输出:
{'statusCode': 200, 'body': {'difficulty': 471861, 'extraData': '0xd88301090d846765746888676f312e31342e32856c696e7578', 'gasLimit': 8000000, 'gasUsed': 0, 'hash': '0x8b4ebaca1d3606630c872cba9ccf4a968c43af24e02800b0a182ef89b149f08b', 'logsBloom': '0x000000000000000000000', 'miner': '0x284F99f929B49Da9D85b2a3dbF606Ed38EeC393E', 'mixHash': '0xd92b8eaa4a7f103f9c76bf7bf9b13b90271fed7f1f25c72f81e429f2108755bc', 'nonce': '0x1f110a5f5dc69827', 'number': 9107, 'parentHash': '0xd40382cd4c2e75cc919d11318672820aab10854951ee4ee137a08d97e84aa4c7', 'receiptsRoot': '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', 'sha3Uncles': '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', 'size': 538, 'stateRoot': '0xbe54d463bf9ffeda68975ff839eec7ecabd42c2f88cfc75372765891f43b1f18', 'timestamp': 1589012313, 'totalDifficulty': 3333037812, 'transactions': [], 'transactionsRoot': '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', 'uncles': []}}
接下来,`build`,`push`,`deploy`该功能。OpenFaaS CLI 使用 Docker 构建并推送函数的容器镜像到`last-block.yml`中自动配置的库;参见 OpenFaaS 配置选项<sup>?? 36自定义默认值。如果使用默认配置,请在本地工作站上安装 Docker,并注册一个免费的 Docker Hub <sup>37</sup> 帐户:</sup>
$ faas-cli build --build-arg ADDITIONAL_PACKAGE=gcc -f ./last-block.yml
faas-cli deploy -f ./last-block.yml
示例输出:
Deploying: last-block.
Deployed. 202 Accepted. URL: faas.data.eth.apk8s.dev/function/la…
最后,使用 web 浏览器或 curl 访问新的公共 last-block 函数:
$ curl faas.data.eth.apk8s.dev/function/la…
输出示例(截断):
{"difficulty":474599,"extraData":"0xd88301090d846765746888676f312e31342e32856c696e7578","gasLimit":8000000,"gasUsed":0,"hash":"0x8154d9edf431821a239fbb72bc2636304e254663b11cddc6987095d391f35248","logsBloom":"0x000…","miner":"0xcc7ADDFC03cb5ec2E3894583895C1bE385625c62","mixHash":"0xd20bf313f1834ec333d7d5cb2870b42487b462ee322aeccdd699fc017f86be51","nonce":"0x1a11af75e7ff468f","number":11694,"parentHash":"0x930d5f7340997cc1f0cd4be6e22aefe02c136c2e93b38ce75eff1815455f730d","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":538,"stateRoot":"0x51a5a7bb73dd5bd6ebb3a96606e115033e6b3bdcdb4a326b5a6c67718969f6cc","timestamp":1589048551,"totalDifficulty":4550348128,"transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]}
最后一个块函数代表了基于 Kubernetes 和 OpenFaaS 的应用开发的简单明了的演示。OpenFaaS 管理、监控和扩展功能。Kubernetes 将多种多样的技术结合在一起,如无服务器、区块链和基于网络的 ide(集成开发环境)。
## 摘要
本章安装了一个以太坊区块链网络,该网络由两个 Bootnode、一个 boot node 注册器、一个 Ethstats 仪表板、三个 miner 节点和两个事务节点组成(参见清单 10-15 ),所有这些节点都运行在一个三节点 Kubernetes 集群上。调整本章中的以太坊区块链网络以支持受保护的公共网络涉及暴露 bootnodes 和矿工以供外部第三方访问,并从以太坊标准工作证明共识协议转移到新团体,即权威证明。共享生产区块链网络的配置取决于业务目标和需求,因此超出了本书的范围。然而,演示的 Kubernetes 实现适用于许多类型的区块链和无服务器平台。
在 Kubernetes 内部实现区块链技术起初可能看起来违反直觉;这是一个技术本身不需要 Kubernetes 提供的任何功能的例子。然而,Kubernetes 在这里并不是作为运行区块链的解决方案,甚至不是无服务器技术。本书将 Kubernetes 作为一个标准化的统一平台,用于扩展数据管理、无服务器、数据科学和区块链平台,由统一存储、网络和控制面板支持,通过声明式配置实现。
本书的下一章也是最后一章涵盖了跨云和内部基础架构的数据科学和机器学习工作负载的管理。
./010-cluster-apk8s-eth/ ├── 000-cluster │ ├── 00-ingress-nginx │ ├── 10-cert-manager │ ├── 20-rook-ceph │ └── 30-monitoring ├── 003-data │ ├── 000-namespace │ ├── 005-keycloak │ ├── 100-jupyterhub │ ├── 120-openfaas │ └── 200-eth │ ├── 10-bootnode │ │ ├── 10-service.yml │ │ └── 30-deployment.yml │ ├── 20-bootnode-reg │ │ ├── 10-service.yml │ │ └── 30-deployment.yml │ ├── 30-ethstats │ │ ├── 10-service.yml │ │ ├── 15-secret.yml │ │ ├── 30-deployment.yml │ │ └── 50-ingress.yml │ ├── 40-miner │ │ ├── 15-secret.yml │ │ ├── 20-configmap.yml │ │ └── 30-deployment.yml │ ├── 50-tx │ │ ├── 10-service.yml │ │ └── 30-deployment.yml │ └── functions │ ├── build │ ├── last-block │ ├── last-block.yml │ └── template └── 005-data-lab
Listing 10-15Chapter 10 organization of Kubernetes-based Blockchain platform components
<aside aria-label="Footnotes" class="FootnoteSection" epub:type="footnotes">Footnotes 1
马尔伯纳德。"每个人都应该读一读比特币和加密货币的简史."福布斯。访问时间是 2020 年 5 月 12 日。 [`www.forbes.com/sites/bernardmarr/2017/12/06/a-short-history-of-bitcoin-and-crypto-currency-everyone-should-read/`](http://www.forbes.com/sites/bernardmarr/2017/12/06/a-short-history-of-bitcoin-and-crypto-currency-everyone-should-read/) 。
2
巴卡达斯,马丁。“在减半事件之前,比特币突破 10,000 美元;都铎-琼斯支持通货膨胀竞赛中“最快的马”街道。访问时间是 2020 年 5 月 12 日。 [`www.thestreet.com/investing/bitcoin-tops-10000-ahead-of-halving-tudor-jones-gives-ok`](http://www.thestreet.com/investing/bitcoin-tops-10000-ahead-of-halving-tudor-jones-gives-ok) 。
3
[`www.hyperledger.org/projects/fabric`](http://www.hyperledger.org/projects/fabric)
4
[`www.corda.net/`](http://www.corda.net/)
5
[`www.goquorum.com/`](http://www.goquorum.com/)
6
[`https://ethereum.org/`](https://ethereum.org/)
7
比特币新闻直播。“安永:以太坊可以为企业做很多事情”,2020 年 4 月 27 日。 [`www.livebitcoinnews.com/ernst-young-ethereum-can-do-a-lot-for-businesses/`](http://www.livebitcoinnews.com/ernst-young-ethereum-can-do-a-lot-for-businesses/) 。
8
[`https://github.com/ethereum/solidity`](https://github.com/ethereum/solidity)
9
[`https://geth.ethereum.org/`](https://geth.ethereum.org/)
10
[`https://github.com/ethereum/go-ethereum/blob/master/params/bootnodes.go`](https://github.com/ethereum/go-ethereum/blob/master/params/bootnodes.go)
11
[`https://geth.ethereum.org/docs/interface/peer-to-peer`](https://geth.ethereum.org/docs/interface/peer-to-peer)
12
[`https://blockgeeks.com/how-to-create-an-ethereum-account/`](https://blockgeeks.com/how-to-create-an-ethereum-account/)
13
[`https://github.com/cubedro/eth-netstats`](https://github.com/cubedro/eth-netstats)
14
[`https://en.wikipedia.org/wiki/Netcat`](https://en.wikipedia.org/wiki/Netcat)
15
[`https://devblogs.microsoft.com/cse/author/jason-poon/`](https://devblogs.microsoft.com/cse/author/jason-poon/)
16
[`https://github.com/jpoon/bootnode-registrar`](https://github.com/jpoon/bootnode-registrar)
17
[`https://github.com/helm/charts/tree/master/stable/ethereum`](https://github.com/helm/charts/tree/master/stable/ethereum)
18
[`https://devblogs.microsoft.com/cse/2018/06/01/creating-private-ethereum-consortium-kubernetes/`](https://devblogs.microsoft.com/cse/2018/06/01/creating-private-ethereum-consortium-kubernetes/)
19
[`https://github.com/cubedro/eth-netstats`](https://github.com/cubedro/eth-netstats)
20
[`https://imti.co/ethereum-ethstats/`](https://imti.co/ethereum-ethstats/)
21
[`https://github.com/ethereum/go-ethereum/wiki/Mining`](https://github.com/ethereum/go-ethereum/wiki/Mining)
22
[`https://geth.ethereum.org/docs/interface/mining`](https://geth.ethereum.org/docs/interface/mining)
23
[`https://geth.ethereum.org/downloads/`](https://geth.ethereum.org/downloads/)
24
婷,李婷婷李婷。“以太坊初学者指南(3)——解释创世纪文件,并用它来定制你的区块链。”中,2018 年 11 月 23 日。 [`https://medium.com/taipei-ethereum-meetup/beginners-guide-to-ethereum-3-explain-the-genesis-file-and-use-it-to-customize-your-blockchain-552eb6265145`](https://medium.com/taipei-ethereum-meetup/beginners-guide-to-ethereum-3-explain-the-genesis-file-and-use-it-to-customize-your-blockchain-552eb6265145) 。
25
[`https://github.com/ethereum/wiki/wiki/JSON-RPC`](https://github.com/ethereum/wiki/wiki/JSON-RPC)
26
[`https://github.com/ethereum/go-ethereum/wiki/Management-APIs`](https://github.com/ethereum/go-ethereum/wiki/Management-APIs)
27
[`https://coinsutra.com/smart-contracts/`](https://coinsutra.com/smart-contracts/)
28
[`www.investopedia.com/terms/1/51-attack.asp`](http://www.investopedia.com/terms/1/51-attack.asp)
29
[`https://blockonomi.com/proof-of-authority/`](https://blockonomi.com/proof-of-authority/)
30
[`https://geth.ethereum.org/docs/interface/private-network`](https://geth.ethereum.org/docs/interface/private-network)
31
[`https://github.com/ethereum/go-ethereum/wiki/JavaScript-Console`](https://github.com/ethereum/go-ethereum/wiki/JavaScript-Console)
32
[`https://github.com/ethereum/web3.py`](https://github.com/ethereum/web3.py)
33
默西,马赫什。"以太坊交易的生命周期."中等,2018 年 4 月 18 日。 [`https://medium.com/blockchannel/life-cycle-of-an-ethereum-transaction-e5c66bae0f6e`](https://medium.com/blockchannel/life-cycle-of-an-ethereum-transaction-e5c66bae0f6e) 。
34
[`https://etherscan.io/chart/blocktime`](https://etherscan.io/chart/blocktime)
35
西里瓦德纳,普拉巴。"块时间背后的秘密"中等,2018 年 7 月 8 日。 [`https://medium.facilelogin.com/the-mystery-behind-block-time-63351e35603a`](https://medium.facilelogin.com/the-mystery-behind-block-time-63351e35603a) 。
36
[`https://docs.openfaas.com/reference/yaml/`](https://docs.openfaas.com/reference/yaml/)
37
[`https://hub.docker.com/`](https://hub.docker.com/)
</aside>