分类
业界构件

在 Ubuntu 20.04 LTS 上搭建 Hadoop 环境

在古时候,人们用牛来拉重物。当一头牛拉不动一根原木时,人们从来没有考虑过要想方设法培育出一种更强壮的牛……

Grace Murray Hopper,美国计算机科学家

在 Apache 基金会的项目中,除了历史悠久、广泛使用的 httpd 网页服务器以外,在业界使用最广泛的技术可能就非 Hadoop 莫属了。Hadoop 提供了针对海量数据可靠的、可拓展的分布式计算解决方案。这些年来我们总能听到一些关于“Hadoop 已死?”的论调。但是来自各个云服务提供商的产品表明,Hadoop 及周边生态仍然在不断良性发展,并且持续触及至更多领域之中。时至今日,Hadoop 仍然是分布式处理海量数据的最佳解决方案。

我个人反感博客中充斥“xxx 如何配置”之类的工具类文章,这些主题出现在这些工具的官方文档和新手入门里会更好。但说到 Hadoop,它已经成为大数据方面的“生态标准”:既支持单机运行,甚至亦支持成百上千台机器组成集群运行。然而,不论是官方文档,还是社区上大多数人写的“配置教程”,我认为都没有向一个刚接触 Hadoop 生态的新手介绍我们做这些配置“是为了什么”、“有什么意义”,遂写本文,以作参考。

安装前的准备工作

我们在最新长期支持版本的 Ubuntu Linux 系统(截止目前,是 Ubuntu 20.04 LTS)的基础上搭建 Hadoop 基础环境。该环境包含大数据分析框架 Hadoop、用于进行数据分布式分析的 MapReduce,以及用于分布式存储数据的 Hadoop 文件系统(HDFS)三部分。这是运行其他大数据周围组件的基础环境。

大数据——数据量足够大或复杂,以至于单节点无法处理。

一种我喜爱的对于大数据的定义方式

Hadoop 设计为任意复杂度的数据进行处理,由于实际可能分析的数据量跨度非常大:从单机可存储一直到需要大规模数据中心才能承载,Hadoop 提供了多种灵活的配置方式。由于面向入门用户,我们假设我们在单台物理计算机中使用伪分布式(Pseudo-Distributed)环境进行配置

出于权限隔离最佳实践的考虑,我们在系统中考虑为 Hadoop 创建单独的用户账户和组:

sudo adduser hadoop

Hadoop 基础组件及其外围项目大多是基于 Java 的,要运行 Hadoop,我们需要在系统中安装 Java 运行时环境(JRE)。有多种 JRE 可供选择,我们使用 openjdk-8 作为 JRE。Hadoop 官方文档中说明了对 Java 版本的支持情况。经测试,使用 openjdk-11 会导致 YARN 启动时出现某个错误。Hadoop 框架使用 SSH 协议与本地(或远程计算机)进行通信,我们需要在本地计算机中安装 SSH 服务器守护程序。

sudo apt install openjdk-8-jdk openssh-server -y

Hadoop 通常运行于分布式环境中。在这种环境下,它使用 SSH 协议与自己(或其他服务器)进行通信,为了能够配置 Hadoop 更安全地与服务器进行通信,我们为本地环境 hadoop 用户生成公-私密钥对,以支持 SSH 中的密钥对认证

sudo -u hadoop ssh-keygen -b 4096 -C hadoop
sudo -u hadoop ssh-copy-id localhost -p 22

上述命令生成带备注信息的长度为 4096 位的 RSA 密钥,并将密钥复制至本地计算机中,用于支持 SSH 密钥对认证方式。

获取 Hadoop 的发行版本

Hadoop 的各种发行版本通常从 Hadoop 官方网站的下载页面Apache Bigtop 项目或诸如 Cloudera Manager 等再发行版中获取。为便于讲解,我们使用来自 Hadoop 官方网站的发行版本 3.2.1 举例。我们准备将相关发行版本安装至文件系统的 /opt 目录下(另一个标准选择是 /usr/local)。我们从下载页面的任一镜像站点下载相关版本的二进制文件档案:

