原创作者: 清如许
导 读
阅读本文您将了解到:配置免密信任登录会极大地便利Ansible/Breeze 工具管理主机群;应用非交互式方法安全便捷地建立主机间 SSH 信任登录关系;应用多并发技术快速信任登录到1 万台规模的主机群;二步法快速构建容器镜像的方法,速度快到以秒为单位。
一、前 言
一个跨境旅行的例子
开头先举个例子:有内地居民想到去香港旅行,旅客从北京南站或者深圳北站出发,坐上直达香港西九龙站的高铁列车。如果在越过深港边界线时,边检人员对每一位旅客都查验港澳通行证或者护照,那将是不可想象的,工作量巨大而且时间很短,验证工作几乎无法完成。实际的入港通关是在旅客到达香港西九龙站后,在西九龙站入境大厅查验旅行证件、办理通关。
IT 运维自动化场景
在 IT 运维中,也有类似的需求场景。例如,在管理主机上运行 IT 运维工具 Ansible,通过 SSH 在一组被管理主机上远程运行命令,可以复制文件、新建目录和查看进程状态等。这里的登录请求就是一次跨境旅行,管理主机相当于出发车站,被管理主机是到达车站(香港九龙西站)。
主机的账号和密码是保护主机不受非法入侵的安全钥匙,因此账号和密码被严格地保护起来,以免主机安全受到威胁。同一类型的操作系统,一般都有共同的基本账号,例如 root, 广为公众所知悉。相对于保护公开的账号,保护登录密码受到 IT 管理人员的更多重视。
虽然主机的密码安全很重要,但在某些情况下,登录主机的安全入口也会对受到信任的主机门户大开,不需要输入账号和密码就能直接登录,就像开通了直达主机内部的直通快车。
在用 Ansible 管理远程主机时,如果还需要管理员输入账号和密码,那会非常地麻烦。先不说复杂的密码很难记住,Ansible 是自动化 IT 运维工具,在执行自动化任务管理大批量的主机时,如果还需要逐个输入主机密码,自动化任务将降级为半自动任务甚至手动任务, 效率极其低下。如果把主机的账号和密码存为配置文件,自动化程序在运行时读入密码,这样可以免去管理员输入密码的烦恼,但也存在密码泄露的风险,反而把主机安全变为不安全。
睿云智合(wise2c.com)的 Breeze 是经过 CNCF 认证过的 Kubernetes 官方安装工具软件。Breeze 安装部署 Kubernetes 时就用到了 Ansible 在远程主机批量执行命令的能力。Breeze 是开 源 项 目 , 源 代 码 托 管 在 Github 源 码 仓 库 , 地 址 是 :htTPS://github.com/wise2c-devops/breeze 。
信任登录认证,就是解决免密登陆的一个办法。一次信任认证,多次免密登录。
大规模主机群的需求
虽然免密登录能便利运维工作,但是面对集群内的主机群,动辄几十台上百台主机,初次信任认证时手工输入一百次密码工作量也不少。
本文作者的小目标是轻松管理一百台主机,大目标是管理一万台主机也不毫不费力。输入一万次密码对管理员来说,无异于暗无天日的苦力活。
写作本文的目的是自动化完成信任认证工作,把管理员从繁重的体力活中解救出来,这样留出时间从事有价值的工作,或者从事不可被机器替代的工作。
每个地方都改进一点,自动化多一点,IT 世界将变得轻松而有乐趣。
把程序装进容器里
本文开发的程序,也需要运行在一个操作系统环境,对不同运行环境的适应将不得不面对大量的技术细节。作者打算把程序放在容器内运行,这样只需要适应唯一一个容器镜像操作系统,会大大简化开发和维护工作量。
编写本文程序的初衷是缘起于开发一个面向主机、网络、存储、K8s、Docker 和各类服务软件的故障诊断系统,而本文程序是为了在诊断主机和被诊断主机群之间建立免密信任登录,是故障诊断系统的一个子功能。本文程序最先运行,一次建立信任,多次运行诊断。故障诊断程序拟部署在容器内运行,作为先行功能,在容器内运行建信程序(建立信任程序),是自然而然的,也是必须要优先解决的。
让容器镜像构建更快一些
用户与故障诊断系统之间有少量的交互,容器镜像应当预先安装交互操作所必须的系统组件,容器镜像规模达到 700MB。如果每次制作镜像都从零开始,从互联网公共仓库安装系统组件会比较耗时,本文尝试将镜像制作过程拆分为两步,引入中间镜像,把镜像制作时间从十几分钟缩短到几秒钟。
源代码分享
文章末尾有彩蛋喔。本文的源代码已经上传到 git 源代码仓库,项目网址和下载方法在文章的末尾。
二、信任登录原理
信任登录,或者说免密登陆,就是比较好的解决办法。
预先在管理主机和被管理主机之间建立信任关系,以后来自管理主机的 SSH 登录请求, 都将免输入密码直接放行。在这里,管理主机是被信任的,称作被信任主机,被管理主机是信任关系的主体,称作信任主机。管理和被管理,被信任和信任,管理主机是被信任的,被管理主机是发起信任的,正好相反,是一组反对称关系。
信任和被信任的关系并不是基于源 IP 地址的。因为 IP 地址可以被伪装,入侵主机很容伪装成被信任的主机 IP 而获得信任,所以基于源 IP 地址的方案不可行。
SSH 通信是以 RSA 公钥密码体制为基础实现的,私钥和公钥互为对称,秘钥的主人持有私钥和公钥,公钥对外公开。RSA 密钥的主人用私钥给明文加密,接收方用公钥解密,得到原始明文。利用这一原理,RSA 也可以用于数字签名,秘钥的主人用私钥给信息片段签名, 接收方用公钥验证签名后的信息片段,确定是否从密钥主人发出。验证数字签名可以排除伪造的签名,因为伪造者不持有私钥。
数字签名可以用于单向验证 SSH 信任关系。其原理是:主机 A(被信任主机)持有私钥, 向主机 B(信任主机)发起登录请求,发送签名信息给主机 B(信任主机),主机 B 用公钥验证签名信息。只有用主机 A 私钥签过名的信息片段才能通过主机 B(信任主机)的验证(主机信任登录认证)。主机 A(被信任主机)通过信任登录认证,允许免密登录,而其他没有被信任的主机不能免密登录。
主机 B 会把主机 A(被信任主机)的公钥信息添加到主机 B 本地的~/.ssh/authorized_keys 文件末尾。只有主机 B 上的 root 用户才有权限读写 authorized_keys 文件,其他用户无访问权限。
查看主机 B 本地授权文件的命令和输出结果如下:
[root@dev-10 ~]# [root@dev-10 ~]# ls -ltr ~/.ssh/authorized_keys -rw------ 1 root root 19712 Aug 30 00:26 /root/.ssh/authorized_keys [root@dev-10 ~]# tail -1 ~/.ssh/authorized_keys ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCh3rFcy99QJI2 rlwhA68H/H1KOY3yMAv1/pxyM7pbLj/ m0EO1QrsiL8eQVbxeiiKPm4E V5hQiMWSXgiwZ/53y2 153TyhmUtnnCEyg/Mxx6544yKeDPq8OeX cWw7SVcVSHfvp6/C/p P/rTPBH7 7ydDCCr4o0mFzMxdjsdndzdGvMqeuyQxiq8vdoLivk4Ke6opcf7 jG5otxr4wzAaSp1DXNOSAy/ZT2F/yvTudaDTt85aJeYf6HxPjQ1IArdZrbTxJJd5h7YPgBwK XADkLbrX 10HcBcodtd6 24lCJlmUqRkrSWli/Q3dx/Virb3m8aYTARon5SPVq1VgnwN root@30ffac589d2f
持有了对方主机的公钥就可以允许对方免密登录进来,那会不会造成免密登录的滥用呢?应该不会,因为对方主机的公钥不是想进就能进来的,对方主机发起复制公钥 ID 到本主机的请求,必须输入正确密码并且通过认证才能复制成功。
初次建立信任登录关系时必须输入密码。一次建立信任,多次免密登录。
信任主机和被信任主机是多对多的关系。一台主机的公钥允许被复制到多台主机,也就是可以免密登录到多台信任主机,这也就为管理大规模集群奠定了基础。一台信任主机也可以接收多台被信任主机的秘钥,可以被多台主机免密登录进来,被多台管理主机所管理。
交互式流程
几个步骤:生成 RSA 密钥对、复制公钥到信任主机。
建立信任关系后,免密登录到主机,即使修改了登录密码也不影响已建立信任关系主机的正常免密登录。先设立统一密码,建立信任关系后,再修改密码(对各主机各不相同的密码,或者自动生成的动态密码),防止因密码失窃而遭受损失。
公钥已存在信任主机的本地文件。私钥存在被信任主机的本地文件。
三、信任登录实现
主机认证配置文件
本程序的主机认证配置文件由两段组成:第一段是默认配置,只有一行;第二段是目标主机配置,允许多行。
默认配置格式:
默认配置以 default 开头,分别是默认的登录用户、默认的登录密码和默认的端口。
## IP user password port default root mypassword 22
目标主机配置格式:
主机配置行的格式与默认配置行格式相同,不同之处是以主机 IP 或者域名开头,主机IP 是必填项。其余三项属性可以省略,省略的属性自动以默认配置行的对应项填充。省略的属性如果位于一行的中间,必须以一对长度为空的双引号或单引号占位。
## IP user password port 192.168.1.11 “” mypassword 22
配置主机的几个场景
所有主机的登录账号、账号密码相同。配置方法是默认配置完整,目标主机行只保留主 机 IP 或者域名。在新建集群时,可以把所有主机的 root 密码设为相同的静态值,便于批量建立信任登录,之后,管理员可以修改密码,或者使用动态密码。
对于不同的场景,具体配置方法如下:
主机的登录用户相同、密码各不相同。配置方法是默认配置完整(也可能有些主机密码 相同),目标主机行配置主机 IP、密码,登录用户以占位符替代,SSH 端口不填。
主机的登录用户、登录密码和端口各不相同。可以不配置默认行,主机行的主机 IP、登录用户、登录密码和 SSH 端口填写完整。这种情况比较少见,要么是对安全性要求高,不便于使用默认端口,要么是主机有特殊要求。
对目标主机群完成信任登录认证后,不再需要密码属性,可以从配置文件删除密码属性,或者以占位符代替,或者删除整个主机认证配置文件。
信任登录实现流程
◇ 信任登录主流程:
S1、生成 RSA 密钥对和证书,在本地主机安装必须的软件包 ssh-clients 和 expect 等。对软件包自动检查、自动安装。
S2、主进程读主机列表文件 hosts-auth.txt。
S3、从主机行解析出主机 IP、登录用户、登录密码和 SSH 端口等属性。对于属性值为空的属性,以默认值填充。
S4、调用 expect 类型脚本 expect-ssh-copy-id.sh,进入非交互式子流程。子流程将完成一个具体主机的信任登录认证。
S5、跳转到 S3 步继续处理,直到处理完列表中的所有主机。
S6、输出统计信息,结束退出。
◇ 非交互式 expect 子流程:
S1、expect 脚本从命令行获得主机 IP、登录用户、登录密码和端口等参数。
S2、spawn 孵化子进程调用 ssh-copy-id 执行信任登录。
S3、进入 expect 期待/应答模式,对对方主机给与提示信息,自动识别,并自动应答。例如,当收到提示"password:"时,输出应答"${SSH_PASS}\r"。
S4、完成单次信任登录认证,结束。
信任登录源代码
主流程源代码:
# ssh.sh ## enable trusting on this local host by remote hosts function ssh_remote_trust() { ## generate rsa-key locally ssh_gen_rsakey; ## check and install packages ssh_local_check_install "expect" "expect" ssh_local_check_install "ssh-copy-id" "openssh-clients" ## get ssh default params ssh_default=$(cat ${HOSTS_AUTH_FILE} | grep default) ssh_default_user=$(echo ${ssh_default} | awk '{print $2}') ssh_default_pass=$(echo ${ssh_default} | awk '{print $3}') ssh_default_port=$(echo ${ssh_default} | awk '{print $4}') ssh_default_user=${ssh_default_user:-root} ssh_default_port=${ssh_default_port:-${SSH_PORT_DEFAULT}} ## echo and log default ssh connect's param logger_debug "ssh_default=${ssh_default}" logger_debug "ssh_default_pass=${ssh_default_pass}" logger_info "ssh_default_user=${ssh_default_user}, ssh_default_pass=***, ssh_default_port=${ssh_default_port}" ## loop for hosts list _count=0 _hosts_auth=$(cat ${HOSTS_AUTH_FILE} | grep -v "default" | grep -v "\[" | grep -v "\]" | grep -v "#" | awk 'BEGIN{OFS=","}{if($1!=""){print $1,$2,$3,$4}}') logger_debug "_hosts_auth=${_hosts_auth}" for _host_line in ${_hosts_auth}; do if [ -z "${_host_line}" ]; then logger_warn "empty host" continue; fi _count=$(expr $_count 1) logger_info "remote host No. ${_count}:" logger_info "------------------------- " ## get ssh connect's params from file line SSH_HOST=$(echo ${_host_line} | awk -F, '{print $1}') SSH_USER=$(echo ${_host_line} | awk -F, '{print $2}') SSH_PASS=$(echo ${_host_line} | awk -F, '{print $3}') SSH_PORT=$(echo ${_host_line} | awk -F, '{print $4}') if [ -z "${SSH_HOST}" ]; then logger_warn "SSH_HOST is empty, omit it and continue." continue; fi ## if ssh connect's params is empty, then set to default values SSH_USER=${SSH_USER:-$ssh_default_user} SSH_PASS=${SSH_PASS:-$ssh_default_pass} SSH_PORT=${SSH_PORT:-$ssh_default_port} PARAM_SSH_PORT="-p ${SSH_PORT}" ## echo and log final ssh connect's params logger_info "SSH_HOST=${SSH_HOST}, SSH_USER=${SSH_USER}, SSH_PASS=***, SSH_PORT=${SSH_PORT}, PARAM_SSH_PORT=${PARAM_SSH_PORT}" logger_debug "SSH_PASS=${SSH_PASS}" ## non-interactive, copy public rsa-key to remote host ./common/expect-ssh-copy-id.sh ${SSH_HOST} ${SSH_USER} "${SSH_PASS}" "${PARAM_SSH_PORT}" done; logger_info "${_hosts_count} remote hosts have try to trust on this host. All done." }
非交互式 expect 子流程源代码:
#!/usr/bin/expect # expect-ssh-copy-id.sh ## set time out set timeout 10 ## get arguments set SSH_HOST [lindex $argv 0] set SSH_USER [lindex $argv 1] set SSH_PASS [lindex $argv 2] set PARAM_SSH_PORT [lindex $argv 3] #set ID_RSA_FILE [lindex $argv 4] set OPTIOTN "-o StrictHostKeyChecking=no -o ConnectTimeout=3" if { "${PARAM_SSH_PORT}" != "" } { set OPTIOTN "${PARAM_SSH_PORT} ${OPTIOTN}"} ## copy public rsa-key to remote host spawn ssh-copy-id -f ${PARAM_SSH_PORT} -o StrictHostKeyChecking=no -o ConnectTimeout=3 ${SSH_USER}@${SSH_HOST} ## expect expect { # first connect, no public key in ~/.ssh/known_hosts # "Are you sure you want to continue connecting (yes/no)?" "**(yes/no)?" { send "yes\r" expect "password:" send "${SSH_PASS}\r" } # already has public key in ~/.ssh/known_hosts "**password:" { send "${SSH_PASS}\r" } # it has authorized, do nothing! "Now try logging into the machine" { } } expect eof
四、大规模主机群处理
大规模主机群
典型的主机群规模大约在 30~50 台,使用前面介绍的单进程顺序处理方法,在 10 秒内能处理完毕。若干个主机群组成的主机群的集群,例如 10 个集群组成大机群,规模达到 500 台主机,大约需要 1~2 分钟。
有些用于大数据处理或者科学计算的超大规模集群,其规模可能达到 10000 台以上。虽然说,万台规模的机群堪称凤毛麟角,难得一见,但是为了检验程序的处理能力,本文打算把处理万台集群作为小目标,看是否能找到合适的方法,在较短时间内处理完这些主机的信任登录认证。
信任认证的并发处理
单进程或单线程处理大规模主机群,显得有些力不从心。多任务并发处理能加速处理过程,理论上讲,并发任务数从 1 个增加到 2 个,则系统处理事务数(TPS)会提升 1 倍,以此类推。但是,处理能力提升也受到主机资源的限制:例如 CPU 利用率达到 100%以后,再增加并发数未必会提升吞吐量,反而会增加单次认证的时长,因为多任务之间会竞争有限的 CPU 资源,当获得的 CPU 时间片变少时,认证时间会拉长。
并发处理由一个主进程和若干个子进程组成:主进程起到调度子进程、分配具体任务的 作用;子进程具体执行建立信任关系。
并发处理流程
◇ 主进程处理流程:
S0、生成 RSA 密钥对和证书,在本地主机安装必须的软件包 ssh-clients 和 sshpass 等。对软件包自动检查、自动安装。
S1、主进程读主机列表文件 hosts-auth.txt,根据主机总数 m、输入的并发子任务数 t, 计算每个子任务分担的主机数 n。
S2、均衡地分配主机群 IP 到各个子任务。前 t-1 个子任务分配 n 个主机 IP,第 t 个子任务分配剩下主机 IP(可能不足 n 个)。
S3、顺序启动 t 个子任务(进程),并把已分配的主机 IP 列表作为参数传给子任务。启动任意一个子任务后暂停 200 毫秒(暂停时长可能变化),以便给子任务初始化预留时间, 确保所有子任务在时间标尺上均匀分布。
S4、主进程等待所有子任务结束后,统计输出任务执行信息,然后退出。
◇ 子进程处理流程:
S1、子进程启动后,首先分解传入的主机 IP 参数为一个列表集合。
S2、从集合中取一个元素,解析出主机 IP、登录用户、登录密码和 SSH 端口等属性。对于属性值为空的属性,以默认值填充。
S3、调用命令 sshpass 和命令 ssh-copy-id,建立目标主机到本主机的信任关系。sshpasss 使用直接 TTY 访问模拟键盘交互,自动输入认证密码,运行在无需人工干预的非交互模式。
sshpass -p "${SSH_PASS}" ssh-copy-id -o StrictHostKeyChecking=no -o ConnectTimeout=3 "${SSH_USER}"@"${SSH_HOST}"
S4、跳到第 S2 步继续处理,直到主机列表集合内的主机元素全部处理完毕。
S5、子任务结束退出。
并发处理源代码
主进程源代码:
## ssh.sh ## enable trusting on this local host by remote hosts ## run multi-tasks concurrently function ssh_remote_trust_concur() { ## get count of subtasks _subtasks_count=5 _arg1=$(echo "$1" | awk '{print int($1)}') if [ ${_arg1} -ge 1 ]; then _subtasks_count=${_arg1} fi ## generate rsa-key locally ssh_gen_rsakey; ## check and install packages ssh_local_check_install "ssh-copy-id" "openssh-clients" ssh_local_check_install "sshpass" "sshpass" ## get ssh default params ssh_default=$(cat ${HOSTS_AUTH_FILE} | grep default) ssh_default_user=$(echo ${ssh_default} | awk '{print $2}') ssh_default_pass=$(echo ${ssh_default} | awk '{print $3}') ssh_default_port=$(echo ${ssh_default} | awk '{print $4}') ssh_default_user=${ssh_default_user:-root} ssh_default_port=${ssh_default_port:-22} ## echo and log default ssh connect's params logger_debug "ssh_default=${ssh_default}" logger_debug "ssh_default_pass=${ssh_default_pass}" logger_info "ssh_default_user=${ssh_default_user}, ssh_default_pass=***, ssh_default_port=${ssh_default_port}" ## purify hosts list _hosts_auth=$(cat ${HOSTS_AUTH_FILE} | grep -v "default" | grep -v "\[" | grep -v "\]" | grep -v "#" | awk 'BEGIN{OFS=","}{if($1!=""){print $1,$2,$3,$4}}') ## calculate subtasks information _hosts_count=$(echo "${_hosts_auth}" | wc -l) _hosts_per_task=$[_hosts_count/_subtasks_count 1] _hosts_left="${_hosts_auth}" logger_info "run remote trust with mutli-subtasks concurrently. _hosts_count=${_hosts_count}, _subtasks_count=${_subtasks_count}, _hosts_per_task=${_hosts_per_task}." ## divide hosts_auth into several set of subtasks for ((_task_id=1; _task_id<=${_subtasks_count}; _task_id )); do _hosts_slice=$(echo "${_hosts_left}" | head -n ${_hosts_per_task}) _count_in_slice=$(echo "${_hosts_slice}" | wc -l) _hosts_slice=$(echo "${_hosts_slice}" | awk 'BEGIN{FS=","; OFS=","; ORS="##"} {{print$1,$2,$3,$4}}') ./common/subtask.sh "${SUBTASK_REMOTE_TRUST}" "${_task_id}" "${_hosts_slice}" & _subtask_pid=$! _hosts_left=$(echo "${_hosts_left}" | tail -n $[_hosts_per_task 1]) logger_info "subtask ${SUBTASK_REMOTE_TRUST} [no.${_task_id}, pid=${_subtask_pid}] forked, assigned ${_count_in_slice} hosts." ## wait 0.2 seconds and delay next subtask sleep 0.2 done; ## wait until all subtasks finished or wait-time reached. _wait_seconds=2 for _times in [1..$[_hosts_count/_wait_seconds]]; do sleep ${_wait_seconds} _subtask_alive=$(ps -ef | grep "subtask.sh" | grep "${SUBTASK_REMOTE_TRUST}" | grep $$ | wc -l) if [ ${_subtask_alive} -le 0 ]; then break; fi done logger_info "${_hosts_count} remote hosts have try to trust on this host. All done." }
子进程源代码:
#!/usr/bin/bash # subtask.sh ## Include shells . ./common/logger.sh ## Define constants ID_RSA_FILE="~/.ssh/id_rsa.pub" SUBTASK_REMOTE_TRUST="REMOTE_TRUST" SSH_PORT_DEFAULT=22 ## enable trusting on this local host by remote hosts ## subtask called by parent shell function subtask_remote_trust() { ## get hosts list from argument and purify _subtask_id=$1 shift _hosts_auth=$(echo "$@" | awk 'BEGIN{FS=","; RS="##"; OFS=","; ORS=" "} {if($1!=""){print $1,$2,$3,$4}}') logger_debug _hosts_auth="${_hosts_auth}" ## get ssh default params ssh_default_user=${ssh_default_user:-root} ssh_default_port=${ssh_default_port:-22} ## loop for hosts list _count=0 for _host_line in ${_hosts_auth}; do if [ -z "${_host_line}" ]; then logger_warn "empty host" continue; fi _count=$[_count 1] logger_info "subtask [${_subtask_id}] remote host No. ${_count}:" logger_info "---------------------------------------" logger_debug _host_line="${_host_line}" ## parse ssh connect's params from line SSH_HOST=$(echo ${_host_line} | awk -F, '{print $1}') SSH_USER=$(echo ${_host_line} | awk -F, '{print $2}') SSH_PASS=$(echo ${_host_line} | awk -F, '{print $3}') SSH_PORT=$(echo ${_host_line} | awk -F, '{print $4}') if [ -z "${SSH_HOST}" ]; then logger_warn "SSH_HOST is empty, omit it and continue." continue; fi ## if ssh connect's params is empty, then set to default values SSH_USER=${SSH_USER:-$ssh_default_user} SSH_PASS=${SSH_PASS:-$ssh_default_pass} SSH_PORT=${SSH_PORT:-$ssh_default_port} PARAM_SSH_PORT="-p ${SSH_PORT}" ## echo and log final ssh connect's params logger_info "SSH_HOST=${SSH_HOST}, SSH_USER=${SSH_USER}, SSH_PASS=***, SSH_PORT=${SSH_PORT}, PARAM_SSH_PORT=${PARAM_SSH_PORT}" logger_debug "SSH_PASS=${SSH_PASS}" ## non-interactive, copy public rsa-key to remote host sshpass -p "${SSH_PASS}" ssh-copy-id -o StrictHostKeyChecking=no -o ConnectTimeout=3 "${SSH_USER}"@"${SSH_HOST}" done; }
五、容器化实现
为什么要用容器技术
建立信任登录的程序运行在 Linux/Unix 环境下,如果要适应不同的操作系统版本,需要考虑一些环境细节,或者说为不同的操作系统版本编写不同的代码,至少某些功能点是这样的。随着操作系统版本升级,还要做适应性升级,否则程序运行时可能会发生异常。这是作者不希望看到的。
容器化技术在程序与宿主机之间引入新的容器层,把程序与容器的关系,变为程序与容器、容器与宿主机两层关系。程序员只需要考虑程序运行时的容器环境,容器与宿主机之间的关系交由类似于 Docker 的容器管理层实现。容器化技术把程序与运行时的宿主机环境隔离开来,这样程序就能适应不同的宿主机操作系统。例如,假如建立信任登录的程序选择CentOS 操作系统作为容器环境,CentOS 容器能运行在 Windows、RedHat Linux、Debian Linux 和 AIX 等宿主机操作系统上,则程序也就间接地能在这些操作系统上运行。
这个建信程序只是故障诊断系统的一个功能,如果说修改建信程序代码以适应不同的运行环境,只需要花费少量的时间,那么修改故障诊断程序适应不同环境,代价就太高昂了。所以,容器化是不得不走的一步,晚走不如早一点走。
容器技术选择
从早期的 chroot(1979)、FreeBSD Jails(2000)、Linux VServer(2001)、Solaris Containers(2004)和LXC(2008),到如今的 Warden(2011)、Docker(2013)、Rocket(2014)和Windows Containers(2016),容器技术经过四十年的发展,已经是遍地开花,得到广泛地应用。
其中,Docker 容器技术挟后发优势,因为其功能强大、性能优良、开源免费(社区版)、广泛适应性(所有主流操作系统),而受到业界广泛推崇。作者在 Docker 基础上做过大量的开发和应用,所以优先选择 Docker 容器技术。
基础镜像选择
选好容器技术后,就要选择容器内运行的操作系统。RedHat Linux 是经典的 Linux 发行版,在国内拥有广泛的用户基础。但是因为版本注册和收费等原因,Redhat 并不适合用作容器操作系统。CentOS 是 Redhat 的社区开源免费版,与 Redhat 有相似的命令集和用户操作习惯。Alpine Linux 是面向安全的轻量级 Linux 发行版,主要用于面向 Serverless 服务而无需用户管理的容器专用操作系统。
本程序和其所属的故障诊断程序需要用户管理维护,所以既需要一个程序运行环境,也需要用户操作环境,CentOS 恰好能满足这两点需求。CentOS 的版本选择比较新的CentOS 7.5,开源社区已经有好心人把 CentOS 操作系统构建成公共镜像,从网上拉取对应版本的镜像即可,开箱即用。
基础容器镜像包含操作系统最核心的版本,如果需要更多软件包,例如 SSH 客户端和SSH 服务器,则需要安装附加软件包,本程序也需要安装进去。这些软件包的安装指令在Dockerfile 文件中描述。
信任登录的容器化实现
作者写了一篇文章《容器化开发及两步法快速构建 Docker 镜像》,专门介绍信任登录的容器化实现方法,详细内容请参考下面的任意一个网址,此处不再赘述。
*https://mp.weixin.qq.com/s/PinTIENVoCPNhT4SKxRKtQ
Github 源码仓库:
https://github.com/solomonxu/k8s-diagnose/blob/ssh-trust/docs/容器化开发及两步法快速构建Docker镜像.pdf
六、功能测试
建立信任登录的过程可以在宿主机(云虚拟机)上进行,也可以在容器内进行。
二者的区别是:在宿主机上进行建立信任关系的测试,那么可以从宿主机免密登录到目标主机群;在容器内进行测试,可以从容器免密登录到目标主机群,从宿主机则不能。因为宿主机和容器分别有各自账号和证书系统,从目标主机群看来是两台不同的机器。
前面已经构建了容器镜像,我们就从镜像启动容器,然后开始测试吧。故障诊断程序的 目标运行环境就是 Docker 容器,为什么不呢?
如果从宿主机上运行,只需要把整个 k8s-diagnose 目录拷贝到合适的上级目录下,就可以开始测试,方法完全相同。
启动容器
从前述过程构建的容器镜像启动一个新容器,然后登录到容器内部,查询容器内运行的进程。
[root@dev-10 k8s-diagnose]# docker-compose up & [1] 26526 [root@dev-10 k8s-diagnose]# Creating network "k8sdiagnose_default" with the default driver Creating k8sdiagnose_supervisor_1 ... done Attaching to k8sdiagnose_supervisor_1 [root@dev-10 k8s-diagnose]# docker exec -it k8sdiagnose_supervisor_1 bash [root@30ffac589d2f /]# [root@30ffac589d2f /]# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 15:16 ? 00:00:00 /usr/bin/bash -c supervisord -c /supervisord/supervisord.conf while true; do sleep 100; done root 7 1 0 15:16 ? 00:00:00 /usr/bin/python /usr/bin/supervisord -c /supervisord/supervisord.conf root 8 1 0 15:16 ? 00:00:00 sleep 100 root 9 0 0 15:16 pts/0 00:00:00 bash root 22 9 0 15:16 pts/0 00:00:00 ps -ef
功能测试
功能测试主要验证非交互式信任登录功能是否可用,采用单进程模式运行测试。
1、编辑 hosts-auth.txt 文件,配置默认的用户、密码和端口,以及目标主机列表。
[root@dev-10 ~]# docker exec -it k8sdiagnose_supervisor_1 bash [root@30ffac589d2f /]# cd /k8s-diagnose/conf/ [root@30ffac589d2f conf]# cat hosts-auth.txt ## IP user password port default root mypassword 22 192.168.1.11 192.168.1.12 192.168.1.13
在目标主机行只填写了主机 IP,忽略用户、密码和 SSH 端口等,使用 default 行的默认值。如果在目标主机行也配置了这三个参数,会优先使用并生效。
2、编辑测试脚本 test-common-ssh.sh,放开代码行的注释:test_ssh_remote_trust; 这行代码将会调用 ssh.sh 脚本的函数 function ssh_remote_trust() ,执行单进程的建立与远程主机的信任关系。
3、运行测试脚本:
[root@30ffac589d2f bin]# ./test/test-common-ssh.sh 2019-08-30 00:26:37.618653 [32551] - INFO TEST: ssh 2019-08-30 00:26:37.622660 [32551] - INFO TEST: call test_ssh_remote_trust 2019-08-30 00:26:37.626693 [32551] - INFO rsa key ~/.ssh/id_rsa.pub has existed in local host. 2019-08-30 00:26:37.640585 [32551] - DEBUG ssh_default=default root mypassword 22 2019-08-30 00:26:37.644509 [32551] - DEBUG ssh_default_pass=mypassword 2019-08-30 00:26:37.648900 [32551] - INFO ssh_default_user=root, ssh_default_pass=***, ssh_default_port=22 2019-08-30 00:26:37.655670 [32551] - DEBUG _hosts_auth=192.168.1.11,,, 192.168.1.12,,, 192.168.1.13,,, 192.168.1.189,,, 2019-08-30 00:26:37.661105 [32551] - INFO remote host No. 1: 2019-08-30 00:26:37.665205 [32551] - INFO -------------------------- 2019-08-30 00:26:37.676351 [32551] - INFO SSH_HOST=192.168.1.11, SSH_USER=root, SSH_PASS=***, SSH_PORT=22, PARAM_SSH_PORT=-p 22 2019-08-30 00:26:37.680113 [32551] - DEBUG SSH_PASS=mypassword spawn ssh-copy-id -f -p 22 -o StrictHostKeyChecking=no -o ConnectTimeout=3 root@192.168.1.11 /usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/root/.ssh/id_rsa.pub" Number of key(s) added: 1 Now try logging into the machine, with: "ssh -p ' 22' -o 'StrictHostKeyChecking=no' -o 'ConnectTimeout=3' 'root@192.168.1.11'" and check to make sure that only the key(s) you wanted were added. ... 2019-08-30 00:26:38.730754 [32551] - INFO remote hosts have try to trust on this host. All done.
4、验证免密登录:
从被信任主机,发起到信任主机的 SSH 登录。运行脚本后,从容器内可以免密登录到目标主机,功能测试验证成功。为了保护云主机安全,对云主机的公网 IP 地址做了技术性处理。
[root@30ffac589d2f bin]# ssh 192.168.1.11 Last login: Thu Aug 29 20:08:49 2019 from 218.89.*.* Welcome to Alibaba Cloud Elastic Compute Service ! [root@dev-7 ~]#
七、性能测试
性能测试的主要目的是:寻找在给定资源条件下(主要是硬件资源),这个程序的最大事务处理能力(TPS)、完成请求的响应时间以及相应的资源消耗情况。
性能测试方法
性能测试的具体方法是:
1、准备 1 台云虚拟机运行信任登录认证程序,3 台云虚拟机作为授予信任登录的主机。因为负载集中在第一台主机,后三台主机的负载很轻,模拟大规模主机群没有任何压力。
2、准备 10000 条主机数据。因为实际可用主机数远远达不到 10000 条,所以通过循环复制的方式来模拟,间接达到主机数总量。对已建立信任关系的主机,对方会返回已建立信任关系的结果,消耗的时间与第一次建立信任关系时接近,效果接近。
3、调用并发函数 function ssh_remote_trust_concur(),启动多个子任务并发执行。
4、并发子任务数从 3、5、10、15、20、30、40...90、100、200 阶梯式增长,由总控脚本调度多次运行并发脚本函数,分别输出到不同的日志文件。
5、在性能测试期间,启动 nmon 工具连续记录主机资源消耗情况:CPU、网络和 IO 等。
6、编写性能分析脚本,计算给定并发数时的以下数据:
运行时长:执行完所有任务后,最后一行日志时间减去第一行日志时间;
平均单次时长(单个事务的平均处理延迟):运行时长*并发数/主机总数;
平均事务处理能力 TPS(平均吞吐量):主机总数/运行时长;
平均 CPU 利用率:从开始到结束时的 CPU 利用率的算术平均值。
其中:并发数和主机总数(固定为 10050 台)已知。
7、对所有并发数执行第 6 步,汇总得到性能测试表。
8、对第 7 步的性能测试表进行图形化展现,绘制运行时长、平均单次时长、平均吞吐量、平均 CPU 利用率等时间序列的曲线图。
对性能测试数据进行分析,应得出以下结论:
1、在给定硬件资源条件下的最大事务处理能力 TPS;
2、达到最大 TPS 时的并发数范围(最低并发数和最高并发数);
3、达到最大 TPS 以及最小事务延迟条件下的并发数范围或者极点值。
测试数据分析
性能测试分析方法:
1、观察完成全部 10000 台主机消耗的总时间,分别计算事务处理量 TPS。
2、观察同一子任务执行一台主机建立信任关系所需延迟时间,该事件会随着并发子任务数变化。
3、分析并发任务数的临界点:TPS 达到最大值,单台主机延迟时间最小。
性能测试曲线图如下图:
图 非交互式主机信任登录认证的性能测试
横轴是并发任务数;左侧纵轴是时长,对总时长的单位是秒,对单次时长单位是毫秒;右边纵轴是 CPU 利用率和吞吐率,CPU 利用率的单位是百分比,吞吐率的单位是 TPS(每秒的事务处理数)。
把 4 种不同类型的指标数据同时在一张图表示,理解起来稍微有一点难度,但是为了更好地在同一并发数下对比个指标数据值,还是合并在一张图上了。
观察 4 条测试数据曲线,发现如下现象:
1、总时长曲线随着并发数的增加而下降,在 1~5 并发时总时长曲线下降得厉害,在 6~20 并发时总时长曲线逐渐放缓,在并发数大于 20 时总时长接近于一条水平线,稳定在 230 秒左右。
2、单次时长在并发数小于等于 10 个时接近于一条水平线,在 11~20 个并发时单次时长曲线缓慢上升,在并发数大于 20 时单次时长曲线变陡并呈现线性增长。
3、CPU 利用率随着并发数的增加而增长。在并发数小于等于 13 时 CPU 利用率增长较快,在 14~19 个并发时 CPU 利用率增长缓慢,在 20 个并发以上时 CPU 利用率增长极其缓慢直至几乎为零,最后稳定在 98%左右。
4、吞吐率随着并发数的增加而增长。在并发数小于等于13 时吞吐率增长较快,在14~19个并发时吞吐率增长缓慢,在 20 个并发以上时吞吐率增长极其缓慢直至几乎为零,最后稳定在 46 次/秒左右。
数据分析结论如下:
1、建立主机间信任关系是计算型为主的任务。主要计算工作在发起信任认证的主机, 包括数据加密、解密和验证签名等。虽然主机之间的网络通信次数较频繁,但是数据量很少。
2、计算型任务主要消耗主机的 CPU 资源,因此 CPU 资源是决定处理能力的瓶颈。
3、在并发数较低时(1~13 并发),因为可用空闲 CPU 资源较多且进程调度开销较少,CPU 利用率成线性增长,吞吐率也成线性增长。
4、在并发数接近临界区时(14~19 并发),因为可用 CPU 资源减少且进程调度开销增加,CPU 利用率曲线的增长钝化变慢,吞吐率曲线增长也钝化变慢。
5、在并发数进入临界区后(20 以上并发),因为空闲 CPU 资源接近于零,随着并发数的增加 CPU 利用率不会增加,吞吐率也不会增加。相反,单次认证的时长反而会增加,因为超分的并发子任务(进程)竞争 CPU,在队列等待的时间会变长。
6、最佳的并发数在 20 个附近。此时,吞吐率为最大值或者极大值,而且单次认证等待时长最短,CPU 利用率小于且接近 100%(充分利用 CPU 资源),完成所有认证工作的总时长为最短。
八、结束语
工作小结
本文以内地居民跨境旅行为例,形象地引入介绍主机间的信任登录认证,并阐明其信任认证的必要性。建立任意两台主机之间的信任登录认证过程并不复杂,利用批量处理脚本能大大减轻运维管理人员的工作负担。
在面对成千上万台主机需要建立信任关系时,即使批处理脚本也显得力不从心,本文引入多任务并发方式,可以快速建立主机间信任关系。经过实际测试,并发方式最快能在 3.65分钟内建立到 10000 台主机间的信任关系,相比单任务无并发方式需耗时45.73 分钟,并发方式的处理速度达到单任务方式的 12.5 倍,提速效果明显。
下一步工作
建立主机间的信任关系不是目的,本文介绍的程序是一个面向云主机集群故障诊断系统的一部分,此后会通过免登录的方式采集目标主机的日志、性能和状态等等数据,用于后续的故障诊断。当然,这已经超出本文的范围了。
源码下载
源代码托管在 github.com 源码仓库, 源代码随时可能会更新。源码仓库地址是
https://github.com/solomonxu/k8s-diagnose.git,所在分支是 ssh-trust。
用下面的命令可以直接克隆源码分支:
git clone -b ssh-trust https://github.com/solomonxu/k8s-diagnose.git
参考文献:
1、容器技术生态概览,
https://www.jianshu.com/p/453021b7c1ff
2 、The Evolution of Linux Containers and Their Future,
https://dzone.com/articles/evolution-of-linux-containers-future
3 、容 器 化 开 发 及 两 步 法 快 速 构 建 Docker 镜 像 ,
https://mp.weixin.qq.com/s/PinTIENVoCPNhT4SKxRKtQ
原文链接:https://mp.weixin.qq.com/s/QdHHHW5J90J2bdAFKH6O4g
Cantact to the author:
Email: xumeng@wise2c.com
Wechat: solomonxu9999
关于睿云智合
深圳睿云智合科技有限公司成立于2012年,总部位于深圳,并分别在成都、深圳设立了研发中心,北京、上海设立了分支机构,核心骨干人员全部为来自金融、科技行业知名企业资深业务专家、技术专家。早期专注于为中国金融保险等大型企业提供创新技术、电子商务、CRM等领域专业咨询服务。
自2016年始,在率先将容器技术引进到中国保险行业客户后,公司组建了专业的容器技术产品研发和实施服务团队,旨在帮助中国金融行业客户将容器创新技术应用于企业信息技术支持业务发展的基础能力改善与提升,成为中国金融保险行业容器技术服务领导品牌。
此外,凭借多年来在呼叫中心领域的业务经验与技术积累,睿云智合率先在业界推出基于开源软交换平台FreeSwitch的微服务架构多媒体数字化业务平台,将语音、视频、webchat、微信、微博等多种客户接触渠道集成,实现客户统一接入、精准识别、智能路由的CRM策略,并以容器化治理来支持平台的全应用生命周期管理,显著提升了数字化业务处理的灵活、高效、弹性、稳定等特性,为帮助传统企业向“以客户为中心”的数字化业务转型提供完美的一站式整体解决方案。
Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved