阿里云

创建 VPC

专有网络VPC(Virtual Private Cloud)是云上安全隔离的虚拟网络环境,支持自定义网络配置、部署和访问云产品资源。
VPC提供了类似于传统数据中心的安全和可配置的私有网络空间,同时又具备云计算的弹性和可扩展性。用户能够完全掌控自己的专有网络,包括选择自己的IP地址范围、创建交换机、配置路由表和网关等。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

安全组

安全组是一种虚拟防火墙,能够控制ECS实例的出入站流量。您可以将具有相同安全需求并相互信任的ECS实例放入相同的安全组,以划分安全域,保障云上资源的安全。本文介绍安全组的功能、分类、最佳实践和操作指引等。
在这里插入图片描述
在这里插入图片描述

下面的规则建议全删然后只加自己的或者公司的专有网络
在这里插入图片描述

购买云服务器 ECS

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

按年付费
在这里插入图片描述
按量付费
在这里插入图片描述

开始配置工作

这里推荐的ssh终端工具是termius,全平台手机平板都能用,而且性能好、颜值高。(之前用finalshell发现太占内存了)

ssh工具准备

创建密钥,一次就好。
在这里插入图片描述
导入之前生成的密钥后,点下右上角的勾即可。
在这里插入图片描述

然后就可以创建链接了,共三个节点所以需要设置三次。
在这里插入图片描述
连上三台机子第一件事先下载trash工具,每个机子都要下载,以后的工作尽量用trash来代替rm

sudo apt install trash-cli

具体配置

主机名映射

三个节点,每个节点内都要设置一次vim /etc/hosts

vim /etc/hosts

加入下面代码
注意这里用私网ip,否则后面没法启动namenode

node1的私网IP node1 node1
node2的私网IP node2 node2
node3的私网IP node3 node3

光这样还不能直接ssh,还需要把密钥文件上传到每个节点上。
在这里插入图片描述
打开隐藏文件的显示
在这里插入图片描述
直接把左边的bigdata-key.pem给拖拽右边就行

改名改权限

mv bigdata-key.pem id_rsa
chmod 400 id_rsa

在这里插入图片描述
现在就能ssh到其他节点了,但是回来只能exit。
在这里插入图片描述

集群分发脚本(root用户免密登陆)

编写分发脚本

vim /usr/local/bin/xsync
#!/usr/bin/env bash
#
# 批量分发文件/目录到集群节点(保持原有绝对路径)
# 节点列表:node1 node2 node3
#
# 用法示例:
#   xsync 文件1 文件2 文件3 ...
#
# 参数说明:
#   文件1 文件2 文件3 ...
#     一个或多个需要分发到集群所有节点的文件或目录。
#     - 文件:将覆盖目标节点相同路径下的同名文件。
#     - 目录:将整个目录及内容同步到目标节点相同路径下。
#
# 示例:
#   xsync /etc/hosts /home/ecs-user/.bashrc /opt/module/jdk
#   ↑ 分发 /etc/hosts、/home/ecs-user/.bashrc 两个文件 和 /opt/module/jdk 整个目录
#
# 注意事项:
#   1. 请提前配置好免密登录(当前节点到所有目标节点)。
#   2. 目录结构会保持一致,目标路径与源路径相同。
#   3. 同步时会覆盖目标节点上的同名文件/目录。
#

set -euo pipefail

# 节点列表
hosts=(node1 node2 node3)

# rsync 参数(可加 --progress 查看传输进度)
RSYNC_OPTS=(-av)