wget "https://mirrors.sonic.net/apache/hadoop/common/hadoop-3.2.1/hadoop-3.2.1.tar.gz"

然后,将其解压至 /opt(或者其他你喜爱的位置)中,后续指令均假设你将 Hadoop (及其周边组件)安装至 /opt 目录中:

sudo tar -zxvf hadoop-3.2.1.tar.gz -C /opt

为使用单独的 hadoop 用户操作,我们赋予 hadoop 用户 Hadoop 软件目录的拥有权:

sudo chown hadoop:hadoop -R /opt/hadoop-3.2.1

截至目前,我们已经完成了所有对于 Hadoop 的准备和安装工作,接下来我们切换为 hadoop 用户,并前往安装目录下,开始配置 Hadoop 的工作:

su hadoop
cd /opt/hadoop-3.2.1

配置 Hadoop 相关软件

Hadoop 并非是一个软件,而是一系列大数据存储及处理的工具集合。所以,有必要针对常用的基础组件进行配置,以完成 Hadoop 环境的搭建。

配置环境变量

很显然,我们最喜欢使用类似于 hdfs 而非 /opt/hadoop-3.2.1/bin/hdfs 的方式执行 Hadoop 命令。作为 hadoop 用户执行如下命令将 Hadoop 的相关变量添加至环境变量中,如果不想每次都输入,可以将这些命令添加至 ~/.bashrc 文件末尾:

export HADOOP_HOME=/opt/hadoop-3.2.1
export HADOOP_INSTALL=$HADOOP_HOME
export HADOOP_MAPRED_HOME=$HADOOP_HOME
export HADOOP_COMMON_HOME=$HADOOP_HOME
export HADOOP_HDFS_HOME=$HADOOP_HOME
export YARN_HOME=$HADOOP_HOME
export HADOOP_COMMON_LIB_NATIVE_DIR=$HADOOP_HOME/lib/native
export PATH=$PATH:$HADOOP_HOME/sbin:$HADOOP_HOME/bin
export HADOOP_OPTS="-Djava.library.path=$HADOOP_HOME/lib/native"

如果你选择将这些命令添加至 ~/.bashrc 中,你需要通过下列命令重新加载你的 .bashrc 配置:

source ~/.bashrc

配置 Hadoop 基础框架

为顺利运行 Hadoop 组件,我们有必要告诉 Hadoop 有关 Java 运行时环境(JRE)的位置信息。Hadoop 框架的基础配置文件位于 ./etc/hadoop/hadoop-env.sh 中,我们只需要修改其中的 JAVA_HOME 行。如果你使用其他方式(比如非标准端口)通过 SSH 客户端连接到(本地)服务器,则需要额外修改 HADOOP_SSH_OPTS 行:

export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
export HADOOP_SSH_OPTS="-p 22"

Hadoop 生态的大多数组件采用 .xml 文件的方式进行配置。它们采用了统一的格式:即将配置项填写在每个配置文件的 <configuration></configuration> 之间,每个配置项通过以下的方式呈现:

<property>
<name>配置项名称</name>
<value>配置值</value>
</property>

我们配置 Hadoop 的核心变量,这些配置位于 ./etc/hadoop/core-site.xml 配置文件中:

配置项
fs.defaultFShdfs://localhost/

……也就是说,将 ./etc/hadoop/core-site.xml 配置文件的 <configuration> 部分替换为以下内容:

<configuration>
<property>
<name>fs.defaultFS</name>
<value>hdfs://localhost/</value>
</property>
</configuration>

这将设置 Hadoop 默认的文件系统为本地 HDFS。Apache Hadoop 项目中提供了一份完整的配置项列表

