安全无小事,我们常常要为了预防安全问题而付出大量的代价。虽然小区楼道里面的灭火器、消防栓常年没人用,但是我们还是要准备着。我们之所以愿意为了这些小概率事件而付出巨大的成本,是因为安全问题一旦发生,很多时候我们将无法承担它带来的后果。
在软件行业,安全问题尤其突出,因为无法预料的事情实在太多了。软件的复杂性让我们几乎无法完全扫清安全问题,模块 A 独立运行可能没问题,但是一旦和模块 B 一起工作也许就产生了安全问题。
不可否认为了让软件更安全,我们引入了很多复杂的机制。不少人开发者也抱怨为了进行安全处理而做了太多额外的事情。在一个复杂的分布式软件 Hadoop 中,我们为此付出的成本将更大。比如,我们可能可以比较轻松的搭建一个无安全机制的集群,但是一旦需要支持安全机制的时候,我们可能会付出额外几倍的时间来进行各种复杂的配置和调试。
Hadoop 在开始的几个版本中其实并没有安全机制的支持,后来 Yahoo 在大规模应用 Hadoop 之后,安全问题也就日益明显起来。大家都在一个平台上面进行操作是很容易引起安全问题的,比如一个人把另一个人的数据删除了,一个人把另一个人正在运行的任务给停掉了,等等。在当今的企业应用里面,一旦我们的数据开始上规模之后,安全机制的引入几乎是必然的选择。所以作为大数据领域的开发者,理解 Hadoop 的安全机制就显得非常重要。
Hadoop 的安全机制现在已经比较成熟,网上关于它的介绍也很多,但相对较零散,下面我将尝试更系统的,并结合实例代码,给大家分享一下最近一段时间关于 Hadoop 安全机制的学习所得,抛个砖。
预计将包括这样几个方面:
做 Web 开发的同学们可能比较熟悉的认证机制是 JWT
,近两年 JWT
的流行几乎让其成为了实现单点登录的一个标准。JWT
将认证服务器认证后得到的 token
及一定的用户信息经过 base64
编码之后放到 HTTP 头中发送给服务器端,得益于 token
的加密机制(一般是非对称加密),服务器端可以在不连接认证服务器就进行 token
验证(第一次验证时会向认证服务器请求公钥),从而实现高性能的鉴权。这里的 token
虽然看起来不可读,实际上我们经过简单的解码就能得到 token
的内容。所以 JWT
一般是要结合 HTTPS
一起应用才能带来不错的安全性。
JWT
看起来还不错呀,安全模型比较简单,能不能直接用在 Hadoop
上面呢?可能可以。但是由于 Hadoop
的出现早于 JWT
太多,所以当时的设计者们是不可能考虑使用 JWT
的。实际上 JWT
主要是针对 web 的场景设计的,对于分布式场景中,很多问题它是没有给出答案的。一些典型的场景比如服务间的认证该如何实现,如何支持其他的协议,等等。Hadoop
的安全认证使用的是 Kerberos
机制。相比 JWT
,Kerberos
是一个更为完整的认证协议,然而也正是因为其设计可以支持众多的功能,也给其理解和使用带来了困难。
这里之所以提到 JWT
,是因为 JWT
实际上可以看成是 Kerberos
协议的一个极简版本。JWT
实现了一部分 Kerberos
的功能。如果我们能对于 JWT
的认证机制比较熟悉,那么对于 Kerberos
机制的理解应当是有较大帮助的。
Kerberos
协议诞生于 MIT 大学,早在上世纪 80 年代就被设计出来了,然后经过了多次版本演进才到了现在我们用的 V5 版本。作为一个久经考验的安全协议,Kerberos
的使用其实是非常广泛的,比如 Windows
操作系统的认证就是基于 Kerberos
的,而 Mac
Red Hat Enterprise Linux
也都对于 Kerberos
有完善的支持。各种编程语言也都有内置的实现。对于这样一个重要的安全协议,就算我们不从事大数据相关的开发,也值得好好学习一下。
Kerberos
设计的有几个大的原则:
那么这个协议是如何工作的呢?与 JWT
类似,Kerberos
同样定义了一个中心化的认证服务器,不过对于这个认证服务器,Kerberos
按照功能进一步将其拆分为了三个组件:认证服务器(Authentication Server,AS)、密钥分发中心(Key Distribution Center,KDC)、票据授权服务器(Ticket Granting Server,TGS)。在整个工作流程中,还有两个参与者:客户端 (Client) 和服务提供端 (Service Server,SS)。
Kerberos
大体上的认证过程与 JWT
一致:第一步是客户端从认证服务器拿到 token
(这里的术语是 Ticket
,下文将不区分这两个词,请根据上下文理解);第二步是将这个 token
发往服务提供端去请求相应的服务。
下图是整个认证过程中各个组件按顺序相互传递的消息内容,在阅读整个流程之前,有几点提需要注意:
看了这个复杂的流程,大家心里应该有很多疑惑。整个通信过程传递了很多的消息,消息被来来回回加密了很多次,真的是有必要的吗?背后的原因是什么呢?事实上,我们结合上面提到的几个设计原则来看,问题就会相对清晰一些。
虽然整个通信过程涉及到的消息很多,但是我们仔细思考就可以发现这几条规律:
JWT
一样,通信过程生成有效时间比较短的会话秘钥用于通信JWT
一样,认证服务器无需存储会话秘钥,各个参与方(Client/SS)可以独立进行消息验证,从而实现高性能。这也是虽然消息 B 和 E 不能被 Client 解密,但是还是会发往 Client,然后再由 Client 回发的原因Kerberos
并没有对 Client
和 SS
之间的通信协议进行限制,虽然和认证服务器进行通信需要基于 TCP/UDP
,但 Client
和 SS
通信可以用任意协议进行理解了上述通信流程之后,可以看到,相比 JWT
,Kerberos
还进行了下面的额外验证:
Kerberos
要求各个组件进行时间同步的原因除了上面这些安全验证,其实 Kerberos
还支持免密码输入的登录,我们可以将用户的秘钥(并非真正的密码,由真正的密码 hash 生成)生成到一个 keytab
格式的文件中,这样在第一步中,就可以由用户提供 ID (principal) 及 keytab
文件来完成了。
虽然 Kerberos
可以支持多种场景的认证,但是由于其协议设计比较复杂,在使用上会给我们带来不少的困难。比如我们需要提前为各个组件生成独立的秘钥,一般要求每个服务器都不一样,与不同的主机绑定,这就给我们部署服务带来了挑战,特别是在当前微服务、云原生应用、容器、k8s 比较流行的时候。
为了更清晰的看到整个通信的过程,我们可以动手实践一下看看
然后安装配置 kdc 并生成相关的秘钥:
# 将kdc kdc.hadoop.com加入hosts,以便后续进行基于hosts文件的主机名解析
yum install net-tools -y
ip_addr=$(ifconfig ens33 | grep inet | awk '{print $2}')
#这里主要的作用就是写一个本地的host的ip地址映射,如上图
echo "$ip_addr kdc-server kdc-server.hadoop.com" >> /etc/hosts
# 安装相关软件并进行配置
yum install krb5-server krb5-libs krb5-workstation -y
# 创建krb5配置文件,详细配置解释请参考:https://web.mit.edu/kerberos/krb5-1.12/doc/admin/conf_files/krb5_conf.html
cat > /etc/krb5.conf < /var/kerberos/krb5kdc/kdc.conf <
操作完以后查看下端口
将生成的 keytab 文件下载到本地,然后就可以进行测试了。编写测试的客户端和服务端代码如下:
准备对应的文件
sz /etc/krb5.conf
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.Oid;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Test {
public static class TestClient {
private String srvPrincal;
private String srvIP;
private int srvPort;
private Socket socket;
private DataInputStream inStream;
private DataOutputStream outStream;
public TestClient(String srvPrincal, String srvIp, int srvPort) throws Exception {
this.srvPrincal = srvPrincal;
this.srvIP = srvIp;
this.srvPort = srvPort;
this.initSocket();
this.initKerberos();
}
private void initSocket() throws IOException {
this.socket = new Socket(srvIP, srvPort);
this.inStream = new DataInputStream(socket.getInputStream());
this.outStream = new DataOutputStream(socket.getOutputStream());
System.out.println("Connected to server: " + this.socket.getInetAddress());
}
private void initKerberos() throws Exception {
System.setProperty("java.security.krb5.conf", "src/main/krb5.conf");
System.setProperty("java.security.auth.login.config", "src/main/gml.keytab");
System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");
System.setProperty("sun.security.krb5.debug", "true");
System.out.println("init kerberos: set up objects as configured");
GSSManager manager = GSSManager.getInstance();
Oid krb5Oid = new Oid("1.2.840.113554.1.2.2");
GSSContext context = manager.createContext(
manager.createName(srvPrincal, null),
krb5Oid, null, GSSContext.DEFAULT_LIFETIME);
context.requestMutualAuth(true);
context.requestConf(true);
context.requestInteg(true);
System.out.println("init kerberos: Do the context establishment loop");
byte[] token = new byte[0];
while (!context.isEstablished()) {
// token is ignored on the first call
token = context.initSecContext(token, 0, token.length);
// Send a token to the server if one was generated by initSecContext
if (token != null) {
System.out.println("Will send token of size " + token.length + " from initSecContext.");
outStream.writeInt(token.length);
outStream.write(token);
outStream.flush();
}
// If the client is done with context establishment then there will be no more tokens to read in this loop
if (!context.isEstablished()) {
token = new byte[inStream.readInt()];
System.out.println(
"Will read input token of size " + token.length + " for processing by initSecContext");
inStream.readFully(token);
}
}
System.out.println("Context Established! ");
System.out.println("Client is " + context.getSrcName());
System.out.println("Server is " + context.getTargName());
}
public void sendMessage() throws Exception {
// Obtain the command-line arguments and parse the port number
String msg = "Hello Server ";
byte[] messageBytes = msg.getBytes();
outStream.writeInt(messageBytes.length);
outStream.write(messageBytes);
outStream.flush();
byte[] token = new byte[inStream.readInt()];
System.out.println("Will read token of size " + token.length);
inStream.readFully(token);
String s = new String(token);
System.out.println(s);
System.out.println("Exiting... ");
}
public static void main(String[] args) throws Exception {
TestClient client = new TestClient("root/[email protected]", "localhost", 9112);
client.sendMessage();
}
}
public static class TestServer {
private int localPort;
private ServerSocket ss;
private Socket socket = null;
public TestServer(int port) {
this.localPort = port;
}
public void receive() throws IOException, GSSException {
this.ss = new ServerSocket(localPort);
socket = ss.accept();
DataInputStream in = new DataInputStream(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
this.initKerberos(in, out);
int length = in.readInt();
byte[] token = new byte[length];
System.out.println("Will read token of size " + token.length);
in.readFully(token);
String s = new String(token);
System.out.println("Receive Client token: " + s);
byte[] token1 = "Receive Client Message".getBytes();
out.writeInt(token1.length);
out.write(token1);
out.flush();
}
private void initKerberos(DataInputStream in, DataOutputStream out) throws GSSException, IOException {
GSSManager manager = GSSManager.getInstance();
GSSContext context = manager.createContext((GSSCredential) null);
byte[] token;
while (!context.isEstablished()) {
token = new byte[in.readInt()];
System.out.println("Will read input token of size " + token.length + " for processing by acceptSecContext");
in.readFully(token);
token = context.acceptSecContext(token, 0, token.length);
// Send a token to the peer if one was generated by acceptSecContext
if (token != null) {
System.out.println("Will send token of size " + token.length + " from acceptSecContext.");
out.writeInt(token.length);
out.write(token);
out.flush();
}
}
System.out.println("Context Established! ");
System.out.println("Client is " + context.getSrcName());
System.out.println("Server is " + context.getTargName());
}
public static void main(String[] args) throws IOException, GSSException {
System.setProperty("java.security.krb5.conf", "src/main/krb5.conf");
System.setProperty("java.security.auth.login.config", "src/main/server.conf");
System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");
System.setProperty("sun.security.krb5.debug", "true");
TestServer server = new TestServer(9112);
server.receive();
}
}
}
先运行 Server
程序,再运行 Client
程序,我们将能从输出内容中看到整个通信的过程。
当前 web 应用成为主流的时候,Kerberos
如何在 HTTP/HTTPS
协议场景下使用呢?我们又要如何配置,才能运行一套支持认证的 Hadoop 集群呢?
我们分析了 Kerberos
协议的设计和通信过程。可以了解到,Kerberos
主要实现了不在网络传输密码的同时又能在本地进行高性能鉴权。
Kerberos
协议回顾假设有三个组件 A B C,A 想和 C 进行安全通信,而 B 作为一个认证中心保存了认证信息。那么以以下的方式进行通信就可以做到安全:
整个过程,A 无需知道 C 的密码,C 也无需知道 A 的密码就可以完成安全通信。
这里的安全性我们可以从以下几个方面来看:
如果消息被截获
当第一步中的消息被截获:这里的消息用 A 的秘钥加密了,截获也无法解密
当第二步中的消息被截获:这里的消息用 A 的秘钥加密了,截获也无法解密
当第三步中的消息被截获:这里的消息用 C 的秘钥加密了,截获也无法解密
当第四步中的消息被截获:这里的消息分别用会话秘钥、C 的秘钥加密了,截获也无法解密
当第五步中的消息被截获:这里的消息用会话秘钥,截获也无法解密
如果 A 是一个攻击方(某一个有权限的用户想要提权)
他只能拿到自己的秘钥,而无法获取 B 或 C 的秘钥,他不能随意生成一个加密消息发给 C 请求服务(冒充其他用户),因为他无法伪造有会话密码而又用 C 的秘钥加密的消息
如果 C 是一个攻击方(欺骗某个有权限的用户)
他无法解密第三步中的消息,所以无法解密 A 的消息,从而也就无从提供服务
如果 B 是一个攻击方
他无法解密 A 的消息,从而无法提供服务
如果传输的消息被破解(任何加密都是可以被破解的,只是时间的问题)
由于整个通信过程由会话秘钥来加密,会话秘钥的有效期通常比较短,当消息被破解之后,攻击者也不能利用破解得到的秘钥去破解后续的消息
从这几个方面来看,这个协议都是比较安全的。
以上的安全通信步骤是 kerberos
安全的核心机制,A 对应文章中的 Client
,B 对应文章中的 TGS
,C 对应文章中的 SS
。
但 kerberos
还引入了一个 AS 的组件,这主要为了提高性能和扩展性。
有了 AS
之后,我们可以将整个通信看成两个上述 ABC 通信模式的重复。第一个通信模式 A 对应文章中的 Client
,B 对应文章中的 AS
,C 对应文章中的 TGS
,为了实现 Client
和 TGS
的安全通信。第二个通信模式 A 对应文章中的 Client
,B 对应文章中的 TGS
,C 对应文章中的 SS
,为了实现 Client
和 SS
的安全通信。
为什么有了两次通信模式之后,就能提高性能和扩展性呢?实际上一般我们可以将 Client/TGS
的会话秘钥有效期配置得更长一些,而将 Client/SS
的会话秘钥有效期配置得比较短。由于一旦我们有一个有效的 TGT
及 Client/TGS
会话秘钥,在这个秘钥的有效期内,我们无需再访问 AS
去生成新的会话秘钥。当 Client/TGS
会话秘钥有效期较长的时候,我们就可以较少的访问 AS
,从而将 AS
这一第一入口服务的负载降低。而 TGS
由于需要经常参与秘钥生成,它的负载会相对较高,这里我们就可以将 TGS
扩展到多台服务器来支撑大的负载。AS
可以给 Client
提供一个有效的 TGS
地址,从而实现 TGS
的分布式扩展。
Kerberos
协议发展GSS API
Kerberos
协议本身只是提供了一种安全认证和通信的手段,要应用这个协议,我们需要一套 API
接口。在具体实现的时候,每个人都会写出不一样的代码,从而产生不同的 API
。这可不是好事,对于应用方而言,不仅仅学习成本高,而且系统迁移能力差,比如换一个 Kerberos
服务器可能就会出现兼容性问题。就像 windows
上面的换行用 \r\n
,而 unix
类操作系统用 \n
,这给每一个开发者都带来了麻烦。
所以,在具体的工程应用时,一种通用的 API
就变得非常重要。这就是 GSS API
,其全称是 The Generic Security Services Application Program Interface,即通用安全服务应用程序接口。这套 API
在设计的时候其实不仅仅考虑了对于 Kerberos
的支持,还考虑了支持其他的协议,所以称为通用接口。由于我们总是会发展出其他的安全协议的,抽象一套可以长期保持不变的通用的 API
接口,就可以避免应用层进行修改。这一套 API
接口就是在上一篇文章中我们用到的接口了。
从 GSS API
接口来看,我们的认证过程可以抽象为这样几个简单的步骤:
Context
上下文用来保存数据 -> 通过 initSecContext
获取一个 token
-> 将 token
发送给服务器 -> 等待服务器回发的用于通信的 token
Context
上下文用来保存数据 -> 读取客户端发来的 token
-> 验证 token
,并(可能)生成一个新的用于通信的 token
-> 将 token
发给客户端这里的认证过程简单到甚至没有出现认证服务器,基于这样的一套通用 API
去实现其他应用就相对轻松多了。Kerberos
内部的通信细节,多次传输的各种密文全部都隐藏在这样的 API
实现中。具体的 GSS API
使用代码示例。
由于 GSS API
设计可以支持多种安全协议,另一个想法会自然的冒出来。我们可以让服务器支持多种认证协议,然后具体用哪种,由客户端和服务器端协商决定。这就使得我们在开发应用时可以给最终的用户提供选择,便于使用他或她所偏好使用的认证方式,从而带来更好的用户体验。同时,服务器和客户端在各自实现时,也可以相互独立的增量式的添加或去掉对于某一具体协议的支持,而不用完全同步的进行修改。这对于同一个服务器要支持多个版本的客户端而言会很有用。
这就是 SPNEGO
了,其全称是 Simple and Protected GSSAPI Negotiation Mechanism,即基于 GSS API
实现的一套简单的协议协商机制。这一协议由微软最早提出并应用在 windows 操作系统中,与我们最贴近的应用,当属于浏览器的系统集成认证了。大家回忆一下我们使用 IE 浏览器的体验,可以发现,很多网站可以直接使用系统的域账户登录。这就是用 SPNEGO
协议实现的浏览器系统集成认证。在企业中,如果我们为所有员工配置了 windows
域账户,而当我们有一些基于 web 的企业应用需要认证时,就可以利用这一机制实现无感知的认证。其实不只是 IE 浏览器,Firefox
Chrome
等主流浏览器基本上都实现了这样的系统集成登录机制。
这个协议的通信过程大致为:
Client
向 Server
请求服务Server
检查 Client
是否有提供有效的认证信息:如果没有,返回消息(包括服务器支持的认证方式)给 Client
,以便 Client
可以完成认证;如果有,就提供服务Client
完成认证之后,向 Server
请求服务,并带上认证信息介绍了这么多,其实都是为了我们分析 Hadoop
的认证机制实现。到这里,相信大家应该也猜到了,在 Hadoop
的认证中,各个节点的通信实际上使用的就是 GSS API
去实现的基于 Kerberos
协议的单点认证。而 Hadoop
对外提供的很多基于 web 的应用,比如 Web HDFS、统计信息页面、Yarn Application 管理等等,其认证都是基于 SPNEGO
协议的。这两个协议的配置其实在我们后续配置 Hadoop
认证时也是最主要的配置了。
(下面的内容请大家结合源代码一起分析,仅仅读文字可能有很多内容会难以理解)
Kerberos
实现我们打开 OpenJDK
的源代码库,浏览到下面这里的代码:
这里的代码量还是挺大的,细节很多,我们一起看一下主要的设计。GSS API
在 Java 语言中通过 jgss
模块来实现。jgss
首先定义了一些底层认证机制需要实现的接口,即 sun.security.jgss.spi
包中的基本接口 GSSContextSpi
GSSNameSpi
GSSCredentialSpi
和工厂接口 MechanismFactory
。底层的协议只需要实现这几个接口就行了,关于 Kerberos
的实现在包 sun.security.jgss.krb5
中,其实这个包里面的代码只是对接了真正的 Kerberos
通信协议实现和 GSS API
接口。这里的设计,按照 DDD 的思想,我们可以理解为一套防腐层,GSS API
和 Kerberos
可以看成两个独立的领域,通过引入防腐层,它们就可以相互独立的各自演进。当接口有改变的时候,我们只需要修改防腐层的代码就行了。
真正的 Kerberos
协议实现在包 sun.security.krb5
下面,这里的实现通过 javax.security.auth.kerberos
包下面的类对应用层暴露接口(应用层在使用 GSS API
时,有时还是需要关心底层认证机制的相关信息的)。作为应用层,如果有必要获取底层认证机制相关的信息,我们将只使用 javax.security.auth.kerberos
中定义的接口,而无需关心 sun
包下面的实现。这里的实现的核心代码在 Credentials
类中,我们看到其定义了 acquireTGTFromCache
acquireDefaultCreds
acquireServiceCreds
等接口用于交换秘钥。更细节的实现代码,大家如有兴趣,可以结合上一篇文章中的通信流程自行研究。我们这里只简要分析一下主要的设计思想。
Hadoop
中使用 GSS API
进行认证Hadoop
中和认证相关的模块主要有两个:一个是直接使用 GSS API
进行认证,用于 tcp
通信的 org.apache.hadoop.security.UserGroupInformation
类;另一个是基于 SPNEGO
协议进行认证,用于 HTTP
通信的 org.apache.hadoop.security.authentication.server.KerberosAuthenticationHandler
。
UserGroupInformation
主要用于 Hadoop
各个内部模块间的通信,也可以用于某一个客户端和 Hadoop
的某个模块进行通信,它同时为服务器和客户端的认证提供了支持。比如 NameNode
的启动之后,它将发起一个登陆请求,用于验证给自己配置的 Principal
和 keytab
是否有效(这里 108 行)。同时当有内部服务(如某个 datanode)的 rpc 请求到来的时候,它将使用登陆得到的认证主体 Subject
中的 doAs
方法来验证发送过来的认证信息,并进行权限验证。有客户端的 rpc 请求到来时,它将获取客户端的用户信息,并根据配置的 ACL(访问控制列表)进行权限验证(实现见这里的 1287 行,及这里)。为了缓存认证信息,避免没必要的重新认证,程序需要维护当前登录的账号的信息,这也就是为什么 UserGroupInformation
在设计上定义了很多静态的属性。同时我们可以注意到很多 synchronized
关键字附加到了某些静态方法上,这是为了支持多线程访问这些全局缓存的信息。
KerberosAuthenticationHandler
的实现是为了支持在 HTTP 服务中进行 Kerberos
认证,这个类最终会封装为一个 Web 服务器中的 Filter
实现对所有 HTTP 请求的权限验证(这里的 AuthFilter 及其基类 AuthenticationFilter)。由于基于 Servlet 的 Web 服务器有很成熟的接口设计,这个模块的实现也相对独立和简单。可以看到它在 init
的时候使用 GSS API
完成了登录,在 authenticate
的时候,将判断是否有有效的认证信息,如果没有将返回协商认证的 HTTP 头部消息以便客户端去完成认证,如果有将进行认证并提供服务。
对于一个运行于 Hadoop 集群的 Spark
应用,我们通常是通过 spark-submit
命令行工具来向集群提交任务的。这一机制对于 spark
应用的开发者看起来很灵活,但如果我们想进行更多的统一管理,比如限制资源使用、提升易用性等等,这样的机制就略显不足了。这个时候一般的做法是将运行 spark
应用的这一能力封装为一个服务,以便进行统一的管理。Livy
就是为实现这样的功能而开发的一个开源工具。
使用 Livy
,我们可以使用 REST 的接口向集群提交 spark
任务。在这里 Livy
其实相当于是整个 Hadoop
大数据集群的一个扩展服务。Livy
在实现的时候如何进行权限的支持呢?当我们去查看 Livy
的源代码的时候,我们会发现,要为每个请求添加 Kerberos
认证,几乎只需要一行代码,这里的 237 行即为那行关键的代码。这里 Livy 就是有效的利用了上面的 KerberosAuthenticationHandler
进行实现的。
简单起见,我们这里的集群所有的组件将运行在同一台机器上。对于 keytab 的配置,我们也从简,只配置一个 kerberos 的 service 账号供所有服务使用。
TDD 是敏捷最重要的实践之一,可以有效的帮助我们确定目标,验证目标,它可以带领我们走得又快又稳。跟随 TDD 的思想,我们先从测试的角度来看这个问题。有了前面的基础知识,假设我们已经有了一套安全的 Hadoop 集群,那么我们应当可以从集群读写文件,运行 MapReduce 任务。我们可以编写读写文件的测试用例如下:
public class HdfsTest {
TestConfig testConfig = new TestConfig();
public void should_read_write_files_from_hdfs() throws IOException {
testConfig.configKerberos();
Configuration conf = new Configuration();
conf.addResource(new Path(testConfig.hdfsSiteFilePath()));
conf.addResource(new Path(testConfig.coreSiteFilePath()));
UserGroupInformation.setConfiguration(conf);
UserGroupInformation.loginUserFromKeytab(testConfig.keytabUser(), testConfig.keytabFilePath());
FileSystem fileSystem = FileSystem.get(conf);
Path path = new Path("/user/root/input/core-site.xml");
if (fileSystem.exists(path)) {
boolean deleteSuccess = fileSystem.delete(path, false);
assertTrue(deleteSuccess);
}
String fileContent = FileUtils.readFileToString(new File(testConfig.coreSiteFilePath()));
try (FSDataOutputStream fileOut = fileSystem.create(path)) {
fileOut.write(fileContent.getBytes("utf-8"));
}
assertTrue(fileSystem.exists(path));
try (FSDataInputStream in = fileSystem.open(path)) {
String fileContentRead = IOUtils.toString(in);
assertEquals(fileContent, fileContentRead);
}
fileSystem.close();
}
}
(完整代码请参考这里)
到这里我们的任务目标就明确了,只要上面的测试能通过,我们的集群就应该搭建好了。
(如果有条件,下面的内容请大家结合代码及参考文档,一边读文章,一边动手实践,否则可能会遗漏很多细节。)
我们先跟随官网的教程搭建一个非安全的集群。
这里我选择的 Hadoop 版本为 2.7.7(我这里是为了和实际项目中用到的版本保持一致,大家可以自行尝试其他版本,思路和大部分的脚本都应该是相同的)。我们选择伪分布式模式(Pseudo-Distributed)来进行尝试,这种模式下,每个组件会运行为一个独立的 java 进程,与真实的分布式环境类似。
我们还是使用容器来进行试验,启动一个容器,并依次运行下面的命令:
(注意,下面如果使用docker跑容器,那么要外部访问的话就把所有的端口暴露出来,这里我建议直接在虚拟机上面跑)
docker run -it --name shd -h shd centos:7 bash
需要的安装包
链接:https://pan.baidu.com/s/1hJogxtc4Kz5nk90VdZN0aA
提取码:yyds
--来自百度网盘超级会员V5的分享
在容器中运行下面的命令:
# 建立并切换到我们的工作目录
mkdir /hd && cd /hd
# 下载软件、解压、进入根目录
yum install wget vim less -y
wget https://archive.apache.org/dist/hadoop/common/hadoop-2.7.7/hadoop-2.7.7.tar.gz
tar xf hadoop-2.7.7.tar.gz
ln -sv hadoop-2.7.7/ hadoop
cd hadoop
# 配置hadoop,这里的shd修改成自己的域名或者是ip,我的是hadoop104,根据自己的情况修改
echo hadoop104 > etc/hadoop/slaves
cat > etc/hadoop/core-site.xml << EOF
fs.defaultFS
hdfs://0.0.0.0:9000
EOF
cat > etc/hadoop/hdfs-site.xml << EOF
dfs.replication
1
dfs.namenode.name.dir
/hd/data/hdfs/namenode
dfs.datanode.data.dir
/hd/data/hdfs/datanode
EOF
# 配置ssh,测试:是否能通过`ssh localhost`免密登录
yum install openssh-clients openssh-server -y
echo 'root:screencast' | chpasswd
sed -i 's/PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd
echo "export VISIBLE=now" >> /etc/profile
ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -P '' && ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key -P ''
/usr/sbin/sshd
ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
chmod 0600 ~/.ssh/authorized_keys
# 安装jdk,并配置环境变量
yum install -y java-1.8.0-openjdk-devel
echo 'export JAVA_HOME=/usr/lib/jvm/java' >> ~/.bashrc
export JAVA_HOME=/usr/lib/jvm/java
# 启动hdfs
bin/hdfs namenode -format
sbin/start-dfs.sh
# 测试
bin/hdfs dfs -mkdir /user
bin/hdfs dfs -mkdir /user/root
bin/hdfs dfs -put etc/hadoop /input
bin/hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.7.jar grep /input /output 'dfs[a-z.]+'
bin/hdfs dfs -cat /output/* # 这里的结果将显示配置文件里面关于dfs的内容
到这里我们的非安全的单机模式集群应该就能运行起来了。但是在这个集群里面我们还没法运行分布式任务,因为目前仅仅是一个 HDFS 分布式文件系统。如果用 jps
查看一下有哪些 java 进程,将发现我们启动了三个进程 NameNode
SecondaryNameNode
DataNode
。
下一步,我们还需要配置并启动用于管理分布式集群任务的关键组件 Yarn
。运行如下这些命令,即可启动 Yarn
:
# 配置Yarn
cat > etc/hadoop/mapred-site.xml << EOF
mapreduce.framework.name
yarn
mapreduce.jobhistory.address
0.0.0.0:10020
mapreduce.jobhistory.webapp.address
0.0.0.0:19888
EOF
cat > etc/hadoop/yarn-site.xml << EOF
yarn.nodemanager.aux-services
mapreduce_shuffle
yarn.log-aggregation-enable
true
yarn.nodemanager.disk-health-checker.max-disk-utilization-per-disk-percentage
99.9
yarn.log.server.url
http://hadoop104:19888/jobhistory/logs
EOF
#文件修改完以后记得查看下是否修改成功
# 启动Yarn:启动之后我们将能通过`./bin/yarn node -list -all`查看到一个RUNNIN的node
sbin/start-yarn.sh
# 启动History server用于查看应用日志
sbin/mr-jobhistory-daemon.sh start historyserver
# 测试:我们将能看到下面的命令从0%到100%按进度完成。
bin/hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.7.jar wordcount /input/* /output/wc/
# 验证:运行`./bin/hadoop dfs -cat /output/wc/part-r-00000`还将看到计算出来的结果。
# 验证:运行`./bin/yarn application -list -appStates FINISHED`可以看到已运行完成的任务,及其日志的地址。
执行上面的命令启动 Yarn
及 historyserver
之后,我们将发现有三个额外的进程 ResourceManager
NodeManager
JobHistoryServer
随之启动了。
(注意,如果是直接虚拟机启动的话,那么直接访问对应的端口就行了)
如果我们的容器所在主机有一个浏览器可以用,那么我们可以通过访问 http://${SHD_DOCKER_IP}:8088/cluster/apps
将能看到上面的 wordcount
程序运行的状态及日志。这里的 SHD_DOCKER_IP
可以通过下面的命令查找出来。
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' shd
如果容器是在一个远端的主机上面启动的,我们可以用 ssh tunnel
的方式建立一个代理,通过代理来访问我们的集群。运行命令 ssh -f -N -D 127.0.0.1:3128 ${USER}@${REMOTE_DOCKER_HOST_IP}
即可建立这样的代理。然后我们运行 echo "${SHD_DOCKER_IP} shd" >> /etc/hosts
将容器的主机名加入到我们本地的 hosts
。再使用 firefox
浏览器来配置代理(如下图),这样我们就可以通过本地的 firefox
来访问到远端的集群了。
我们将能看到如下的 web 应用,通过这个 web 应用,我们实际上还可以查询到更多的集群相关的信息。
http://hadoop104:50070/dfshealth.html#tab-overview
http://hadoop104:8088/cluster
可以看到,经过多年的优化,即便是一个非常复杂的分布式系统,我们现在也可以快速的上手了。几乎所有的配置都有相对合理的默认值,我们仅仅需要调整很少的配置。
Hadoop 本身内置了很多实用的工具,当我们遇到问题的时候,这些工具可以有效的辅助诊断问题。如果大家经过上面的步骤还是没法通过测试(命令行中的测试)。大家可能可以从以下几个方面去查找问题:
datanode
启动失败,可能我们要查看 logs/hadoop-root-datanode-shd.log
日志做进一步分析bin/yarn node -list -all
检查 node
的状态http://172.17.0.12:8042/conf
是否是我们所希望的,比如我们可能由于拼写错误导致配置不对在本系列第一篇文章中,我们尝试了搭建一个 kerberos 认证服务器,这里我们可以用与之前一致的方式先搭建起一个 kerberos 认证服务器。需要的执行脚本如下:
# 将kdc kdc.hadoop.com加入hosts,以便后续进行基于hosts文件的主机名解析
yum install net-tools -y
ip_addr=$(ifconfig eth0 | grep inet | awk '{print $2}')
echo "$ip_addr kdc-server kdc-server.hadoop.com" >> /etc/hosts
# 安装相关软件并进行配置
yum install krb5-server krb5-libs krb5-workstation -y
# 创建krb5配置文件,详细配置解释请参考:https://web.mit.edu/kerberos/krb5-1.12/doc/admin/conf_files/krb5_conf.html
cat > /etc/krb5.conf < /var/kerberos/krb5kdc/kdc.conf <
前面我们分析了 Kerberos 的运行原理,及 Hadoop 的相关源代码,可以知道,为了启动安全支持,每一个集群节点的每一个 hadoop 组件都将需要单独的 Kerberos 账号及其 keytab 文件,每个组件最好还能用不同的账户启动。这里由于我们使用伪分布式模式来部署集群,所有的组件都运行在同一个节点,简单起见,我们这里将使用 root 账号来启动集群,并让所有的组件使用同一个 kerberos 账号。
首先我们生成账号如下:
mkdir /hd/conf/
# 生成hadoop集群需要的账号
kadmin.local addprinc -randkey root/[email protected]
kadmin.local addprinc -randkey HTTP/[email protected]
kadmin.local xst -k /hd/conf/hadoop.keytab root/[email protected] HTTP/[email protected]
# 生成测试用的普通账号
kadmin.local addprinc -randkey [email protected]
kadmin.local xst -k /hd/conf/root.keytab [email protected]
接下来我们来完成 hadoop 的配置,由于配置文件内容比较多,我统一整理到了 github 的一个 repo 中,下面的配置将主要通过 copy 这些文件来生成,而辅以说明主要修改的地方。如果大家有兴趣知道确切的修改之处,可以备份这些文件,然后用 diff 来查看修改,或者用 git 对配置文件进行版本管理,然后查看修改。
可以运行命令也可以直接修改配置文件
配置 core-site.xml
wget https://raw.githubusercontent.com/gmlove/bigdata_conf/master/auth/hadoop/etc/hadoop/core-site.xml -O etc/hadoop/core-site.xml
sed -i 's/hd01-7/hadoop104/g' etc/hadoop/core-site.xml
fs.defaultFS
hdfs://hadoop104:9000
hadoop.proxyuser.root.hosts
*
hadoop.proxyuser.root.groups
*
hadoop.proxyuser.HTTP.hosts
*
hadoop.proxyuser.HTTP.groups
*
hadoop.security.authorization
true
hadoop.security.authentication
kerberos
这里主要加入的配置项及其解释如下:
hadoop.proxyuser.root.hosts=* # 配置root用户(组件启动时认证的kerberos账户)可以以任意客户端认证过的用户(proxy user)来执行操作,详见:https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-common/Superusers.html
hadoop.proxyuser.root.groups=*
hadoop.proxyuser.HTTP.hosts=*
hadoop.proxyuser.HTTP.groups=*
hadoop.security.authorization=true
hadoop.security.authentication=kerberos
配置 hdfs-site.xml
wget https://raw.githubusercontent.com/gmlove/bigdata_conf/master/auth/hadoop/etc/hadoop/hdfs-site.xml -O etc/hadoop/hdfs-site.xml
sed -i 's/hd01-7/hadoop104/g' etc/hadoop/hdfs-site.xml
dfs.replication
1
dfs.namenode.name.dir
/hd/data/hdfs/namenode
dfs.datanode.data.dir
/hd/data/hdfs/datanode
dfs.block.access.token.enable
true
dfs.namenode.keytab.file
/hd/conf/hadoop.keytab
dfs.namenode.kerberos.principal
root/[email protected]
dfs.namenode.kerberos.internal.spnego.principal
HTTP/[email protected]
dfs.web.authentication.kerberos.principal
HTTP/[email protected]
dfs.web.authentication.kerberos.keytab
/hd/conf/hadoop.keytab
dfs.datanode.keytab.file
/hd/conf/hadoop.keytab
dfs.datanode.kerberos.principal
root/[email protected]
dfs.datanode.address
hadoop104:1004
dfs.datanode.http.address
hadoop104:1006
dfs.journalnode.keytab.file
/hd/conf/hadoop.keytab
dfs.journalnode.kerberos.principal
root/[email protected]
dfs.journalnode.kerberos.internal.spnego.principal
HTTP/[email protected]
这里主要加入的配置项如下:
dfs.block.access.token.enable=true
dfs.namenode.keytab.file=/hd/conf/hadoop.keytab
dfs.namenode.kerberos.principal=root/[email protected]
dfs.namenode.kerberos.internal.spnego.principal=HTTP/[email protected]
dfs.web.authentication.kerberos.principal=HTTP/[email protected]
dfs.web.authentication.kerberos.keytab=/hd/conf/hadoop.keytab
dfs.datanode.keytab.file=/hd/conf/hadoop.keytab
dfs.datanode.kerberos.principal=root/[email protected]
dfs.datanode.address=hadoop104:1004
dfs.datanode.http.address=hadoop104:1006
dfs.journalnode.keytab.file=/hd/conf/hadoop.keytab
dfs.journalnode.kerberos.principal=root/[email protected]
dfs.journalnode.kerberos.internal.spnego.principal=HTTP/[email protected]
配置 mapred-site.xml
wget https://raw.githubusercontent.com/gmlove/bigdata_conf/master/auth/hadoop/etc/hadoop/mapred-site.xml -O etc/hadoop/mapred-site.xml
sed -i 's/hd01-7/hadoop104/g' etc/hadoop/mapred-site.xml
mapreduce.framework.name
yarn
mapreduce.jobhistory.address
hadoop104:10020
mapreduce.jobhistory.webapp.address
hadoop104:19888
mapreduce.jobhistory.principal
root/[email protected]
mapreduce.jobhistory.keytab
/hd/conf/hadoop.keytab
这里主要加入的配置项如下:
mapreduce.jobhistory.address=hadoop104:10020
mapreduce.jobhistory.webapp.address=hadoop104:19888
mapreduce.jobhistory.principal=root/[email protected]
mapreduce.jobhistory.keytab=/hd/conf/hadoop.keytab
配置 yarn-site.xml
wget https://raw.githubusercontent.com/gmlove/bigdata_conf/master/auth/hadoop/etc/hadoop/yarn-site.xml -O etc/hadoop/yarn-site.xml
sed -i 's/hd01-7/hadoop104/g' etc/hadoop/yarn-site.xml
yarn.nodemanager.aux-services
mapreduce_shuffle
yarn.resourcemanager.hostname
hadoop104
yarn.log-aggregation-enable
true
yarn.nodemanager.disk-health-checker.max-disk-utilization-per-disk-percentage
99.9
yarn.nodemanager.pmem-check-enabled
false
yarn.nodemanager.vmem-check-enabled
false
yarn.log.server.url
http://hadoop104:19888/jobhistory/logs
yarn.resourcemanager.principal
root/[email protected]
yarn.resourcemanager.keytab
/hd/conf/hadoop.keytab
yarn.resourcemanager.webapp.https.address
${yarn.resourcemanager.hostname}:8090
yarn.nodemanager.principal
root/[email protected]
yarn.nodemanager.keytab
/hd/conf/hadoop.keytab
yarn.web-proxy.principal
root/[email protected]
yarn.web-proxy.keytab
/hd/conf/hadoop.keytab
这里主要加入的配置项如下:
yarn.resourcemanager.principal=root/[email protected]
yarn.resourcemanager.keytab=/hd/conf/hadoop.keytab
yarn.resourcemanager.webapp.https.address=${yarn.resourcemanager.hostname}:8090
yarn.nodemanager.principal=root/[email protected]
yarn.nodemanager.keytab=/hd/conf/hadoop.keytab
yarn.web-proxy.principal=root/[email protected]
yarn.web-proxy.keytab=/hd/conf/hadoop.keytab
配置 hadoop-env.sh
wget https://raw.githubusercontent.com/gmlove/bigdata_conf/master/auth/hadoop/etc/hadoop/hadoop-env.sh -O etc/hadoop/hadoop-env.sh
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Set Hadoop-specific environment variables here.
# The only required environment variable is JAVA_HOME. All others are
# optional. When running a distributed configuration it is best to
# set JAVA_HOME in this file, so that it is correctly defined on
# remote nodes.
# The java implementation to use.
export JAVA_HOME=${JAVA_HOME}
export JAVA_HOME=/usr/lib/jvm/java
export JSVC_HOME=/usr/bin
# The jsvc implementation to use. Jsvc is required to run secure datanodes
# that bind to privileged ports to provide authentication of data transfer
# protocol. Jsvc is not required if SASL is configured for authentication of
# data transfer protocol using non-privileged ports.
#export JSVC_HOME=${JSVC_HOME}
export HADOOP_CONF_DIR=${HADOOP_CONF_DIR:-"/etc/hadoop"}
# Extra Java CLASSPATH elements. Automatically insert capacity-scheduler.
for f in $HADOOP_HOME/contrib/capacity-scheduler/*.jar; do
if [ "$HADOOP_CLASSPATH" ]; then
export HADOOP_CLASSPATH=$HADOOP_CLASSPATH:$f
else
export HADOOP_CLASSPATH=$f
fi
done
# The maximum amount of heap to use, in MB. Default is 1000.
#export HADOOP_HEAPSIZE=
#export HADOOP_NAMENODE_INIT_HEAPSIZE=""
export HADOOP_JAAS_DEBUG=true
export HADOOP_OPTS="-Djava.net.preferIPv4Stack=true -Dsun.security.krb5.debug=true -Dsun.security.spnego.debug"
export HADOOP_SECURE_DN_USER=root
export HADOOP_HDFS_USER=root
# Extra Java runtime options. Empty by default.
export HADOOP_OPTS="$HADOOP_OPTS -Djava.net.preferIPv4Stack=true"
# Command specific options appended to HADOOP_OPTS when specified
export HADOOP_NAMENODE_OPTS="-Dhadoop.security.logger=${HADOOP_SECURITY_LOGGER:-INFO,RFAS} -Dhdfs.audit.logger=${HDFS_AUDIT_LOGGER:-INFO,NullAppender} $HADOOP_NAMENODE_OPTS"
export HADOOP_DATANODE_OPTS="-Dhadoop.security.logger=ERROR,RFAS $HADOOP_DATANODE_OPTS"
export HADOOP_SECONDARYNAMENODE_OPTS="-Dhadoop.security.logger=${HADOOP_SECURITY_LOGGER:-INFO,RFAS} -Dhdfs.audit.logger=${HDFS_AUDIT_LOGGER:-INFO,NullAppender} $HADOOP_SECONDARYNAMENODE_OPTS"
export HADOOP_NFS3_OPTS="$HADOOP_NFS3_OPTS"
export HADOOP_PORTMAP_OPTS="-Xmx512m $HADOOP_PORTMAP_OPTS"
# The following applies to multiple commands (fs, dfs, fsck, distcp etc)
export HADOOP_CLIENT_OPTS="-Xmx512m $HADOOP_CLIENT_OPTS"
#HADOOP_JAVA_PLATFORM_OPTS="-XX:-UsePerfData $HADOOP_JAVA_PLATFORM_OPTS"
# On secure datanodes, user to run the datanode as after dropping privileges.
# This **MUST** be uncommented to enable secure HDFS if using privileged ports
# to provide authentication of data transfer protocol. This **MUST NOT** be
# defined if SASL is configured for authentication of data transfer protocol
# using non-privileged ports.
export HADOOP_SECURE_DN_USER=${HADOOP_SECURE_DN_USER}
# Where log files are stored. $HADOOP_HOME/logs by default.
#export HADOOP_LOG_DIR=${HADOOP_LOG_DIR}/$USER
# Where log files are stored in the secure data environment.
export HADOOP_SECURE_DN_LOG_DIR=${HADOOP_LOG_DIR}/${HADOOP_HDFS_USER}
###
# HDFS Mover specific parameters
###
# Specify the JVM options to be used when starting the HDFS Mover.
# These options will be appended to the options specified as HADOOP_OPTS
# and therefore may override any similar flags set in HADOOP_OPTS
#
# export HADOOP_MOVER_OPTS=""
###
# Advanced Users Only!
###
# The directory where pid files are stored. /tmp by default.
# NOTE: this should be set to a directory that can only be written to by
# the user that will run the hadoop daemons. Otherwise there is the
# potential for a symlink attack.
export HADOOP_PID_DIR=${HADOOP_PID_DIR}
export HADOOP_SECURE_DN_PID_DIR=${HADOOP_PID_DIR}
# A string representing this instance of hadoop. $USER by default.
export HADOOP_IDENT_STRING=$USER
主要加入的配置项如下:
export JSVC_HOME=/usr/bin # 指定jsvc的路径,以便运行安全模式的datanode
export HADOOP_JAAS_DEBUG=true # 开启Kerberos认证的debug日志
export HADOOP_OPTS="-Djava.net.preferIPv4Stack=true -Dsun.security.krb5.debug=true -Dsun.security.spnego.debug" # 开启Kerberos认证的debug日志
export HADOOP_SECURE_DN_USER=root # 运行安全模式的datanode组件的用户
export HADOOP_HDFS_USER=root # 运行hdfs组件的用户
修复启动脚本
由于我们开启了 Kerberos
的调试日志,原来的脚本需要稍加修改才能使用。执行脚本如下:
wget https://raw.githubusercontent.com/gmlove/bigdata_conf/master/auth/hadoop/sbin/stop-dfs.sh -O sbin/stop-dfs.sh
wget https://raw.githubusercontent.com/gmlove/bigdata_conf/master/auth/hadoop/sbin/start-dfs.sh -O sbin/start-dfs.sh
stop-dfs.sh
#!/usr/bin/env bash
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
bin=`dirname "${BASH_SOURCE-$0}"`
bin=`cd "$bin"; pwd`
DEFAULT_LIBEXEC_DIR="$bin"/../libexec
HADOOP_LIBEXEC_DIR=${HADOOP_LIBEXEC_DIR:-$DEFAULT_LIBEXEC_DIR}
. $HADOOP_LIBEXEC_DIR/hdfs-config.sh
#---------------------------------------------------------
# namenodes
NAMENODES=$($HADOOP_PREFIX/bin/hdfs getconf -namenodes | tail -n 1)
echo "Stopping namenodes on [$NAMENODES]"
"$HADOOP_PREFIX/sbin/hadoop-daemons.sh" \
--config "$HADOOP_CONF_DIR" \
--hostnames "$NAMENODES" \
--script "$bin/hdfs" stop namenode
#---------------------------------------------------------
# datanodes (using default slaves file)
if [ -n "$HADOOP_SECURE_DN_USER" ]; then
echo \
"Attempting to stop secure cluster, skipping datanodes. " \
"Run stop-secure-dns.sh as root to complete shutdown."
else
"$HADOOP_PREFIX/sbin/hadoop-daemons.sh" \
--config "$HADOOP_CONF_DIR" \
--script "$bin/hdfs" stop datanode
fi
#---------------------------------------------------------
# secondary namenodes (if any)
SECONDARY_NAMENODES=$($HADOOP_PREFIX/bin/hdfs getconf -secondarynamenodes 2>/dev/null | tail -n 1)
if [ -n "$SECONDARY_NAMENODES" ]; then
echo "Stopping secondary namenodes [$SECONDARY_NAMENODES]"
"$HADOOP_PREFIX/sbin/hadoop-daemons.sh" \
--config "$HADOOP_CONF_DIR" \
--hostnames "$SECONDARY_NAMENODES" \
--script "$bin/hdfs" stop secondarynamenode
fi
#---------------------------------------------------------
# quorumjournal nodes (if any)
SHARED_EDITS_DIR=$($HADOOP_PREFIX/bin/hdfs getconf -confKey dfs.namenode.shared.edits.dir 2>&- | tail -n 1)
case "$SHARED_EDITS_DIR" in
qjournal://*)
JOURNAL_NODES=$(echo "$SHARED_EDITS_DIR" | sed 's,qjournal://\([^/]*\)/.*,\1,g; s/;/ /g; s/:[0-9]*//g')
echo "Stopping journal nodes [$JOURNAL_NODES]"
"$HADOOP_PREFIX/sbin/hadoop-daemons.sh" \
--config "$HADOOP_CONF_DIR" \
--hostnames "$JOURNAL_NODES" \
--script "$bin/hdfs" stop journalnode ;;
esac
#---------------------------------------------------------
# ZK Failover controllers, if auto-HA is enabled
AUTOHA_ENABLED=$($HADOOP_PREFIX/bin/hdfs getconf -confKey dfs.ha.automatic-failover.enabled | tail -n 1)
if [ "$(echo "$AUTOHA_ENABLED" | tr A-Z a-z)" = "true" ]; then
echo "Stopping ZK Failover Controllers on NN hosts [$NAMENODES]"
"$HADOOP_PREFIX/sbin/hadoop-daemons.sh" \
--config "$HADOOP_CONF_DIR" \
--hostnames "$NAMENODES" \
--script "$bin/hdfs" stop zkfc
fi
# eof
start-dfs.sh
#!/usr/bin/env bash
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Start hadoop dfs daemons.
# Optinally upgrade or rollback dfs state.
# Run this on master node.
usage="Usage: start-dfs.sh [-upgrade|-rollback] [other options such as -clusterId]"
bin=`dirname "${BASH_SOURCE-$0}"`
bin=`cd "$bin"; pwd`
DEFAULT_LIBEXEC_DIR="$bin"/../libexec
HADOOP_LIBEXEC_DIR=${HADOOP_LIBEXEC_DIR:-$DEFAULT_LIBEXEC_DIR}
. $HADOOP_LIBEXEC_DIR/hdfs-config.sh
# get arguments
if [[ $# -ge 1 ]]; then
startOpt="$1"
shift
case "$startOpt" in
-upgrade)
nameStartOpt="$startOpt"
;;
-rollback)
dataStartOpt="$startOpt"
;;
*)
echo $usage
exit 1
;;
esac
fi
#Add other possible options
nameStartOpt="$nameStartOpt $@"
#---------------------------------------------------------
# namenodes
NAMENODES=$($HADOOP_PREFIX/bin/hdfs getconf -namenodes | tail -n 1)
echo "Starting namenodes on [$NAMENODES]"
"$HADOOP_PREFIX/sbin/hadoop-daemons.sh" \
--config "$HADOOP_CONF_DIR" \
--hostnames "$NAMENODES" \
--script "$bin/hdfs" start namenode $nameStartOpt
#---------------------------------------------------------
# datanodes (using default slaves file)
if [ -n "$HADOOP_SECURE_DN_USER" ]; then
echo \
"Attempting to start secure cluster, skipping datanodes. " \
"Run start-secure-dns.sh as root to complete startup."
else
"$HADOOP_PREFIX/sbin/hadoop-daemons.sh" \
--config "$HADOOP_CONF_DIR" \
--script "$bin/hdfs" start datanode $dataStartOpt
fi
#---------------------------------------------------------
# secondary namenodes (if any)
SECONDARY_NAMENODES=$($HADOOP_PREFIX/bin/hdfs getconf -secondarynamenodes 2>/dev/null | tail -n 1)
if [ -n "$SECONDARY_NAMENODES" ]; then
echo "Starting secondary namenodes [$SECONDARY_NAMENODES]"
"$HADOOP_PREFIX/sbin/hadoop-daemons.sh" \
--config "$HADOOP_CONF_DIR" \
--hostnames "$SECONDARY_NAMENODES" \
--script "$bin/hdfs" start secondarynamenode
fi
#---------------------------------------------------------
# quorumjournal nodes (if any)
SHARED_EDITS_DIR=$($HADOOP_PREFIX/bin/hdfs getconf -confKey dfs.namenode.shared.edits.dir 2>&- | tail -n 1)
case "$SHARED_EDITS_DIR" in
qjournal://*)
JOURNAL_NODES=$(echo "$SHARED_EDITS_DIR" | sed 's,qjournal://\([^/]*\)/.*,\1,g; s/;/ /g; s/:[0-9]*//g')
echo "Starting journal nodes [$JOURNAL_NODES]"
"$HADOOP_PREFIX/sbin/hadoop-daemons.sh" \
--config "$HADOOP_CONF_DIR" \
--hostnames "$JOURNAL_NODES" \
--script "$bin/hdfs" start journalnode ;;
esac
#---------------------------------------------------------
# ZK Failover controllers, if auto-HA is enabled
AUTOHA_ENABLED=$($HADOOP_PREFIX/bin/hdfs getconf -confKey dfs.ha.automatic-failover.enabled | tail -n 1)
if [ "$(echo "$AUTOHA_ENABLED" | tr A-Z a-z)" = "true" ]; then
echo "Starting ZK Failover Controllers on NN hosts [$NAMENODES]"
"$HADOOP_PREFIX/sbin/hadoop-daemons.sh" \
--config "$HADOOP_CONF_DIR" \
--hostnames "$NAMENODES" \
--script "$bin/hdfs" start zkfc
fi
# eof
主要修改为将通过 hdfs getconf SOME_CONFIG
命令拿到的配置,修改为通过 hdfs getconf SOME_CONFIG >/dev/null | tail -n 1
去获取配置。这里的 tail -n 1
可以去掉命令运行中的 Kerberos
调试日志。
启动集群并运行测试如下:
yum install -y apache-commons-daemon-jsvc.x86_64 # 安装jsvc以便可以用安全模式启动datanode,详见:https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-common/SecureMode.html#Secure_DataNode
sbin/start-dfs.sh && ./sbin/start-secure-dns.sh && sbin/start-yarn.sh && sbin/mr-jobhistory-daemon.sh start historyserver # 依次启动集群的其他组件
# 测试:我们将能看到下面的命令从0%到100%按进度完成。
bin/hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.7.jar wordcount /input/* /output/wcc/
# 验证:运行`./bin/hadoop dfs -cat output/wc/part-r-00000`还将看到计算出来的结果。
# 验证:运行`./bin/yarn application -list -appStates FINISHED`可以看到已运行完成的任务,及其日志的地址。
如果我们无需再测试了,可以用以下命令停止集群:
sbin/stop-dfs.sh && ./sbin/stop-secure-dns.sh && sbin/stop-yarn.sh && sbin/mr-jobhistory-daemon.sh stop historyserver
执行命令如下:
# 加入相关的hosts
SHD_DOCKER_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' shd)
echo "${SHD_DOCKER_IP} shd kdc-server kdc-server.hadoop.com" >> /etc/hosts
# 下载源代码
git clone https://github.com/gmlove/bigdata_conf.git
# 更新配置文件
cd bigdata_conf
cd test/src/test && mv resources resources.1
docker cp shd:/hd/hadoop/etc/hadoop/hdfs-site.xml ./resources/
docker cp shd:/hd/hadoop/etc/hadoop/core-site.xml ./resources/
docker cp shd:/hd/hadoop/etc/hadoop/yarn-site.xml ./resources/
docker cp shd:/etc/krb5.conf ./resources/
docker cp shd:/hd/conf/root.keytab ./resources/
cp ./resources.1/log4j.properties ./resources/
# 运行测试
mvn -Dtest=test.HdfsTest test
运行上面的命令,我们将能看到测试成功执行。
如果容器是在一个远端的主机上面启动的,我们还是可以通过 ssh tunnel
的方式将远端的端口映射到本地来执行此测试。不过,我们需要对前面步骤中的内容作出一些修改。主要的修改是将涉及到的 hostname
配置从 shd
改为 localhost
。这是由于在做端口映射之后,所有的服务均会通过 localhost
来访问,如果我们还是用 shd
,则集群在进行 Kerberos
认证时,主机名验证会出错。
这个任务还是挺有意思的,可以有效的检验我们对于网络、Hadoop 集群、Kerberos
认证机制等的理解。有兴趣的小伙伴可以尝试实验一下,本文就不赘述了。
搭建一套安全的 hadoop 集群,确实不容易,即使我们只是一个伪分布式环境,还做了各种配置简化,也需要花费一番功夫,更别提真正在生产环境中搭建一套集群了。如果是生产可用,我们可能还需要关心机架、集群网络情况、稳定性、性能、跨地域高可用、不停机升级等等一系列的问题。在实际企业应用中,这些大数据基础设施运维实际上是一个比较复杂的工作,这些工作更可能是由一个单独的运维团队去完成的。这里我们所完成的例子的主要价值不在于生产可用,而在于它可以帮助我们理解 hadoop 集群的安全机制,以便指导我们日常的开发工作。另一个价值是,这里的例子实际上完全可以作为我们平时测试用的一套小集群,简单而又功能完整,我们完全可以将这里完成的工作制作为一个 docker 镜像(后续文章将尝试制作此镜像),随时启动这样一套集群,这对于我们测试一些集群集成问题时将带来很大的便利。
大家如果有自己实践,相信在这个过程中可能还会碰到其他的问题,欢迎留言交流,一起学习。
在这篇文章里,我们搭建了一个安全的 hadoop 集群,那么大数据相关的其他组件应该要如何安全的和 hadoop 集群进行整合呢?下一篇文章我们将选取几个典型的组件来分析并进行实践,欢迎持续关注。
参考
Hadoop安全认证机制 (三) | Bright LGM's Blog
也就是说kdc里面存储了客户端还有服务的秘匙,客户端先请求kdc认证,得到服务端的私钥,然后通过服务端的私钥加密信息发送给服务端,服务端揭秘,用客户端的私钥加密发送给客户端,然后双方通过session key进行共同信任的加密揭秘秘钥串进行通信。还有就是他们与时间相关,默认session key的有效时间是8-10小时,所以服务器间的时间要尽量的同步,最少不超过5分钟。
#域名前期规划
域名: mydomain.com
领域名: MYREALM.COM
# 安装密匙分发中心(KDC)
yum install krb5-server krb5-libs krb5-workstation -y
#配置下/etc/hosts
vi /etc/hosts
192.168.10.102 kdc-server.hadoop.com
#配置KDC,krb5.conf是最高层的配置,配置KDC的位置,管理服务器,主机名与Kerberos领域名的映射
vi /etc/krb5.conf
#配置如下
# Configuration snippets may be placed in this directory as well
includedir /etc/krb5.conf.d/
[logging]
default = FILE:/var/log/krb5.log
kdc = FILE:/var/log/krb5kdc.log
admin_server = FILE:/var/log/kadmind.log
[kdc]
profile = /var/kerberos/krb5kdc/kdc.conf
[libdefaults]
forwardable = true
default_realm = MYREALM.COM
dns_lookup_realm = false
dns_lookup_kdc = false
ticket_lifetime = 24h
renew_lifetime = 7d
[realms]
MYREALM.COM = {
kdc = hadoop102
admin_server = hadoop102
}
[domain_realm]
.mydomain.com = MYREALM.COM
mydomain.com = MYREALM.COM
#kdc.conf文件包括kdc配置中Kerberos票据,领域相关配置,kdc数据库和登录详细信息
#修改配置
vi /var/kerberos/krb5kdc/kdc.conf
[kdcdefaults]
kdc_ports = 88
[realms]
MYREALM.COM = {
profile = /etc/krb5.conf
supported_enctypes = arcfour-hmac:normal des3-hmac-sha1:normal des-cbc-crc:normal des:normal des:v4 des:norealm des:onlyrealm des:afs3
allow-null-ticket-addresses = true
database_name = /var/kerberos/krb5kdc/principal
acl_file = /var/kerberos/krb5kdc/kadm4.acl
admin_database_lockfile = /var/kerberos/krb5kdc/kadm5_adb.lock
admin_keytab = FILE:/var/kerberos/krb5kdc/kadm5.keytab
key_stash_file = /var/kerberos/krb5kdc/.k5stash
kdc_ports = 88
kadmind_port = 748
max_life = 10h 0m 0s
max_renewable_life = 7d 0h 0m 0s
}
#建立KDC数据库,用于存储用户密码信息,这个数据库可以是一个文件,或者是ldap存储
kdb5_util create -r MYREALM.COM -s
命令数据以后会初始下密码
cd /var/kerberos/krb5kdc/
我生成的文件没有下面描述的最后一个文件,没有影响
/usr/sbin/krb5kdc && /usr/sbin/kadmind
#创建管理员标识和密码,下面表示账号为admin,密码为admin,admin\nadmin这个表示第一次输入密码和第二次密码
echo -e 'admin\nadmin' | kadmin.local addprinc admin
#验证管理员认证确保KDC支持认证
kinit [email protected]
输入的密码就是刚才设置的admin