# 1) 参数校验
if [ $# -lt 1 ]; then
  echo "用法: $(basename "$0") 文件1 [文件2 ...]"
  exit 1
fi

# 2) 遍历节点
for host in "${hosts[@]}"; do
  echo "========== 同步至:$host =========="

  # 3) 遍历每个要同步的文件/目录
  for file in "$@"; do
    if [ -e "$file" ]; then
      # 绝对父目录路径
      pdir="$(cd -P "$(dirname "$file")" && pwd)"
      # 文件或目录名称
      fname="$(basename "$file")"

      # 远端创建父目录
      ssh "$host" "mkdir -p '$pdir'"

      # 同步到远端
      rsync "${RSYNC_OPTS[@]}" "$pdir/$fname" "$host:$pdir/"

      echo "✓ 已同步:$pdir/$fname$host:$pdir/"
    else
      echo "⚠ 跳过:$file 不存在"
    fi
  done
done

echo "✅ 全部同步完成"

给上权限

chmod 755 /usr/local/bin/xsync

测试并分发改脚本到其他节点

xsync /usr/local/bin/xsync

在这里插入图片描述

去其他节点检查下

ll /etc/loacl/bin/xsync

在这里插入图片描述

这就做好了,最后分发下刚才的密钥文件

xsync ~/.ssh/id_rsa

在这里插入图片描述

现在三个节点可以随意ssh其他节点了。
在这里插入图片描述

普通用户之间免密登陆

方案一

给出两个方案,下面是方案一,个人建议直接方案二
三个节点都需要
先新建普通用户 hadoop

 useradd -m -d /home/hadoop -s /bin/bash -c "Hadoop 服务账户" hadoop

## 若需删除用户并同时删除主目录
## userdel -r hadoop

改密码

passwd hadoop

切换成hadoop用户

su - hadoop

在这里插入图片描述

ssh-keygen -t rsa -b 4096

# 删除私钥
# rm -f /home/hadoop/.ssh/id_rsa
# 删除公钥
# rm -f /home/hadoop/.ssh/id_rsa.pub
ssh-copy-id node1
ssh-copy-id node2
ssh-copy-id node3

在这里插入图片描述
如果没被拒绝,三个节点都新建用户、生成密钥并分发即可不用管后面的,但是出于方便后面的多用户管理,建议采用后面的方案。

方案二

若被拒绝了,则采用方案二

/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/hadoop/.ssh/id_rsa.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
hadoop@node1: Permission denied (publickey).

exit退出来,用root身份操作。
这里直接做了一个脚本工具,后续若有新加用户,直接一步到位。
在这里插入图片描述

mkdir -p /root/scripts
vim /root/scripts/cluster_user_ssh.sh
#!/usr/bin/env bash
# 说明:
# - 在 node1 上以 root 执行(要求 root 已能免密到所有节点)
# - 仅使用 RSA(4096) 公钥免密,不开启密码登录
# - 统一主组管理:默认将新用户的主组设为 DEFAULT_GROUP(避免一人一组)
# - 子命令:
#     create <username>           # 创建用户并分发密钥,实现节点间免密(主组=DEFAULT_GROUP)
#     status <username>           # 查看各节点用户与密钥状态
#     rotate <username>           # 轮换该用户的密钥并在所有节点覆盖
#     delete <username> [--purge] # 删除用户;--purge 连同家目录一起删除
#
# 使用示例:
#   /root/scripts/cluster_user_ssh.sh create hadoop
#   /root/scripts/cluster_user_ssh.sh status hadoop
#   /root/scripts/cluster_user_ssh.sh rotate hadoop
#   /root/scripts/cluster_user_ssh.sh delete hadoop --purge
#
set -euo pipefail

# ========= 节点清单:按需修改 =========
NODES=("node1" "node2" "node3")
# 本机在 NODES 中的逻辑名(用于判断是否复制私钥;按你的 /etc/hosts 对应)
LOCAL_NODE="${NODES[0]}"
# 统一主组(避免一人一组)
DEFAULT_GROUP="bigdata"
# 可选:给新用户添加的附加组(用逗号分隔,如需要 docker 等)
EXTRA_GROUPS=""   # 例如:EXTRA_GROUPS="sudo,docker"
# ====================================

# ======== 基础函数 ========
die() { echo "[ERR] $*" >&2; exit 1; }

need_cmd() {
  for c in "$@"; do
    command -v "$c" >/dev/null 2>&1 || die "缺少命令: $c"
  done
}

run_remote() {
  local host="$1"; shift
  ssh -o BatchMode=yes "root@${host}" "$@"
}

copy_to_remote() {
  local src="$1" host="$2" dst="$3"
  scp -q "$src" "root@${host}:${dst}"
}

usage() {
  cat <<EOF
用法:
  $0 create <username>           # 创建用户(主组=${DEFAULT_GROUP})并分发 RSA(4096) 密钥,实现节点间免密
  $0 status <username>           # 查看各节点用户与密钥状态
  $0 rotate <username>           # 轮换该用户密钥并在所有节点覆盖
  $0 delete <username> [--purge] # 删除用户;--purge 时连同家目录一起删除

说明:
  - 本脚本不启用密码登录(PasswordAuthentication=no)。
  - 外部 SSH 工具(Xshell/MobaXterm/FinalShell 等)登录该普通用户时,
    请从任意节点下载 /home/<username>/.ssh/id_rsa 到本地并在工具中导入该私钥。
  - 轮换密钥(rot ate):用于怀疑密钥泄漏、人员变动、或定期安全更新;它会生成新密钥并覆盖所有节点的私钥和 authorized_keys。
EOF
}

# ======== 依赖检查 ========
need_cmd ssh scp ssh-keygen adduser id awk grep sed printf getent

# ======== 路径函数 ========
user_home() { printf "/home/%s" "$1"; }
ssh_dir()   { printf "%s/.ssh" "$(user_home "$1")"; }
pri_key()   { printf "%s/id_rsa" "$(ssh_dir "$1")"; }
pub_key()   { printf "%s.pub" "$(pri_key "$1")"; }
auth_keys() { printf "%s/authorized_keys" "$(ssh_dir "$1")"; }

# ======== 组相关 ========
ensure_group_local() {
  local grp="$1"
  if ! getent group "$grp" >/dev/null 2>&1; then
    echo "[INFO] 本机创建组: ${grp}"
    addgroup "$grp"
  fi
}

ensure_group_remote() {
  local host="$1" grp="$2"
  run_remote "$host" "getent group '${grp}' >/dev/null 2>&1 || addgroup '${grp}'"
}

maybe_add_extra_groups_local() {
  local user="$1"
  if [[ -n "$EXTRA_GROUPS" ]]; then
    usermod -a -G "$EXTRA_GROUPS" "$user" || true
  fi
}

maybe_add_extra_groups_remote() {
  local host="$1" user="$2"
  if [[ -n "$EXTRA_GROUPS" ]]; then
    run_remote "$host" "usermod -a -G '$EXTRA_GROUPS' '$user' || true"
  fi
}

# ======== 子命令实现 ========

create_user_and_keys_local() {
  local user="$1"
  local home; home="$(user_home "$user")"
  local sdir; sdir="$(ssh_dir "$user")"
  local prik; prik="$(pri_key "$user")"
  local pubk; pubk="$(pub_key "$user")"
  local auth; auth="$(auth_keys "$user")"

  ensure_group_local "$DEFAULT_GROUP"

  if ! id -u "$user" >/dev/null 2>&1; then
    echo "[INFO] 本机不存在用户 ${user},正在以主组 ${DEFAULT_GROUP} 创建..."
    adduser --disabled-password --gecos "" --ingroup "$DEFAULT_GROUP" "$user"
    maybe_add_extra_groups_local "$user"
  else
    # 已存在用户:如主组非 DEFAULT_GROUP,不强改;加入 bigdata 为附加组并提示
    local cur_gid cur_grp
    cur_gid="$(id -g "$user")"
    cur_grp="$(getent group "$cur_gid" | cut -d: -f1 || true)"
    if [[ "$cur_grp" != "$DEFAULT_GROUP" ]]; then
      echo "[WARN] 用户 ${user} 的当前主组为 ${cur_grp},未强制改为 ${DEFAULT_GROUP}。"
      echo "       已将其加入 ${DEFAULT_GROUP} 作为附加组(如需变更主组,可人工执行:usermod -g ${DEFAULT_GROUP} ${user})。"
      usermod -a -G "$DEFAULT_GROUP" "$user" || true
    fi
    maybe_add_extra_groups_local "$user"
  fi

  install -d -m 700 -o "$user" -g "$DEFAULT_GROUP" "$sdir"

  if [[ ! -f "$pubk" ]]; then
    echo "[INFO] 生成 RSA(4096) 密钥..."
    sudo -u "$user" ssh-keygen -t rsa -b 4096 -f "$prik" -N '' >/dev/null
  else
    echo "[INFO] 本机已有密钥,跳过生成"
  fi

  touch "$auth"
  chown "$user:$DEFAULT_GROUP" "$auth"
  chmod 600 "$auth"
  if ! grep -q -F "$(cat "$pubk")" "$auth"; then
    cat "$pubk" >> "$auth"
  fi
}

distribute_pubkey_to_all() {
  local user="$1"
  local pubk; pubk="$(pub_key "$user")"
  local sdir; sdir="$(ssh_dir "$user")"
  local auth; auth="$(auth_keys "$user")"

  echo "[STEP] 分发公钥到各节点 authorized_keys(统一主组:${DEFAULT_GROUP})"
  for n in "${NODES[@]}"; do
    echo "[INFO] 节点: ${n} -> 写入 authorized_keys"
    ensure_group_remote "$n" "$DEFAULT_GROUP"

    # 创建用户(如不存在),主组设为 DEFAULT_GROUP
    run_remote "$n" "id -u $user >/dev/null 2>&1 || adduser --disabled-password --gecos '' --ingroup '$DEFAULT_GROUP' $user"

    # 若已存在用户但主组不同,仅加入为附加组并告警(不强改主组)
    run_remote "$n" "
      CUR_GID=\$(id -g $user 2>/dev/null || true); 
      CUR_GRP=\$(getent group \"\$CUR_GID\" | cut -d: -f1 2>/dev/null || true);
      if [ \"\$CUR_GRP\" != '$DEFAULT_GROUP' ]; then
        echo '[WARN][$HOSTNAME] 用户 $user 主组为 '\$CUR_GRP',未改为 $DEFAULT_GROUP;加入为附加组';
        usermod -a -G '$DEFAULT_GROUP' '$user' || true;
      fi
    "

    # 附加组
    if [[ -n "$EXTRA_GROUPS" ]]; then
      maybe_add_extra_groups_remote "$n" "$user"
    fi

    # 分发公钥
    copy_to_remote "$pubk" "$n" "/tmp/${user}_id.pub"
    run_remote "$n" "
      install -d -m 700 -o ${user} -g ${DEFAULT_GROUP} ${sdir} && \
      touch ${auth} && \
      chown ${user}:${DEFAULT_GROUP} ${auth} && \
      chmod 600 ${auth} && \
      grep -q -F \"\$(cat /tmp/${user}_id.pub)\" ${auth} || cat /tmp/${user}_id.pub >> ${auth} && \
      rm -f /tmp/${user}_id.pub
    "
  done
}

distribute_private_key_to_others() {
  local user="$1"
  local prik; prik="$(pri_key "$user")"
  local sdir; sdir="$(ssh_dir "$user")"

  echo "[STEP] 分发私钥到其它节点(用于任意节点互登免密)"
  for n in "${NODES[@]}"; do
    if [[ "$n" != "$LOCAL_NODE" ]]; then
      echo "[INFO] 复制私钥到 ${n}"
      ensure_group_remote "$n" "$DEFAULT_GROUP"
      run_remote "$n" "install -d -m 700 -o ${user} -g ${DEFAULT_GROUP} ${sdir}"
      copy_to_remote "$prik" "$n" "$prik"
      run_remote "$n" "chown ${user}:${DEFAULT_GROUP} ${prik} && chmod 600 ${prik}"
    fi
  done
}

cmd_create() {
  local user="${1:-}"; [[ -z "$user" ]] && die "缺少用户名。用法:$0 create <username>"
  echo "[STEP] 本机创建用户与密钥(主组=${DEFAULT_GROUP})"
  create_user_and_keys_local "$user"

  distribute_pubkey_to_all "$user"
  distribute_private_key_to_others "$user"

  echo
  echo "[OK] 用户 ${user} 的 RSA(4096) 公钥免密已在所有节点就绪(主组=${DEFAULT_GROUP})。"
  echo
  echo "【验证步骤】"
  echo "1) 在任意节点执行:"
  echo "   su - ${user}"
  echo "2) 互登测试(应免密直连):"
  echo "   ssh ${NODES[1]}    # 或:ssh ${NODES[2]}"
  echo
  echo "【外部 SSH 工具使用说明】"
  echo "  本脚本不启用密码登录。请从任意节点下载私钥:/home/${user}/.ssh/id_rsa"
  echo "  将其导入 Xshell/MobaXterm/FinalShell 等工具后,使用 ${user}@nodeX 即可登录。"
}

cmd_status() {
  local user="${1:-}"; [[ -z "$user" ]] && die "缺少用户名。用法:$0 status <username>"
  local sdir; sdir="$(ssh_dir "$user")"
  local prik; prik="$(pri_key "$user")"
  local auth; auth="$(auth_keys "$user")"

  echo "[STEP] 查询各节点状态(用户/主组/密钥/authorized_keys)"
  for n in "${NODES[@]}"; do
    echo "---- ${n} ----"
    if run_remote "$n" "id -u ${user} >/dev/null 2>&1"; then
      echo "用户      : 存在"
      run_remote "$n" "id -g -n ${user} 2>/dev/null" || true
      run_remote "$n" "printf '主组      : '; id -g -n ${user} 2>/dev/null || echo '未知'"
      run_remote "$n" "test -d ${sdir} && echo 'SSH目录   : 存在' || echo 'SSH目录   : 不存在'"
      run_remote "$n" "test -f ${prik} && echo '私钥      : 存在' || echo '私钥      : 不存在'"
      run_remote "$n" "test -f ${auth} && echo 'authorized_keys: 存在' || echo 'authorized_keys: 不存在'"
    else
      echo "用户      : 不存在"
    fi
  done
}

cmd_rotate() {
  local user="${1:-}"; [[ -z "$user" ]] && die "缺少用户名。用法:$0 rotate <username>"
  local sdir; sdir="$(ssh_dir "$user")"
  local prik; prik="$(pri_key "$user")"
  local pubk; pubk="$(pub_key "$user")"
  local auth; auth="$(auth_keys "$user")"

  echo "[STEP] 本机轮换 ${user} 的 RSA(4096) 密钥(生成新钥并覆盖全集群)"
  if ! id -u "$user" >/dev/null 2>&1; then
    die "本机不存在用户 ${user},请先执行:$0 create ${user}"
  fi
  # 备份旧密钥(仅本机)
  if [[ -f "$prik" ]]; then
    mv -f "${prik}" "${prik}.bak.$(date +%s)"
    [[ -f "${pubk}" ]] && mv -f "${pubk}" "${pubk}.bak.$(date +%s)" || true
  fi
  sudo -u "$user" ssh-keygen -t rsa -b 4096 -f "$prik" -N '' >/dev/null
  touch "$auth"; chown "$user:$DEFAULT_GROUP" "$auth"; chmod 600 "$auth"
  # 清空并写入新的公钥(本机)
  : > "$auth"
  cat "$pubk" >> "$auth"

  echo "[STEP] 向各节点写入新的公钥与私钥(覆盖)"
  for n in "${NODES[@]}"; do
    echo "[INFO] 节点: ${n}"
    ensure_group_remote "$n" "$DEFAULT_GROUP"
    run_remote "$n" "id -u ${user} >/dev/null 2>&1 || adduser --disabled-password --gecos '' --ingroup '$DEFAULT_GROUP' ${user}"

    # 覆盖 authorized_keys
    copy_to_remote "$pubk" "$n" "/tmp/${user}_id.pub"
    run_remote "$n" "
      install -d -m 700 -o ${user} -g ${DEFAULT_GROUP} ${sdir} && \
      umask 077 && \
      cat /tmp/${user}_id.pub > ${auth} && \
      chown ${user}:${DEFAULT_GROUP} ${auth} && chmod 600 ${auth} && \
      rm -f /tmp/${user}_id.pub
    "
    # 覆盖私钥
    run_remote "$n" "install -d -m 700 -o ${user} -g ${DEFAULT_GROUP} ${sdir}"
    copy_to_remote "$prik" "$n" "$prik"
    run_remote "$n" "chown ${user}:${DEFAULT_GROUP} ${prik} && chmod 600 ${prik}"
  done

  echo
  echo "[OK] 已完成 ${user} 的密钥轮换并在集群内全部覆盖。"
  echo "【外部 SSH 工具提醒】请重新下载新的 /home/${user}/.ssh/id_rsa 到本地并更新工具内的私钥。"
}

cmd_delete() {
  local user="${1:-}"; shift || true
  [[ -z "$user" ]] && die "缺少用户名。用法:$0 delete <username> [--purge]"
  local purge="no"
  if [[ "${1:-}" == "--purge" ]]; then purge="yes"; fi

  echo "[WARN] 即将在所有节点删除用户 ${user}。"
  if [[ "$purge" == "yes" ]]; then
    echo "[WARN] --purge 模式:将连同家目录一起删除,操作不可逆!"
  else
    echo "[INFO] 不带 --purge:仅删除账号,保留家目录(/home/${user})。"
  fi
  read -r -p "确认继续? [y/N]: " ans
  [[ "${ans}" == "y" || "${ans}" == "Y" ]] || die "用户取消。"

  for n in "${NODES[@]}"; do
    echo "[INFO] 节点: ${n} -> 删除用户 ${user}"
    # 尝试结束该用户的进程,避免 userdel 失败
    run_remote "$n" "id -u ${user} >/dev/null 2>&1 && pkill -u ${user} || true"
    if [[ "$purge" == "yes" ]]; then
      run_remote "$n" "id -u ${user} >/dev/null 2>&1 && userdel -r ${user} || true"
    else
      run_remote "$n" "id -u ${user} >/dev/null 2>&1 && userdel ${user} || true"
    fi
  done

  echo "[OK] 已在所有节点完成对 ${user} 的删除操作。"
}

# ======== 主调度 ========
main() {
  local cmd="${1:-}"; shift || true
  case "$cmd" in
    create) cmd_create "$@";;
    status) cmd_status "$@";;
    rotate) cmd_rotate "$@";;
    delete) cmd_delete "$@";;
    ""|-h|--help|help) usage;;
    *) die "未知子命令:$cmd   使用 --help 查看帮助";;
  esac
}