配置 HDFS(Hadoop 文件系统)

在使用 Hadoop 文件系统(HDFS)之前,我们需要显式配置 HDFS,以指定 NameNode 与 DataNode 的存储位置。我们计划将 NameNode 与 DataNode 存储于本地文件系统上,即存放于 ~/hdfs 中:

mkdir -p ~/hdfs/namenode ~/hdfs/datanode

我们将以下属性添加至 HDFS 的配置文件 ./etc/hadoop/hdfs-site.xml 中,以体现这一点:

配置项
dfs.replication1
dfs.name.dir/home/hadoop/hdfs/namenode
dfs.data.dir/home/hadoop/hdfs/datanode
请参考上方配置 ./etc/hadoop/core-site.xml 文件的方式来将这些配置项添加到配置文件中

首次启动 Hadoop 环境之前,我们需要初始化 Namenode 节点数据:

hdfs namenode -format

忘记初始化 Namenode 节点可能导致遇到 NameNode is not formatted. 错误。

配置 MapReduce(由 YARN 驱动)

MapReduce 是一种简单的用于数据处理的编程模型,YARN(Yet Another Resource Negotiator)是 Hadoop 的集群资源管理系统。我们使用与上边类似的方法编辑 ./etc/hadoop/mapred-site.xml 文件,配置以下项目:

配置项
mapreduce.framework.nameyarn

同样地,我们编辑 ./etc/hadoop/yarn-site.xml 文件:

配置项
yarn.nodemanager.aux-servicesmapreduce_shuffle
yarn.resourcemanager.hostnamelocalhost

启动 Hadoop 集群环境

上述配置完成后,我们启动 Hadoop 的相关服务:

start-dfs.sh
start-yarn.sh
mr-jobhistory-daemon.sh start historyserver

这将启动以下守护进程:一个 namenode、一个辅助 namenode、一个 datanode(HDFS)、一个资源管理器、一个节点管理器(YARN)以及一个历史服务器(MapReduce)。为验证 Hadoop 相关服务启动成功,我们使用 JDK 中提供的 jps 命令。该命令显示所有正在运行的 Java 程序。

jps

你可以通过访问该主机的 9870 端口,以查看 HFS 的相关情况。如果你发现某些组件未按预期工作,你可以在 Hadoop 安装目录的 ./logs 下找到各服务对应的运行日志进行排查。

其他可选组件的安装

不同于 Hadoop 的基础框架,这些组件通常是可选的,并且是随需求安装的。我们在此介绍一些工具的安装与配置。

Hive

Hive 是构建于 Hadoop 上的数据仓库框架,用于在分布式存储的架构上读取、写入并管理大规模数据集,并使用 SQL 语法进行查询。它将 SQL 查询转换为一系列在 Hadoop 集群上运行的作业,特别适合熟悉 SQL 而不善于使用 Java 进行数据分析的用户。通过 Hive 官方网站下载最新版本的 Hive 发行版,我们使用 3.1.2 版本:

wget "http://mirrors.ibiblio.org/apache/hive/hive-3.1.2/apache-hive-3.1.2-bin.tar.gz"

与安装 Hadoop 基础组件的方法类似,我们将 Hive 解包到 /opt 中。类似地,我们修改 apache-hive-x.y.z-bin 目录的拥有者:

sudo tar -zxvf apache-hive-3.1.2-bin.tar.gz -C /opt
sudo chown hadoop:hadoop -R /opt/apache-hive-3.1.2-bin

与 MySQL 发行版本类似,我们使用 mysql_secure_installation 命令配置 MariaDB,设置数据库 root 用户密码。

然后我们以 hadoop 的身份修改 ~/.bashrc 文件,以添加与 Hive 执行相关的环境变量配置。以下命令需要以 hadoop 用户身份运行:

export HIVE_HOME=/opt/apache-hive-3.1.2-bin
export PATH=$PATH:$HIVE_HOME/bin

