我们团队维护着一个规模不小的、基于物理机部署的Hadoop集群。安全审计提出的新要求是:所有核心组件之间的数据传输必须强制加密,尤其是NameNode与DataNode之间的RPC调用,以及YARN ResourceManager与NodeManager之间的通信。传统的解决方案是启用Hadoop自身的Kerberos + RPC SASL加密,但任何经历过Hadoop Kerberos配置与运维的人都清楚,那是一场关于principal、keytab和复杂配置文件的噩梦,尤其是在一个已经运行了多年的庞大集群上进行改造,风险和工作量都难以估量。
团队内部开始探讨,能否用云原生的思路来解决这个“遗留系统”的问题。服务网格技术,特别是其透明化的mTLS加密能力,进入了我们的视野。多数服务网格方案(如Istio)与Kubernetes深度绑定,但我们的Hadoop集群是纯粹的VM/物理机环境。HashiCorp的Consul,凭借其平台无关的Agent模式,成为了最可行的候选者。
目标很明确:利用Consul Connect作为旁路组件,为Hadoop核心服务(HDFS, YARN)提供服务发现和自动mTLS加密,同时对Hadoop应用程序做到最小化侵入,彻底绕开Kerberos。
初步构想与架构设计
核心思路是为每个Hadoop守护进程(Daemon)部署一个Consul Agent,并为其启动一个Envoy Sidecar代理。Hadoop进程本身不直接相互通信,而是将所有出站流量发送到本地的Sidecar,由Sidecar负责建立mTLS连接并将流量转发到目标服务的Sidecar,最终再由目标Sidecar将解密后的流量发送给目标Hadoop进程。
这将形成如下的数据流:
DataNode进程 -> 本地Envoy Proxy -> mTLS加密隧道 -> NameNode节点的Envoy Proxy -> NameNode进程
这个方案的成败关键在于两点:
- Hadoop组件能否被“欺骗”,将流量指向本地代理而不是远程节点。
- 如何为非云原生的、多端口的Hadoop服务正确配置Consul服务定义和代理。
我们的实验环境由五台虚拟机组成:
-
consul-server-1
,consul-server-2
,consul-server-3
: Consul集群服务端。 -
hadoop-master
: 运行HDFS NameNode和YARN ResourceManager。 -
hadoop-worker-1
: 运行HDFS DataNode和YARN NodeManager。
graph TD subgraph VM: hadoop-worker-1 DN[DataNode Process] -- localhost:16001 --> DNP[Envoy Proxy]; NM[NodeManager Process] -- localhost:16002 --> NMP[Envoy Proxy]; CAW[Consul Agent]; DNP -- Manages --> DN; NMP -- Manages --> NM; CAW -.-> DNP; CAW -.-> NMP; end subgraph VM: hadoop-master NN[NameNode Process]; RM[ResourceManager Process]; NNP[Envoy Proxy] -- localhost:8020 --> NN; RMP[Envoy Proxy] -- localhost:8132 --> RM; CAM[Consul Agent]; NNP -- Manages --> NN; RMP -- Manages --> RM; CAM -.-> NNP; CAM -.-> RMP; end DNP <== mTLS over Wire ==> NNP; NMP <== mTLS over Wire ==> RMP; style DN fill:#f9f,stroke:#333,stroke-width:2px; style NN fill:#f9f,stroke:#333,stroke-width:2px; style NM fill:#ccf,stroke:#333,stroke-width:2px; style RM fill:#ccf,stroke:#333,stroke-width:2px;
步骤一:部署Consul集群
这一步相对直接。在三台consul-server
节点上,我们创建配置文件 consul.hcl
。
// /etc/consul.d/consul.hcl on consul-server-1
// 生产环境中,IP地址应为内网固定IP
server = true
bootstrap_expect = 3
client_addr = "0.0.0.0"
bind_addr = "192.168.10.11" // 当前服务器的私有IP
ui_config {
enabled = true
}
data_dir = "/opt/consul"
enable_local_script_checks = true // 允许本地脚本健康检查
// 用于自动加入集群
retry_join = ["192.168.10.11", "192.168.10.12", "192.168.10.13"]
在三台服务器上分别修改bind_addr
后启动Consul:
# 在所有consul-server节点上执行
sudo consul agent -config-file=/etc/consul.d/consul.hcl
通过 consul members
确认集群状态正常。
接下来,在 hadoop-master
和 hadoop-worker-1
上安装并配置Consul Agent作为客户端。
// /etc/consul.d/consul.hcl on hadoop nodes
client_addr = "0.0.0.0"
bind_addr = "192.168.10.21" // 当前hadoop-master的私有IP
data_dir = "/opt/consul"
enable_local_script_checks = true
// 加入Consul Server集群
retry_join = ["192.168.10.11", "192.168.10.12", "192.168.10.13"]
启动客户端Agent:
sudo consul agent -config-file=/etc/consul.d/consul.hcl
步骤二:为NameNode和ResourceManager注册服务
这是第一个真正的挑战。Hadoop的组件通常监听多个端口,我们需要将这些端口都代理起来。我们选择为每个核心服务(如HDFS-NN, YARN-RM)定义一个独立的Consul服务,而不是将所有服务塞进一个定义。
在 hadoop-master
节点上,创建服务定义文件 /etc/consul.d/hadoop-master-services.hcl
:
// /etc/consul.d/hadoop-master-services.hcl
service {
name = "namenode-rpc"
id = "namenode-rpc-1"
port = 8020 // NameNode RPC的实际监听端口
// 健康检查至关重要,这里用一个简单的脚本检查进程是否存在
check {
id = "namenode-process-check"
name = "Check NameNode Process"
script = "pgrep -f 'proc_namenode' > /dev/null"
interval = "10s"
timeout = "2s"
}
// 这是Consul Connect的核心配置
connect {
sidecar_service {
// Sidecar代理将监听在18020端口,并将流量转发给本地的8020端口
port = 18020
proxy {
// 配置上游依赖。NameNode需要与DataNode通信
upstreams {
destination_name = "datanode-ipc"
local_bind_port = 25001 // DataNode IPC服务的本地监听端口
}
upstreams {
destination_name = "datanode-http"
local_bind_port = 25002 // DataNode HTTP服务的本地监听端口
}
}
}
}
}
service {
name = "resourcemanager-scheduler"
id = "resourcemanager-scheduler-1"
port = 8030 // RM Scheduler的实际端口
check {
id = "resourcemanager-process-check"
name = "Check ResourceManager Process"
script = "pgrep -f 'proc_resourcemanager' > /dev/null"
interval = "10s"
timeout = "2s"
}
connect {
sidecar_service {
port = 18030
// RM不需要主动连接其他服务,所以upstreams为空
}
}
}
这里的坑在于:
-
port
字段必须是Hadoop进程实际监听的端口。 -
sidecar_service
内部的port
字段是Envoy代理为该服务监听的入站端口,其他服务的Sidecar将连接到这个端口。 -
upstreams
定义了当前服务需要访问哪些其他服务。destination_name
是目标Consul服务名,local_bind_port
是在本地为这个上游连接创建的监听端口。Hadoop进程将通过连接localhost:local_bind_port
来访问目标服务。
重载Consul配置以注册服务:
consul reload
现在,为这两个服务启动Sidecar代理:
# 为NameNode启动代理
consul connect envoy -sidecar-for namenode-rpc-1 -admin-bind localhost:19001 &
# 为ResourceManager启动代理
consul connect envoy -sidecar-for resourcemanager-scheduler-1 -admin-bind localhost:19002 &
-sidecar-for
参数告诉Envoy它在为哪个Consul服务ID工作,Consul会据此动态生成Envoy的配置。
步骤三:为DataNode和NodeManager注册服务
在 hadoop-worker-1
节点上,创建 /etc/consul.d/hadoop-worker-services.hcl
。DataNode是配置中最复杂的部分,因为它既是其他服务(如NameNode)的上游,也需要连接到其他服务(NameNode)。
// /etc/consul.d/hadoop-worker-services.hcl
service {
// DataNode的IPC服务,供NameNode连接
name = "datanode-ipc"
id = "datanode-ipc-worker-1"
port = 9867
check {
id = "datanode-process-check"
name = "Check DataNode Process"
script = "pgrep -f 'proc_datanode' > /dev/null"
interval = "10s"
timeout = "2s"
}
connect {
sidecar_service {
port = 19867 // 其他服务连接此代理端口
}
}
}
service {
// DataNode的HTTP服务
name = "datanode-http"
id = "datanode-http-worker-1"
port = 9864
check {
id = "datanode-http-check"
name = "Check DataNode HTTP Port"
tcp = "localhost:9864"
interval = "10s"
timeout = "2s"
}
connect {
sidecar_service {
port = 19864
}
}
}
service {
// 这是一个"虚拟"服务,仅用于定义DataNode进程的出站连接
// 它不监听任何端口,只是为了让Consul Connect为DataNode进程管理上游
name = "datanode-client"
id = "datanode-client-worker-1"
connect {
sidecar_service {
proxy {
upstreams {
destination_name = "namenode-rpc"
local_bind_port = 28020 // DataNode将通过localhost:28020连接NameNode
}
}
}
}
}
service {
name = "nodemanager"
id = "nodemanager-worker-1"
port = 8041 // NodeManager的实际端口
check {
id = "nodemanager-process-check"
name = "Check NodeManager Process"
script = "pgrep -f 'proc_nodemanager' > /dev/null"
interval = "10s"
timeout = "2s"
}
connect {
sidecar_service {
port = 18041
proxy {
upstreams {
destination_name = "resourcemanager-scheduler"
local_bind_port = 28030 // NodeManager通过此端口连接RM
}
}
}
}
}
设计抉择:
我们为DataNode创建了一个名为 datanode-client
的虚拟服务。这是因为DataNode进程本身需要发起对NameNode的出站连接,但它本身没有一个单一的服务身份来承载这个upstreams
配置。通过这个虚拟服务,我们可以启动一个专门管理DataNode出站流量的Sidecar。
在worker节点上启动所有代理:
consul reload
consul connect envoy -sidecar-for datanode-ipc-worker-1 &
consul connect envoy -sidecar-for datanode-http-worker-1 &
consul connect envoy -sidecar-for datanode-client-worker-1 &
consul connect envoy -sidecar-for nodemanager-worker-1 &
步骤四:修改Hadoop配置,将流量重定向至Sidecar
这是整个方案中最具侵入性但也是最关键的一步。我们需要修改 core-site.xml
和 yarn-site.xml
,让Hadoop组件连接本地的Sidecar代理端口,而不是直接连接远程服务。
修改 hadoop-master
上的 /opt/hadoop/etc/hadoop/core-site.xml
:
<configuration>
<property>
<name>fs.defaultFS</name>
<!--
重要变更: 客户端和组件现在连接到NameNode RPC的本地代理端口。
注意:这里的地址是NameNode节点自己为namenode-rpc上游配置的本地监听端口。
然而,由于NameNode本身就是namenode-rpc服务,它不需要通过upstream访问自己。
这里的配置主要是给集群中的其他客户端使用,例如HDFS CLI。
为了统一,DataNode将连接它自己本地的代理端口。
-->
<value>hdfs://localhost:28020</value>
<!-- 假设DataNode上的namenode-rpc上游监听在28020 -->
</property>
</configuration>
修改 hadoop-worker-1
上的 /opt/hadoop/etc/hadoop/core-site.xml
:
<configuration>
<property>
<name>fs.defaultFS</name>
<!--
关键: DataNode通过连接它本地的Sidecar代理来访问NameNode。
这个端口号(28020)是在datanode-client服务的upstreams中定义的。
-->
<value>hdfs://localhost:28020</value>
</property>
</configuration>
修改 hadoop-worker-1
上的 /opt/hadoop/etc/hadoop/yarn-site.xml
:
<configuration>
<property>
<name>yarn.resourcemanager.hostname</name>
<!--
关键: NodeManager不再直接连接RM的主机名,
而是连接到为resourcemanager-scheduler上游配置的本地代理端口。
这个端口号(28030)是在nodemanager服务的upstreams中定义的。
-->
<value>localhost</value>
</property>
<property>
<name>yarn.resourcemanager.scheduler.address</name>
<value>${yarn.resourcemanager.hostname}:28030</value>
</property>
</configuration>
在修改完所有相关节点的配置后,需要重启HDFS和YARN服务,使新配置生效。
步骤五:配置Consul Intentions并验证
默认情况下,Consul Connect会拒绝所有服务间的通信(零信任网络)。我们需要明确定义哪些服务可以相互通信。
# 允许datanode-client(代表DataNode进程)访问namenode-rpc
consul intention create -allow datanode-client namenode-rpc
# 允许NameNode访问DataNode的IPC和HTTP服务
consul intention create -allow namenode-rpc datanode-ipc
consul intention create -allow namenode-rpc datanode-http
# 允许nodemanager访问resourcemanager-scheduler
consul intention create -allow nodemanager resourcemanager-scheduler
此时,如果一切顺利,Hadoop集群应该能正常工作。在 hadoop-worker-1
上执行 hdfs dfs -ls /
,命令应该成功返回。
验证加密:
在 hadoop-worker-1
上使用 tcpdump
抓取物理网卡上的包:
sudo tcpdump -i eth0 -A host hadoop-master and port 18020
你会看到输出全是加密的TLS流量,没有任何明文的Hadoop RPC协议内容。
同时,使用 lsof
可以验证进程连接:
# 在hadoop-worker-1上
sudo lsof -i -P -n | grep java
输出会显示,DataNode的Java进程正在连接 localhost:28020
,而不是 hadoop-master:8020
。这证明流量确实经过了本地Sidecar。
现在,如果我们删除一条intention规则:
consul intention delete datanode-client namenode-rpc
稍等片刻,再次执行 hdfs dfs -ls /
,命令会卡住然后超时失败。这有力地证明了Consul Connect的mTLS策略正在生效。
遗留问题与未来展望
这个方案成功地为存量的、非容器化的Hadoop集群提供了一层透明的mTLS加密,极大地简化了安全配置。在真实项目中,这种方式避免了对现有Kerberos体系的依赖和改造,显著降低了运维复杂性。
但它并非银弹,还存在一些局限性和需要进一步考虑的问题:
- 性能开销:引入两层Envoy代理(源端和目的端)必然会带来额外的网络延迟和CPU消耗。对于RPC密集型和数据密集型的Hadoop作业,需要进行详尽的性能压测来评估其影响。
- 生命周期管理:当前我们是手动启动和管理Sidecar进程。在生产环境中,需要使用Systemd或其他进程管理工具来确保Sidecar与对应的Hadoop守护进程同生命周期,并在Hadoop进程启动前确保Sidecar已就绪。
- 配置复杂性:虽然避免了Kerberos,但引入了Consul服务定义和Intention配置。对于非常庞大的集群,管理这些HCL文件和Sidecar也需要自动化工具支持。
- 覆盖范围:此方案主要覆盖了Hadoop核心组件间的通信。Hadoop生态系统中的其他组件,如Hive Server2, Presto, HBase等,也需要进行类似的改造,工作量不容小觑。
- 高可用性:对于HDFS NameNode HA场景,需要更复杂的Consul服务定义和上游配置来处理主备切换,这需要结合Consul的健康检查和DNS功能来实现动态路由。