main "$@"
chmod +x /root/scripts/cluster_user_ssh.sh
/root/scripts/cluster_user_ssh.sh -h
/root/scripts/cluster_user_ssh.sh create hadoop

在这里插入图片描述

现在普通用户hadoop也可以无缝ssh了,至于termius的普通用户登录则需要和root类似,通过下载服务器节点中的密钥文件/home//.ssh/id_rsa 并上传到shell工具中即可。

# 设置密码
sudo passwd hadoop
# 加到sudo组
usermod -aG sudo hadoop

大数据组件

先准备好目录

# 三个节点都要
mkdir -p /export/server
cd /export/server/

jdk

官网
在这里插入图片描述

# 解压
tar -zxvf /export/server/jdk-17.0.16_linux-x64_bin.tar.gz -C /export/server/

# 软链(-s 软链,-f 覆盖,-n 当目标是软链名时按普通文件对待)
ln -sfn /export/server/jdk-17.0.16 /export/server/jdk

# 分发
xsync /export/server/jdk /export/server/jdk-17.0.16

配置环境变量

vim /etc/profile

加入下面代码

export JAVA_HOME=/export/server/jdk
export PATH=$PATH:$JAVA_HOME/bin

保存退出,也分发一下

xsync /etc/profile

每个节点都要

# source一下
source /etc/profile

# 删除自带的java
rm -f /usr/bin/java

# 创建软链接
ln -s /export/server/jdk/bin/java /usr/bin/java

# 校验下
java -version
javac -version

集群命令批量执行脚本

vim /usr/local/bin/xcall
#!/usr/bin/env bash
#
# 批量在集群节点执行命令(加载环境变量,保持当前工作目录)
# 节点列表:node1 node2 node3
#
# 用法示例:
#   xcall "jps"
#   xcall "hostname && whoami"
#   xcall "ls -l"
#
# 说明:
#   1) 显式加载 /etc/profile 与 ~/.bashrc,避免非登录/非交互 shell 下 PATH 失效
#   2) 在远端先 cd 到本地当前目录再执行命令,保证路径一致
#   3) 需要提前配置免密登录
#