通过 source ~/.bashrc 应用更改后,我们配置Hive 使用的关系型数据库。 Hive 将数据仓库的数据(存放于 HDFS 上)与描述这些数据的元数据存放于不同的位置。其中,元数据(Metastore)一般存放于关系型数据库中。我们以 Apache Derby 为例:

schematool -dbType derby -initSchema

需要注意,由于本教程示例中使用的 Hadoop 3.2.1 版本,与 Hive 3.1.2 版本中均包含 guava 组件,但其版本并不兼容,如果在运行 hive 指令时遇到 java.lang.NoSuchMethodError 错误,请考虑使用下列变通方法:

rm /opt/apache-hive-3.1.2-bin/lib/guava-19.0.jar
cp /opt/hadoop-3.2.1/share/hadoop/hdfs/lib/guava-27.0-jre.jar /opt/apache-hive-3.1.2-bin/lib/

截止目前,我们就可以使用 hive 命令访问由本地 Derby 元数据库承载的存储于 HDFS 上的数据仓库了。

分类
C++ 程序设计语言

为什么要有指针和引用类型?

……显然,我们能通过名字使用对象。然而在 C++ 中,大多数对象都“有身份”;也就是说对象位于内存的某个地址中,如果我们知道对象的地址和类型,就能访问它……

翻译自 Bjarne Stroustrup 《The C++ Programming Language》(Fourth Edition),Chapter 7.

@Lollipop9z 同学在上次与我讨论时提出了这个很有趣的问题。由于 lollipop 之前有学习 Python 程序设计语言的背景,所以对于 C++ 等语言中为何提供这些特性感到困惑。事实上,很多学习过包含指针和/或引用概念程序设计语言的同学也仍然对于为什么会存在这些语言元素的原因缺乏思考。下述代码以 C++ 为例。

感谢 @szouc 对于数组声明与数组类型隐式转换方面的指正。

为什么要有指针?

现代通用电子计算机在程序运行时将所需数据存储于内部存储器(内存)中。通过机器可理解的语言,我们能令电子计算机存取内存中某一指定位置数据,就比如编写一个统计字符串字面量中含有多少个英文小写字母 a 的程序一样,这个程序将用于计数的数据存储于内存空间中的某个位置中。像这样的操作使用是如此频繁,以至于高级程序设计语言专门为程序员提供一种被称为“变量”的语言概念。

变量(Variable)是一块具名(Named)地址空间。在高级程序设计语言中,我们不再令计算机程序访问某一个特定位置的数据,而只需指出我们需要一块在内存中名为 a 的,存储默认长度整型数据的空间。就像在大多数编程语言中我们的如下定义一样:

int a;

这样的名称 – 地址一对一的方式使程序员不再关注变量存储的具体位置,这也为兼容于不同内存寻址方式提供了更多方便。然而,并不是所有类型的变量都与计算机为存储该变量所分配的地址空间之间存在一对一的关系,就比如:

int b[10];

上述 C++ 代码声明了一组连续的,能够存储 10 个默认长度整型变量的空间块,其中标识符(Identifier) b 是数组名称。C++ 标准(conv.array、expr.unary.op 与 expr.sub)指导我们,在某些语境下,包含数组名称 b 的表达式中,标识符 b 可以隐式转换为所分配的空间块中第一块的地址。习惯上,我们使用类似 *b 的包含间接寻址运算符的表达式计算第一块地址所对应的内容

我们如何读取其余几块的内容?我们并没有一种方式能够直接访问这些空间的内容,但因为我们知道第一块的地址,(编译器)也知道每块默认长度整型的空间有多长,所以编程语言能够提供类似 *(b+2) 或者 b[2] 的方式,允许我们使用首块地址+偏移量(Offset)的方式间接地访问其他块的内容。

这种通过地址+偏移量间接地访问其他块内容的方式,被称为间接寻址(Indirection)与数组下标(Subscript)运算。像这种保存空间中某一个块的地址的变量,被称为指针。这样,我们很容易接受指针这个概念。

要注意,由于不同数据类型每块所占用的空间各有不同,所以指针是具有类型的。类型系统有助于指导程序以何种方式去解释内存中某块位置的数据,也能够正确处理类似 *(b+2) 的偏移量操作。

其他编程语言,比如 Python ,并没有提供指针数据类型。但也通过下标操作(类似 b[2] )提供访问非具名数据的能力。

那……引用呢?

想象我们有以下函数,完成一项具有超凡成就的工作:

void nicejob(int a)
{
	a = a * 2;
}

这段代码意义非凡。它尝试将传入的变量加倍后,将自身改为加倍后的新值。可惜,这段代码不能正常工作。当你尝试执行下列代码时,你就会发现它并不能如期运行:

#include <iostream>

int main()
{
	int x = 2;
	std::cout << x << std::endl;
	nicejob(x);
	std::cout << x << std::endl;
	return 0;
}
输出:
2
2

发生了什么?在 nicejob 中对于变量 a 的更改并不会影响 x 的值。包括 C/C++ 在内的多种语言中,当按值传递参数到函数时,函数中获得的变量 a 仅仅是 x 的拷贝——你一直在对拷贝进行操作,当然不会影响到原来的 x 值。

显然我们有两种方法解决这个问题。我们可以将变量 x地址传递给函数 nicejob ,在函数中修改内存对应位置的 x 值,简单且粗暴。我们还可以指示我们的程序将 x 值本身(而非拷贝!)传入函数 nicejob 中,这样在函数中操作 a 就相当于直接操作 x 一样—— ax 的别名。

void nicejob(int& a)
{
	a = a * 2;
}

我们只需在函数形式参数列表中将变量 a 的传递类型由 int 改为 int& —— 引用类型即可。这样我们的 nicejob 函数就能如期工作了。

当我们将一个很大的变量传递给函数时,为了避免在变量拷贝过程中的大量开销(比如,我们传递一个大小为 1GB 的图片给图片处理函数时),我们也使用引用类型。为防止函数对传入变量的误修改,我们可以将函数形参列表(Parameter-list)中的变量类型设为常量引用,就比如 const Image&

指针提供了一种(直接或间接)访问非具名数据的能力;引用是一种程序变量在构造过程中初始化的方式;

DGideas
分类
元博客

你好,世界!

我最早开设个人博客的经历要追溯到 2009 年,记得当时第九驿站提供了免费的个人空间服务。对于刚上初中的我来说,虽然网站空间位于美国,并且走的是(现在看起来)速度极其慢的线路。但当时在本地写好网页文件,然后再用 FTP 客户端将网页文件拖放到服务器上的有趣体验仍然让我难忘。

时光飞逝,如今对于已经从大学科班毕业的我来说,经历了多年的计算机科学与技术方面的学习,以及在行业中(实习)混迹的经历,总算开始有一种从“接收知识”到“向别人输出知识”的冲动。就读大学期间产生的些许零碎想法,也终于在这几年的反思总结中形成体系,能够以高质量且系统化的方式向大家分享。

2020年夏季,于北京市朝阳区

一直有很多同学通过各种渠道(面基、邮件、Telegram 等)跟我进行互相交流。在和这么多同学进行高质量的交流后,我发现个人力量毕竟有限,面对面的沟通终究只能帮助到少数人。得益于互联网,我愿意把自己对于知识的思考、想法、创造以及生活的点滴通过个人独立博客的形式分享给更多人。

个人力量终究有限,就像没有系统能够 100% 正确工作一样,尤其对于技术类的文章,我的行文中肯定会出现不准确甚至错误的情况。如遇问题,希望大家通过博客中提到的各种联系渠道和我讨论,感激不尽。

王万霖(@DGideas),2020年5月4日