set -euo pipefail

# 节点列表
hosts=(node1 node2 node3)

# 参数校验
if [ $# -lt 1 ]; then
  echo "用法: $(basename "$0") \"命令\""
  exit 1
fi

# 要执行的命令(原样传递给远端)
cmd="$*"

# 本地当前目录(在远端也切到该目录)
workdir="$(pwd)"

for host in "${hosts[@]}"; do
  echo "========== $host =========="
  ssh -o BatchMode=yes "$host" "
    # 尽量加载环境(不存在也不报错)
    [ -f /etc/profile ] && source /etc/profile >/dev/null 2>&1 || true
    [ -f ~/.bashrc ] && source ~/.bashrc >/dev/null 2>&1 || true
    # 切换到调用时的工作目录(不存在则忽略)
    cd '$workdir' 2>/dev/null || true
    # 执行命令
    $cmd
  "
done
chmod 755 /usr/local/bin/xcall

在这里插入图片描述

Hadoop

官网下载
在这里插入图片描述
先解压

tar -zxvf ~/hadoop-3.4.1.tar.gz -C /export/server/
cd /export/server/
ln -s /export/server/hadoop-3.4.1 hadoop
cd hadoop

在这里插入图片描述

HDFS

配置文件

配置HDFS集群,我们主要涉及到如下文件的修改:
workers:配置从节点(DataNode)有哪些
hadoop-env.sh:配置Hadoop的相关环境变量
core-site.xml:Hadoop核心配置文件
hdfs-site.xml:HDFS核心配置文件
这些文件均存在与$HADOOP_HOME/etc/hadoop文件夹中。
PS:SHADOOP_HOME是后续我们要设置的环境变量,其指代Hadoop安装文件夹即/export/server/hadoop

work文件
cd /export/server/hadoop/etc/hadoop/
vim /export/server/hadoop/etc/hadoop/workers

删除第一行,然后添加如下三个节点

node1
node2
node3
hadoop-env文件
vim /export/server/hadoop/etc/hadoop/hadoop-env.sh

在顶部添加如下内容

# ====== 基础路径======
export JAVA_HOME=/export/server/jdk                         # JDK 安装目录
export HADOOP_HOME=/export/server/hadoop                    # Hadoop 根目录
export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop              # Hadoop 配置目录
export HADOOP_LOG_DIR=$HADOOP_HOME/logs                     # Hadoop 日志目录(确保可写)

# ====== JVM 兼容性设置(JDK17+/Guice/cglib 反射所需)======
# 说明:
# - 解决报错:java.lang.reflect.InaccessibleObjectException: module java.base does not "opens java.lang" ...
# - 仅对以本 env 启动的 Hadoop 进程生效;不影响系统其他 Java 程序。
# - 如需回滚,删除这一行即可。
 export HADOOP_OPTS="$HADOOP_OPTS \
  --add-opens=java.base/java.lang=ALL-UNNAMED \
  --add-opens=java.base/java.lang.reflect=ALL-UNNAMED"
core-site文件
vim /export/server/hadoop/etc/hadoop/core-site.xml

删除<configuration></configuration>,并添加如下内容

<configuration>

    <!--
        fs.defaultFS
        指定 Hadoop 文件系统的默认 URI
        格式:schema://host:port
        例如 hdfs://node1:8020 表示 HDFS 集群,NameNode 在 node1 主机上,RPC 端口为 8020
        注意:这里的主机名必须能被集群所有节点解析
    -->
    <property>
        <name>fs.defaultFS</name>
        <value>hdfs://node1:8020</value>
    </property>

    <!--
        io.file.buffer.size
        Hadoop 读写文件时使用的缓冲区大小(单位:字节)
        默认值为 4096,这里设置为 131072(128KB)以提升大文件的读写效率
        值过大可能导致小文件场景下内存浪费
    -->
    <property>
        <name>io.file.buffer.size</name>
        <value>131072</value>
    </property>

</configuration>

hdfs-site文件
vim /export/server/hadoop/etc/hadoop/hdfs-site.xml

删除<configuration></configuration>,并添加如下内容

<configuration>
    <!-- DataNode 存储目录的权限设置,700 表示仅属主可读写执行 -->
    <property>
        <name>dfs.datanode.data.dir.perms</name>
        <value>700</value>
    </property>

    <!-- NameNode 元数据存储目录 -->
    <property>
        <name>dfs.namenode.name.dir</name>
        <value>/data/nn</value>
    </property>

    <!-- 允许连接到 NameNode 的主机列表 -->
    <property>
        <name>dfs.namenode.hosts</name>
        <value>node1,node2,node3</value>
    </property>

    <!-- HDFS 块大小(单位:字节),此处为 256MB -->
    <property>
        <name>dfs.blocksize</name>
        <value>268435456</value>
    </property>

    <!-- NameNode 处理请求的线程数量 -->
    <property>
        <name>dfs.namenode.handler.count</name>
        <value>100</value>
    </property>

    <!-- DataNode 存储数据块的目录 -->
    <property>
        <name>dfs.datanode.data.dir</name>
        <value>/data/dn</value>
    </property>
</configuration>

在node1节点:

mkdir -p /data/nn
mkdir /data/dn

在node2和node3节点:

mkdir -p /data/dn
分发配置好的Hadoop文件夹
cd /export/server/
xsync hadoop-3.4.1/ hadoop
配置下Hadoop的脚本到环境变量
vim /etc/profile

在底部追加

export HADOOP_HOME=/export/server/hadoop
export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin

然后每个节点

source /etc/profile
授权为普通用户hadoop

安全起见,不要用root去控制,改成普通用户hadoop

xcall ls -ld /data /export/
xcall chown -R hadoop:bigdata /data
xcall chown -R hadoop:bigdata /export

在这里插入图片描述

格式化并启动
su - hadoop

注意,所有设计配置变动的,比如更改配置文件,更改jdk版本都需要重新格式化。
若格式化后发现缺少服务,则可能需要去手动清空一下data目录下的一些文件。

hadoop namenode -format

# 如只有主节点启动namenode
# xcall rm -rf /data/dn/*

平时如果有什么问题需要重置集群,先格式化,然后每个节点删除/data/dn/*
在这里插入图片描述

一键启动

start-dfs.sh

查看进程

xcall jps

一键停止集群

stop-dfs.sh

在这里插入图片描述
webui(http://node1的公网IP:9870)应该也是能正常访问的

打包备份

(不推荐,文件太大了,建议用yarn配置好后的备份方案)
可以留存上面几个配置文件,也可以直接打包整个Hadoop文件夹

mkdir /export/server/back/
tar -zcvf /export/server/back/hadoop-ok-1.tar.gz /export/server/hadoop-3.4.1/

YARN

mapred-env
cd /export/server/hadoop/etc/hadoop
vim /export/server/hadoop/etc/hadoop/mapred-env.sh

顶部添加

# ====== 基础路径 / 日志 ======
export JAVA_HOME=/export/server/jdk

# JobHistoryServer 堆大小(单位 MB)
export HADOOP_JOB_HISTORYSERVER_HEAPSIZE=1000

# 日志级别(旧变量,兼容保留)
# export HADOOP_MAPRED_ROOT_LOGGER=INFO,RFA
# 建议同时设置新的根日志变量(可选)
export HADOOP_ROOT_LOGGER=INFO,RFA

# ====== JDK17+ 兼容性开关(解决 HistoryServer/AM Web 报 InaccessibleObjectException)======
 export MAPRED_OPTS="$MAPRED_OPTS \
   --add-opens=java.base/java.lang=ALL-UNNAMED \
   --add-opens=java.base/java.lang.reflect=ALL-UNNAMED"
mapred-site
vim /export/server/hadoop/etc/hadoop/mapred-site.xml

替换原文件的<configuration></configuration>

<configuration>
    <!-- MapReduce 的运行框架设置为 YARN -->
    <property>
        <name>mapreduce.framework.name</name>
        <value>yarn</value>
        <description>MapReduce 的运行框架设置为 YARN</description>
    </property>

    <!-- 历史服务器 RPC 地址(JobHistory Server) -->
    <property>
        <name>mapreduce.jobhistory.address</name>
        <value>node1:10020</value>
        <description>历史服务器 RPC 监听地址</description>
    </property>

    <!-- 历史服务器 Web UI 地址 -->
    <property>
        <name>mapreduce.jobhistory.webapp.address</name>
        <value>node1:19888</value>
        <description>历史服务器 Web 端口为 node1 的 19888</description>
    </property>

    <!-- 历史信息在 HDFS 的记录临时路径 -->
    <property>
        <name>mapreduce.jobhistory.intermediate-done-dir</name>
        <value>/data/mr-history/tmp</value>
        <description>历史信息在 HDFS 的记录临时路径</description>
    </property>

    <!-- 历史信息在 HDFS 的记录完成路径 -->
    <property>
        <name>mapreduce.jobhistory.done-dir</name>
        <value>/data/mr-history/done</value>
        <description>历史信息在 HDFS 的记录完成路径</description>
    </property>

    <!-- ApplicationMaster 环境变量:指定 MapReduce 的 HOME -->
    <property>
        <name>yarn.app.mapreduce.am.env</name>
        <value>HADOOP_MAPRED_HOME=${HADOOP_HOME}</value>
        <description>MapReduce HOME 设置为 HADOOP_HOME</description>
    </property>

    <!-- Map 任务环境变量:指定 MapReduce 的 HOME -->
    <property>
        <name>mapreduce.map.env</name>
        <value>HADOOP_MAPRED_HOME=${HADOOP_HOME}</value>
        <description>Map 任务环境变量设置为 HADOOP_HOME</description>
    </property>

    <!-- Reduce 任务环境变量:指定 MapReduce 的 HOME -->
    <property>
        <name>mapreduce.reduce.env</name>
        <value>HADOOP_MAPRED_HOME=${HADOOP_HOME}</value>
        <description>Reduce 任务环境变量设置为 HADOOP_HOME</description>
    </property>
</configuration>
yarn-env
vim yarn-env.sh

顶部添加

# ====== 基础路径 ======
export JAVA_HOME=/export/server/jdk
export HADOOP_HOME=/export/server/hadoop
export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop
export HADOOP_LOG_DIR=$HADOOP_HOME/logs

# ====== JDK17+ 兼容性开关(解决 NodeManager/ResourceManager 启动时报 InaccessibleObjectException)======
export HADOOP_OPTS="${HADOOP_OPTS} \
  --add-opens=java.base/java.lang=ALL-UNNAMED \
  --add-opens=java.base/java.lang.reflect=ALL-UNNAMED"

yarn-site
vim yarn-site.xml

替换原文件的<configuration></configuration>

<configuration>
    <!-- ResourceManager 设置在 node1 节点 -->
    <property>
        <name>yarn.resourcemanager.hostname</name>
        <value>node1</value>
        <description>ResourceManager 主机名</description>
    </property>

    <!-- NodeManager 中间数据本地存储路径 -->
    <property>
        <name>yarn.nodemanager.local-dirs</name>
        <value>/data/nm-local</value>
        <description>NodeManager 中间数据本地存储路径</description>
    </property>

    <!-- NodeManager 日志本地存储路径 -->
    <property>
        <name>yarn.nodemanager.log-dirs</name>
        <value>/data/nm-log</value>
        <description>NodeManager 数据日志本地存储路径</description>
    </property>

    <!-- 为 MapReduce 程序开启 Shuffle 服务 -->
    <property>
        <name>yarn.nodemanager.aux-services</name>
        <value>mapreduce_shuffle</value>
        <description>为 MapReduce 程序开启 Shuffle 服务</description>
    </property>

    <!-- 历史服务器日志访问 URL -->
    <property>
        <name>yarn.log.server.url</name>
        <value>http://node1:19888/jobhistory/logs</value>
        <description>历史服务器日志访问 URL</description>
    </property>

    <!-- Web 代理服务主机和端口 -->
    <property>
        <name>yarn.web-proxy.address</name>
        <value>node1:8089</value>
        <description>Web 代理服务主机和端口</description>
    </property>

    <!-- 开启日志聚合 -->
    <property>
        <name>yarn.log-aggregation-enable</name>
        <value>true</value>
        <description>开启日志聚合功能</description>
    </property>

    <!-- 程序日志在 HDFS 的存储路径 -->
    <property>
        <name>yarn.nodemanager.remote-app-log-dir</name>
        <value>/tmp/logs</value>
        <description>程序运行日志在 HDFS 的存储路径</description>
    </property>

    <!-- 使用公平调度器 -->
    <property>
        <name>yarn.resourcemanager.scheduler.class</name>
        <value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FairScheduler</value>
        <description>选择公平调度器(Fair Scheduler)</description>
    </property>
</configuration>
同步配置
xsync `pwd`/

在这里插入图片描述

启动
# 启动yarn
start-yarn.sh
# 停止yarn
stop-yarn.sh

#启动history
mapred --daemon start historyserver
mapred --daemon stop historyserver

在这里插入图片描述

在这里插入图片描述

轻量级备份(强烈推荐)
# 先确保目标目录存在
mkdir -p /export/server/back

# 打包并保留原文件
tar -czvf /export/server/back/hadoop_conf_$(date +%F).tar.gz -C /export/server/hadoop/etc/hadoop .

Mysql

安装

# 1. 更新系统软件包索引
sudo apt update

# 2. 安装 MySQL 服务器(会自动拉取最新可用版本)
sudo apt install -y mysql-server

# 3. 启动并设置开机自启
sudo systemctl enable --now mysql

# 4. 检查 MySQL 状态
sudo systemctl status mysql

在这里插入图片描述

# MySQL 提供的安全初始化向导
sudo mysql_secure_installation

执行后会有几个步骤(推荐选择):

  1. 设置 root 密码等级
  2. 移除匿名用户 → Y
  3. 禁止 root 远程登录 → Y(我们会用新用户)
  4. 删除 test 数据库 → Y
  5. 重新加载权限 → Y

修改监听配置

编辑

sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf

修改(允许来自任意 IP 的连接请求)

bind-address = 0.0.0.0

重启:

sudo systemctl restart mysql

监听放开不等于谁都能连,真正能登录的账号还需要授权。
root 没有远程授权时,即使能到端口也会被拒绝。

Hive

为hive准备专用账号

sudo mysql -u root -p

这里是本地连接不用密码的,直接回车即可

-- 如果已存在旧库,先删除
DROP DATABASE IF EXISTS hive;

-- 重新创建 Hive 元数据库
CREATE DATABASE hive
  DEFAULT CHARACTER SET utf8mb4
  COLLATE utf8mb4_general_ci;

-- 如果已存在旧用户,先删除
DROP USER IF EXISTS 'hive'@'%';

-- 创建 Hive 用户(允许远程连接)
CREATE USER 'hive'@'%' IDENTIFIED BY 'Hive!123';

-- 授权 Hive 用户管理 hive 库
GRANT ALL PRIVILEGES ON hive.* TO 'hive'@'%';

-- 刷新权限
FLUSH PRIVILEGES;

EXIT;

配置Hadoop

core-site

新增两个配置

	<!-- 允许 hadoop 用户代理访问的主机列表,* 表示任意主机 -->
	<property>
			<name>hadoop.proxyuser.hadoop.hosts</name>
			<value>*</value>
	</property>

	<!-- 允许 hadoop 用户代理访问的用户组,* 表示任意用户组 -->
	<property>
			<name>hadoop.proxyuser.hadoop.groups</name>
			<value>*</value>
	</property>

下载

官网
在这里插入图片描述
然后把准备好的hive包通过sftp拖到/export/server下

mysql driver包

cd /export/server
wget https://repo1.maven.org/maven2/com/mysql/mysql-connector-j/8.0.33/mysql-connector-j-8.0.33.jar

安装

tar -zxvf /export/server/apache-hive-4.1.0-bin.tar.gz -C /export/server/

mv /export/server/mysql-connector-j-8.0.33.jar /export/server/apache-hive-4.1.0-bin/lib/
ln -s /export/server/apache-hive-4.1.0-bin hive

配置文件

hive-env
cd /export/server/hive/conf
cp hive-env.sh.template hive-env.sh
vim /export/server/hive/conf/hive-env.sh

顶部加上

# Hadoop 根路径
export HADOOP_HOME=/export/server/hadoop

# Hive 根路径(建议加上,后面引用更清晰)
export HIVE_HOME=/export/server/hive

# Hive 配置目录
export HIVE_CONF_DIR=${HIVE_HOME}/conf

# Hive 的额外依赖 JAR 搜索路径(用于放置 MySQL 驱动等第三方 JAR)
export HIVE_AUX_JARS_PATH=${HIVE_HOME}/lib

# 增大 HS2 堆
export HIVESERVER2_HEAPSIZE=4096
export HADOOP_HEAPSIZE=4096
hive-site
vim /export/server/hive/conf/hive-site.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- Hive Metastore 使用的 JDBC 连接 URL(MySQL 8.x) -->
    <property>
        <name>javax.jdo.option.ConnectionURL</name>
        <value>jdbc:mysql://node1:3306/hive?createDatabaseIfNotExist=true&amp;useSSL=false&amp;allowPublicKeyRetrieval=true&amp;useUnicode=true&amp;charsetEncoding=UTF-8&amp;serverTimezone=Asia/Tokyo</value>
        <description>Hive Metastore 连接 MySQL 的 JDBC URL;允许自动建库,关闭 SSL,设置时区与字符集</description>
    </property>

    <!-- MySQL 8.x 驱动类(8.x 必须使用 com.mysql.cj.jdbc.Driver) -->
    <property>
        <name>javax.jdo.option.ConnectionDriverName</name>
        <value>com.mysql.cj.jdbc.Driver</value>
        <description>MySQL Connector/J 8.0.x 对应的 JDBC 驱动类</description>
    </property>

    <!-- 数据库用户名 -->
    <property>
        <name>javax.jdo.option.ConnectionUserName</name>
        <value>hive</value>
        <description>Metastore 数据库用户名</description>
    </property>

    <!-- 数据库密码 -->
    <property>
        <name>javax.jdo.option.ConnectionPassword</name>
        <value>Hive!123</value>
        <description>Metastore 数据库密码</description>
    </property>

    <!-- HiveServer2 绑定主机(可按需修改为具体主机名或 0.0.0.0) -->
    <property>
        <name>hive.server2.thrift.bind.host</name>
        <value>node1</value>
        <description>HiveServer2 绑定主机名</description>
    </property>

    <!-- Hive Metastore Thrift 服务地址 -->
    <property>
        <name>hive.metastore.uris</name>
        <value>thrift://node1:9083</value>
        <description>Metastore 对外提供的 Thrift 服务地址</description>
    </property>

    <!-- 事件通知 API 鉴权(很多入门/实验环境会先关闭) -->
    <property>
        <name>hive.metastore.event.db.notification.api.auth</name>
        <value>false</value>
        <description>是否开启 Metastore 事件通知 API 的鉴权</description>
    </property>
</configuration>

初始化

创建日志文件夹

mkdir /export/server/hive/logs
cd /export/server/hive
bin/schematool -initSchema -dbType mysql -verbose

在这里插入图片描述
确定hive目录的权限是属于普通用户hadoop的,如果不是

chown -R hadoop:bigdata apache-hive-4.1.0-bin hive

启动

一般起后台服务即可

# 先启动metastore服务
nohup /export/server/hive/bin/hive --service metastore >> logs/metastore.log 2>&1 &
# 然后启动hiveserver2服务
nohup /export/server/hive/bin/hive --service hiveserver2 >> logs/hiveserver2.log 2>&1 &

然后就能用datagrip等工具直接连接了。

想验证的话

ps -ef|grep 【两个runjar的进程号】

若需要beeline的方式写sql

/export/server/hive/bin/beeline -u "jdbc:hive2://node1:10000/default" -n hadoop -p ""

然后就能show databases;,愉快写sql了。

一键脚本

vim /export/server/hive/scripts/hivectl.env
# ===== hivectl.env =====
# Hive 管控统一配置(被 hivectl.sh source)
# 建议文件编码:UTF-8,换行:LF(Unix)

# 基础路径
export HIVE_PREFIX="/export/server/hive"
export HIVE_HOME="${HIVE_HOME:-/export/server/apache-hive-4.1.0-bin}"
export HIVE_BIN="${HIVE_BIN:-$HIVE_PREFIX/bin}"
export HIVE_CONF_DIR="${HIVE_CONF_DIR:-$HIVE_HOME/conf}"

# 运行/日志目录
export HIVE_RUN_DIR="${HIVE_RUN_DIR:-$HIVE_PREFIX/run}"
export HIVE_LOG_DIR="${HIVE_LOG_DIR:-$HIVE_PREFIX/logs}"

# 可执行文件
export HIVE_CMD="${HIVE_CMD:-$HIVE_BIN/hive}"
export BEELINE_CMD="${BEELINE_CMD:-$HIVE_BIN/beeline}"

# Java/Hadoop 环境
export JAVA_HOME="${JAVA_HOME:-/usr/lib/jvm/java-11-openjdk-amd64}"
export HADOOP_HOME="${HADOOP_HOME:-/export/server/hadoop-3.4.1}"
export HADOOP_CONF_DIR="${HADOOP_CONF_DIR:-$HADOOP_HOME/etc/hadoop}"

# 统一 Java 选项(你环境里已有 add-opens)
export JAVA_TOOL_OPTIONS="${JAVA_TOOL_OPTIONS:- --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED}"

# ---- 日志输出策略(遵循 HIVE-19886)----
# 不再用命令行 --hiveconf 传 hive.root.logger,而是用 HADOOP_CLIENT_OPTS。
# 默认走控制台 appender(stdout 通过 nohup 重定向到日志文件)。
# 若需 Log4j2 的滚动文件,请将 HIVE_ROOT_LOGGER 改为 INFO,RFA 并确认 hive-log4j2.properties 定义了 appender RFA。
export HIVE_ROOT_LOGGER="${HIVE_ROOT_LOGGER:-console}"


# 强制覆盖到 HADOOP_CLIENT_OPTS(避免继承系统里旧的 INFO,DRFA)
# 如需追加自定义 JVM 选项,可通过 HADOOP_CLIENT_OPTS_EXTRA 注入。
export HADOOP_CLIENT_OPTS="-Dhive.root.logger.level=INFO -Dhive.root.logger.appender=${HIVE_ROOT_LOGGER} ${HADOOP_CLIENT_OPTS_EXTRA:-}"


# Metastore 配置
export METASTORE_HOST="${METASTORE_HOST:-node1}"
export METASTORE_PORT="${METASTORE_PORT:-9083}"
export HIVE_METASTORE_URIS="${HIVE_METASTORE_URIS:-thrift://${METASTORE_HOST}:${METASTORE_PORT}}"

# HiveServer2 配置
export HS2_MODE="${HS2_MODE:-binary}"   # binary 或 http
export HS2_HOST="${HS2_HOST:-node1}"
export HS2_PORT="${HS2_PORT:-10000}"    # binary 端口
export HS2_HTTP_PORT="${HS2_HTTP_PORT:-10001}"
export HS2_HTTP_PATH="${HS2_HTTP_PATH:-cliservice}"

# 认证
export HS2_AUTH="${HS2_AUTH:-NONE}"     # NONE / KERBEROS / LDAP ...
export HS2_KRB_PRINCIPAL="${HS2_KRB_PRINCIPAL:-}"

# Beeline 默认凭据
export BEELINE_USER="${BEELINE_USER:-hadoop}"
export BEELINE_PASSWORD="${BEELINE_PASSWORD:-}"

# 启停与就绪检测
export START_TIMEOUT_SEC="${START_TIMEOUT_SEC:-90}"
export STOP_GRACE_SEC="${STOP_GRACE_SEC:-10}"

# 日志显示策略
export TAIL_LINES="${TAIL_LINES:-200}"           # logs/尾部行数(用于 -v)
export ERROR_SCAN_LINES="${ERROR_SCAN_LINES:-400}"  # 错误扫描行数

# 日志文件
export METASTORE_LOG="${METASTORE_LOG:-$HIVE_LOG_DIR/metastore.log}"
export HS2_LOG="${HS2_LOG:-$HIVE_LOG_DIR/hiveserver2.log}"

# PID 文件
export METASTORE_PID_FILE="${METASTORE_PID_FILE:-$HIVE_RUN_DIR/metastore.pid}"
export HS2_PID_FILE="${HS2_PID_FILE:-$HIVE_RUN_DIR/hiveserver2.pid}"

# 体检时额外检查端口
export EXTRA_CHECK_PORTS="${EXTRA_CHECK_PORTS:-$HS2_PORT $HS2_HTTP_PORT $METASTORE_PORT}"

# ===== 结束 =====
vim /export/server/hive/scripts/hivectl.sh
#!/usr/bin/env bash
# ===== hivectl.sh =====
# 统一管理 Hive Metastore 与 HiveServer2 的启动/停止/状态/日志
# 用法:
#   ./hivectl.sh start|stop|restart|status [-v]|logs|tail|beeline|doctor

set -euo pipefail

# -------- 加载配置 --------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HIVECTL_ENV="${HIVECTL_ENV:-$SCRIPT_DIR/hivectl.env}"

if [[ ! -f "$HIVECTL_ENV" ]]; then
  echo "未找到配置文件:$HIVECTL_ENV" >&2
  exit 1
fi
# shellcheck disable=SC1090
source "$HIVECTL_ENV"

# -------- 工具函数 --------
mkdir -p "$HIVE_LOG_DIR" "$HIVE_RUN_DIR"

ts() { date "+%Y-%m-%d %H:%M:%S"; }
log() { echo "[$(ts)] $*"; }

require_cmd() {
  for c in "$@"; do
    command -v "$c" >/dev/null 2>&1 || { echo "缺少命令:$c" >&2; exit 1; }
  done
}

is_running() {
  local pidfile="$1"
  if [[ -f "$pidfile" ]]; then
    local pid
    pid="$(cat "$pidfile" 2>/dev/null || true)"
    [[ -n "${pid:-}" ]] && ps -p "$pid" >/dev/null 2>&1
  else
    return 1
  fi
}

write_pid() { echo "$2" > "$1"; }
rm_pid() { rm -f "$1"; }

wait_port_listen() {
  local host="$1" port="$2" timeout="${3:-30}"
  local start=$(date +%s)
  while true; do
    if ss -ltnp 2>/dev/null | grep -qE "[:.]${port}\s"; then
      return 0
    fi
    local now=$(date +%s)
    (( now - start >= timeout )) && return 1
    sleep 1
  done
}

wait_tcp_connect() {
  local host="$1" port="$2" timeout="${3:-30}"
  local start=$(date +%s)
  while true; do
    if nc -z "$host" "$port" >/dev/null 2>&1; then
      return 0
    fi
    local now=$(date +%s)
    (( now - start >= timeout )) && return 1
    sleep 1
  done
}

hs2_jdbc_url() {
  local url
  if [[ "${HS2_MODE,,}" == "http" ]]; then
    url="jdbc:hive2://${HS2_HOST}:${HS2_HTTP_PORT}/default;transportMode=http;httpPath=${HS2_HTTP_PATH}"
  else
    url="jdbc:hive2://${HS2_HOST}:${HS2_PORT}/default"
  fi
  if [[ "${HS2_AUTH^^}" == "KERBEROS" && -n "${HS2_KRB_PRINCIPAL:-}" ]]; then
    url="${url};principal=${HS2_KRB_PRINCIPAL}"
  fi
  echo "$url"
}

beeline_ping() {
  local url user pass
  url="$(hs2_jdbc_url)"
  user="${BEELINE_USER}"
  pass="${BEELINE_PASSWORD}"
  "$BEELINE_CMD" -u "$url" -n "$user" -p "$pass" -e "SELECT 1" >/dev/null 2>&1
}

wait_hs2_ready() {
  local timeout="${1:-$START_TIMEOUT_SEC}"
  log "等待 HiveServer2 就绪(最长 ${timeout}s)..."
  if [[ "${HS2_MODE,,}" == "http" ]]; then
    wait_tcp_connect "$HS2_HOST" "$HS2_HTTP_PORT" "$timeout" || return 1
  else
    wait_tcp_connect "$HS2_HOST" "$HS2_PORT" "$timeout" || return 1
  fi
  local start=$(date +%s)
  while true; do
    if beeline_ping; then
      log "HiveServer2 探活成功"
      return 0
    fi
    local now=$(date +%s)
    (( now - start >= timeout )) && break
    sleep 2
  done
  return 1
}

print_usage() {
  cat <<EOF
用法:
  $(basename "$0") start        启动 Metastore 与 HiveServer2(带就绪检测)
  $(basename "$0") stop         停止 HiveServer2 与 Metastore
  $(basename "$0") restart      重启
  $(basename "$0") status [-v]  查看状态(默认仅摘要;-v 显示日志尾部 ${TAIL_LINES} 行)
  $(basename "$0") logs         快速查看最新日志(${TAIL_LINES} 行)
  $(basename "$0") tail         实时跟随日志(Ctrl-C 退出)
  $(basename "$0") beeline      打开 Beeline 会话(基于 env 构造 JDBC)
  $(basename "$0") doctor       一键体检(依赖/端口/配置摘要)
环境文件:
  $HIVECTL_ENV
EOF
}

# -------- 错误摘要扫描 --------
scan_errors() {
  local file="$1" label="$2"
  [[ -f "$file" ]] || return 0
  # 仅在日志中出现关键字时输出摘要
  local patt='ERROR|FATAL|Exception|Unable to locate appender'
  if tail -n "$ERROR_SCAN_LINES" "$file" | egrep -q "$patt"; then
    echo "--- ${label}:发现可疑日志 ---"
    tail -n "$ERROR_SCAN_LINES" "$file" | egrep -n "$patt" | tail -n 50
  fi
}

# -------- 启动/停止实现 --------
start_metastore() {
  if is_running "$METASTORE_PID_FILE"; then
    log "Metastore 已在运行,PID=$(cat "$METASTORE_PID_FILE")"
    return 0
  fi
  require_cmd "$HIVE_CMD" nohup ss nc
  log "启动 Metastore ... 日志: $METASTORE_LOG"
  nohup "$HIVE_CMD" --service metastore >> "$METASTORE_LOG" 2>&1 &
  local pid=$!; write_pid "$METASTORE_PID_FILE" "$pid"; sleep 1
  wait_port_listen "$METASTORE_HOST" "$METASTORE_PORT" "$START_TIMEOUT_SEC" || true
  if ! wait_tcp_connect "$METASTORE_HOST" "$METASTORE_PORT" "$START_TIMEOUT_SEC"; then
    log "错误:无法连接 Metastore ${METASTORE_HOST}:${METASTORE_PORT},查看日志 $METASTORE_LOG"
    return 1
  fi
  log "✅ Metastore 已启动,PID=$pid"
}

start_hs2() {
  if is_running "$HS2_PID_FILE"; then
    log "HiveServer2 已在运行,PID=$(cat "$HS2_PID_FILE")"
    return 0
  fi
  require_cmd "$HIVE_CMD" "$BEELINE_CMD" nohup ss nc
  log "启动 HiveServer2 ... 日志: $HS2_LOG"
  nohup "$HIVE_CMD" --service hiveserver2 \
    --hiveconf hive.metastore.uris="${HIVE_METASTORE_URIS}" \
    >> "$HS2_LOG" 2>&1 &
  local pid=$!; write_pid "$HS2_PID_FILE" "$pid"; sleep 1
  if ! wait_hs2_ready "$START_TIMEOUT_SEC"; then
    log "错误:HiveServer2 未在 ${START_TIMEOUT_SEC}s 内就绪,请查看日志 $HS2_LOG"
    return 1
  fi
  log "✅ HiveServer2 已启动,PID=$pid"
}

stop_service() {
  local name="$1" pidfile="$2" logfile="$3"
  if ! is_running "$pidfile"; then
    log "$name 未在运行"
    rm_pid "$pidfile" || true
    return 0
  fi
  local pid; pid="$(cat "$pidfile")"
  log "停止 $name,PID=$pid ..."
  kill "$pid" 2>/dev/null || true
  local start=$(date +%s)
  while ps -p "$pid" >/dev/null 2>&1; do
    local now=$(date +%s)
    if (( now - start >= STOP_GRACE_SEC )); then
      log "$name 未优雅退出,发送 SIGKILL"
      kill -9 "$pid" 2>/dev/null || true
      break
    fi
    sleep 1
  done
  ps -p "$pid" >/dev/null 2>&1 && { log "$name 停止失败,日志:$logfile"; return 1; }
  rm_pid "$pidfile" || true
  log "$name 已停止"
}

start_all() { start_metastore; start_hs2; }
stop_all()  { stop_service "HiveServer2" "$HS2_PID_FILE" "$HS2_LOG" || true; stop_service "Metastore" "$METASTORE_PID_FILE" "$METASTORE_LOG" || true; }

status_all() {
  local verbose="${1:-0}"
  echo "=== 状态 ==="
  if is_running "$METASTORE_PID_FILE"; then
    echo "Metastore : 运行中 (PID $(cat "$METASTORE_PID_FILE"))"
  else
    echo "Metastore : 未运行"
  fi
  if is_running "$HS2_PID_FILE"; then
    echo "HiveServer2: 运行中 (PID $(cat "$HS2_PID_FILE"))"
  else
    echo "HiveServer2: 未运行"
  fi

  echo "=== 端口监听 ==="
  ss -ltnp | egrep ":(${METASTORE_PORT}|${HS2_PORT}|${HS2_HTTP_PORT})\b" || echo "无相关监听"

  # 错误摘要:只在发现问题时输出
  scan_errors "$METASTORE_LOG" "Metastore 日志"
  scan_errors "$HS2_LOG" "HiveServer2 日志"

  # 仅在 -v 模式下输出日志尾部
  if [[ "$verbose" == "1" ]]; then
    echo "=== 日志尾部(${TAIL_LINES} 行)==="
    echo "--- $METASTORE_LOG ---"; tail -n "$TAIL_LINES" "$METASTORE_LOG" 2>/dev/null || true
    echo "--- $HS2_LOG ---"; tail -n "$TAIL_LINES" "$HS2_LOG" 2>/dev/null || true
  fi
}

logs_quick() {
  echo "=== $METASTORE_LOG (末尾 ${TAIL_LINES}) ==="
  tail -n "$TAIL_LINES" "$METASTORE_LOG" 2>/dev/null || true
  echo
  echo "=== $HS2_LOG (末尾 ${TAIL_LINES}) ==="
  tail -n "$TAIL_LINES" "$HS2_LOG" 2>/dev/null || true
}

logs_tail() {
  echo "Ctrl-C 退出跟随"
  tail -n "$TAIL_LINES" -F "$METASTORE_LOG" "$HS2_LOG"
}

open_beeline() {
  require_cmd "$BEELINE_CMD"
  local url; url="$(hs2_jdbc_url)"
  log "打开 Beeline:$url"
  exec "$BEELINE_CMD" -u "$url" -n "$BEELINE_USER" -p "$BEELINE_PASSWORD"
}

doctor() {
  echo "=== HIVECTL 体检 ==="
  echo "[1/6] 可执行文件"
  for c in "$HIVE_CMD" "$BEELINE_CMD" ss nc awk sed grep java; do
    printf " - %s: " "$c"
    if command -v "$c" >/dev/null 2>&1; then echo "OK"; else echo "缺失"; fi
  done

  echo "[2/6] 关键路径"
  printf " - HIVE_HOME=%s : " "$HIVE_HOME"; [[ -d "$HIVE_HOME" ]] && echo "OK" || echo "不存在"
  printf " - HIVE_CONF_DIR=%s : " "$HIVE_CONF_DIR"; [[ -d "$HIVE_CONF_DIR" ]] && echo "OK" || echo "不存在"
  printf " - HIVE_RUN_DIR=%s : " "$HIVE_RUN_DIR"; [[ -d "$HIVE_RUN_DIR" ]] && echo "OK" || echo "不存在"
  printf " - HIVE_LOG_DIR=%s : " "$HIVE_LOG_DIR"; [[ -d "$HIVE_LOG_DIR" ]] && echo "OK" || echo "不存在"

  echo "[3/6] 端口连通"
  for p in $EXTRA_CHECK_PORTS; do
    [[ "$p" =~ ^[0-9]+$ ]] || continue
    printf " - %s:%s : " "$HS2_HOST" "$p"
    if nc -z "$HS2_HOST" "$p" >/dev/null 2>&1; then echo "可达"; else echo "不可达"; fi
  done

  echo "[4/6] 配置摘要"
  echo " - Metastore URIs: $HIVE_METASTORE_URIS"
  echo " - HS2_MODE: $HS2_MODE"
  echo " - HS2 Binary: ${HS2_HOST}:${HS2_PORT}"
  echo " - HS2 HTTP  : ${HS2_HOST}:${HS2_HTTP_PORT} path=${HS2_HTTP_PATH}"
  echo " - HS2_AUTH : $HS2_AUTH"
  [[ "${HS2_AUTH^^}" == "KERBEROS" ]] && echo " - principal: $HS2_KRB_PRINCIPAL"
  echo " - HIVE_ROOT_LOGGER: $HIVE_ROOT_LOGGER"

  echo "[5/6] 运行状态"
  status_all 0

  echo "[6/6] Beeline URL"
  echo " - URL: $(hs2_jdbc_url)"
  echo "体检完成。"
}

# -------- 主流程 --------
cmd="${1:-}"
case "${cmd}" in
  start)   start_all ;;
  stop)    stop_all ;;
  restart) stop_all || true; start_all ;;
  status)  # 可选 -v
           shift || true
           if [[ "${1:-}" == "-v" ]]; then status_all 1; else status_all 0; fi ;;
  logs)    logs_quick ;;
  tail)    logs_tail ;;
  beeline) open_beeline ;;
  doctor)  doctor ;;
  ""|-h|--help|help) print_usage ;;
  *) echo "未知命令:$cmd" >&2; print_usage; exit 2 ;;
esac

# ===== 结束 =====
# 赋权
chmod +x /export/server/hive/scripts/hivectl.sh

/export/server/hive/scripts/hivectl.sh start

/export/server/hive/scripts/hivectl.sh status

/export/server/hive/scripts/hivectl.sh stop

# 先清日志,重启
:> /export/server/hive/logs/hiveserver2.log
:> /export/server/hive/logs/metastore.log
/export/server/hive/scripts/hivectl.sh restart

测试

本地模式
/export/server/hive/bin/beeline -u "jdbc:hive2://node1:10000/default" -n hadoop -p "" \
-e "SET hive.execution.engine=mr; SET mapreduce.framework.name=local; CREATE DATABASE IF NOT EXISTS test_local; CREATE TABLE IF NOT EXISTS test_local.t1(id INT, name STRING); INSERT INTO test_local.t1 VALUES(1,'local_test'); SELECT * FROM test_local.t1;"
yarn模式
/export/server/hive/bin/beeline -u "jdbc:hive2://node1:10000/default" -n hadoop -p "" \
-e "SET hive.execution.engine=mr; SET mapreduce.framework.name=yarn; CREATE DATABASE IF NOT EXISTS test_yarn; CREATE TABLE IF NOT EXISTS test_yarn.t1(id INT, name STRING); INSERT INTO test_yarn.t1 VALUES(2,'yarn_test'); SELECT * FROM test_yarn.t1;"

DataGrip

在这里插入图片描述
在这里插入图片描述
这里用户名其实就是Linux对应的用户,密码可以不用输入测试连接能通过就可以点应用了。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